超简单的Python教程系列——第12篇:文件处理

Posted 飞天程序猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超简单的Python教程系列——第12篇:文件处理相关的知识,希望对你有一定的参考价值。

超简单的Python教程系列——第12篇:文件处理_python教程

对我来说,在程序开始读取或写入外部文件之前,任何项目都不会开始感觉“真实”。

不幸的是,这是教程过度简化最容易造成的问题之一。要是有人能在一个地方用文件记录下所有的怪事就好了……


打开文件

让我们从最基本的文件读取示例开始。假设我在与我的代码文件相同的目录中有一个文件,名为​​journal1.txt​​.

打开文件的标准方式是使用内置​​open()​​​函数,默认从​​io​​模块导入。

file = open("journal1.txt", r)
for line in file:
print(line)
file.close()

​open()​​函数接受许多参数,用于以高级方式与文件交互,但大多数情况下,你只需要前两个参数

第一个参数​​file​​接受一个字符串,该字符串包含打开的文件的绝对路径或相对路径。这是唯一严格要求的参数。

第二个参数​​mode​​接受一个指示文件模式的字符串。如果未指定,将使用​​rt​​​,这意味着它将文件作为文本读取。模式​​r​​​实际上是相同的,因为文本模式 ( ​​t​​) 是默认行为的一部分。

我可以用这行来代替,得到同样的行为。。。

file = open("journal1.txt")

但我个人更喜欢明确指出我是在阅读(​​r​​​),写作(​​w​​),还是你有什么。

open()返回的所有“文件”对象都是可迭代的。对于文本文件,返回一个​​TextIOWrapper​​​对象。在上面的示例中,我遍历​​TextIOWrapper​​​对象文件中的行​,并打印出每一行。

处理完文件后,我需要使用​​file.close()​​关闭它。 重要的是不要依赖垃圾收集器为你关闭文件,因为这种行为既不能保证也不方便实现。此外,Python 不能保证在调用​​.close()​​之前完成对文件的写入。

运行该代码,至少在我的情况下,不会打印出​​journal1.txt​​内容:

Could this be where all missing things are found?

Magic Hair Only for the Pure of Heart

Not naturally occurring?

Could result in minor gravity anomalies!


上下文管理器

在实践中,始终记住调用​​close()​​可能是一件非常痛苦的事情,尤其是当你考虑到打开文件时可能出现的错误时。值得庆幸的是,还有一种更简洁的方法:上下文管理器

上下文管理器由Python 中的​​with​​语句定义。我可以使用以下语法重写我之前的代码:

with open("journal1.txt", r) as file:
for line in file:
print(line)

调用​​open()​​​函数,如果成功,则生成的​​TextIOWrapper​​​对象存储在 中​​file​​​,并且可以在​​with​​​语句的主体中使用。一旦控制离开​​with​​​语句,​​file.close()​​就会被隐式调用;你永远不必记得调用它!

我们将在下一章更深入地介绍这一点。


文件模式

该文档提到了几种可用于​​open()​​的模式:

  • ​r​​打开文件进行读取(默认)。
  • ​w​​打开或创建文件以首先写入、删除(截断)其内容。
  • ​a​​打开或创建要写入的文件,但追加到末尾而不是截断。
  • ​x​​创建并打开一个新文件进行写入;它无法打开现有文件。
  • ​+​​打开文件进行读写(见下表)。
  • ​t​​以文本模式处理文件(默认)。
  • ​b​​以二进制模式处理文件。

这些模式标志可以组合在一起。例如,在二进制模式下,a+b允许写入和读取,其中写入附加到文件末尾。

+标志始终与另一标志组合。当与​​r​​​结合使用时,它添加了​​a​​​的功能,但它从文件开头开始(不截断)。当与​​w​​​、​​a​​​或​​x​​组合时,它也允许读取。

使用此表可以最好地理解不同标志的行为:

| r   r+   w   w+   a   a+   x   x+
---------------------|----------------------------------
allow read |
allow write |
create new file |
open existing file |
erase file contents |
allow seek |
position at start |
position at end |


我们可以使用​​read()​​​,​​readline()​​​或者​​readlines()​​函数以文本模式读取文件,也可以直接迭代

当然,这需要使用适当的文件模式标志打开文件进行读取(请参阅“文件模式”部分)。如果需要检查对象文件是否可以读取,请使用​​file.readable()​​函数。

让我们对比一下读取文件的三种方式:


read()

​read()​​函数将文件的全部内容作为一个长字符串读取。

with open("journal1.txt", r) as file:
contents = file.read()
print(contents)

# Could this be where all missing things are found?
# Magic Hair Only for the Pure of Heart
# Not naturally occurring?
# Could result in minor gravity anomalies!

或者,你可以告诉​​read()​​从文件流中读取的最大字符数:

with open("journal1.txt", r) as file:
contents = file.read(20)
print(contents)

# Could this be where


readline()

​readline()​​​函数的行为与​​read()​​完全相同,只是它在遇到换行符时停止读取。换行符包含在返回的字符串中。

with open("journal1.txt", r) as file:
contents = file.readline()
print(contents)

# Could this be where all missing things are found?

与​​read()​​一样,你可以指定要读取的最大字符数:

with open("journal1.txt", r) as file:
contents = file.readline(20)
print(contents)

# Could this be where


readlines()

​readlines()​​函数以字符串列表的形式返回整个文件,每个字符串为一行。

with open("journal1.txt", r) as file:
contents = file.readlines()
for c in contents:
print(c)

# Could this be where all missing things are found?
#
# Magic Hair Only for the Pure of Heart
#
# Not naturally occurring?
#
# Could result in minor gravity anomalies!
#

你会注意到每一行都包含换行符。我们可以通过在每个字符串上调用​​.strip()​​函数来删除它。

with open("journal1.txt", r) as file:
contents = file.readlines()
for c in contents:
print(c.strip())

# Could this be where all missing things are found?
# Magic Hair Only for the Pure of Heart
# Not naturally occurring?
# Could result in minor gravity anomalies!

你还可以通过指定最大字符数来限制从文件中读取的内容。然而,与以前不同的是,这不是硬性限制。相反,一旦到目前为止从所有行读取的字符总数超过了指定的限制,则只会读取当前行的其余部分。

通过比较​​read()​​​和​​readlines()​​可以最好地理解这一点​。首先,我将阅读限制为60个字符:

with open("journal1.txt", r) as file:
contents = file.read(60)
print(contents)

# Could this be where all missing things are found?
# Magic Hair

将​​readlines()​​与使用 60 个字符的“提示”进行比较:

with open("journal1.txt", r) as file:
contents = file.readlines(60)
for c in contents:
print(c.strip())

# Could this be where all missing things are found?
# Magic Hair Only for the Pure of Heart

在第二个示例中,读取前两行的全部内容,但不再读取。

与其他两个函数不同,​​readlines()​​总是读取整行。


迭代

如你之前所见,我们可以直接迭代文件:

with open("journal1.txt", r) as file:
for line in file:
print(line)

# Could this be where all missing things are found?
# Magic Hair Only for the Pure of Heart
# Not naturally occurring?
# Could result in minor gravity anomalies!

这在功能上与以下内容相同:

with open("journal1.txt", r) as file:
for line in file.readlines():
print(line)

两者的区别在于第一种方法,直接迭代,是惰性的,而第二种方法在迭代内容之前首先读取整个文件。


使用​​write()​​​or​​writelines()​​函数,我们可以以几乎相同的方式写入文件。

这需要打开文件进行写入(参见“文件模式”部分)。​​file.writable()​​​函数可用于检查​​file​​对象是否可写。

在本节的示例中,我将在底部的注释中显示文件内容。


write()

​write()​​函数将给定的行写入文件。

我可以使用​​write()​​​将整个多行字符串写入一个名为​​journal3.txt​​的新文件​,如下所示:

entry = """If you go on enough road trips
chances are, youve seen a
certain bumper sticker:
WHAT IS THE MYSTERY SHACK?
"""

with open("journal3.txt", x) as file:
file.write(entry)

# If you go on enough road trips
# chances are, youve seen a
# certain bumper sticker:
# WHAT IS THE MYSTERY SHACK?
#

只要​​journal3.txt​​不存在,它将使用给定的内容创建。

我可以使用​​w​​​文件模式覆盖​​journal3.txt​​的全部内容:

with open("journal3.txt", w) as file:
file.write("GNOMES\\nWEAKNESS?\\n")

# GNOMES
# WEAKNESS?
#

注意:注意你的文件模式!​​w​​​并将​​w+​​​删除文件的全部内容。使用​​a​​​或​​a+​​写入文件末尾。

我可以使用​​a​​文件模式附加到文件中:

with open("journal3.txt", a) as file:
file.write("leaf blowers\\n")

# GNOMES
# WEAKNESS?
# leaf blowers
#

​write()​​函数返回一个整数,表示写入的字符数。


writelines()

​writelines()​​函数将字符串列表写入文件。

lines = [
"Finally back safe and sound\\n",
"from one of the weirdest days\\n",
"at Gravity Falls.\\n"
]

with open("journal3.txt", w) as file:
file.writelines(lines)

# Finally back safe and sound
# from one of the weirdest days
# at Gravity Falls.
#

与 with​​write()​​​ 不同​,​​writelines()​​​函数只返回​​None​​。


搜索

​file.seek()​​​函数允许你在文件对象​​file​​中逐个字符地来回移动。处理文本流时,它接受一个参数:一个正整数,表示要移动到的新位置,表示为从开头开始的字符数。

除了改变位置之外,该​​file.seek()​​​函数还将返回一个表示文件中新绝对位置的整数。你可用​​file.tell()​​来获取该文件的当前位置。

​r+​​​文件模式最好与函数​​seek()​​​一起使用,​尽管它可以与 ​​a​​​和​​a+​​之外的任何其他文件模式一起使用。

我将首先使用​​seek()​​​函数来读取​​journal1.txt​​文件的一部分:

with open("journal1.txt", r) as file:
file.seek(50)
contents = file.read(5)
print(contents)

# MAGIC

我将编写​​journal3.txt​​文件的新初始版本:

with open("journal3.txt", w) as file:
file.write("FLOATING EYEBALLS")

# FLOATING EYEBALLS

我可以使用该​​r+​​模式更改此文件的一部分。

注意:​write()​​命令将始终覆盖文件的现有内容,除非你追加到末尾。要将文本非破坏性地插入文件,通常最好将整个内容作为字符串(或列表)读取,编辑字符串,然后将其写回。

在这里,我将用“NONSENSE!”替换“EYEBALLS”这个词:

with open("journal3.txt", r+) as file:
file.seek(9)
file.write("NONSENSE!")

# FLOATING NONSENSE!

打开文件后,我从头移动到第 9 个字符,然后​​write()​​是新数据。


用二进制搜索

当你以二进制模式 ( ​​b​​) 打开文件时,你可以以更动态的方式在文件中移动,使用两个参数而不是一个参数:

  • ​offset​​:字符移动的距离(可以是负数)
  • ​whence​​​: 计​​算偏移量的位置:​​0​​​表示文件的开始位置(默认),​​1​​​表示当前位置,​​2​​表示文件的结束位置。

不幸的是,使用​​whence​​参数不适用于以文本模式打开的文件。


文件错误

与处理文件相关的四个最常见错误如下:


FileNotFoundError

​r​​​和​​r+​​​模式要求文件在打开之前存在​。​否则,将引发​​FileNotFoundError​​错误:

try:
with open("notreal.txt", r) as file:
print(file.read())
except FileNotFoundError as e:
print(e)


FileExistsError

​x​​​和​​x+​​​文件模式专门用于创建新文件。如果文件已存在,将引发​​FileExistsError​​错误:

try:
with open("journal3.txt", x+) as file:
print(file.read())
except FileExistsError as e:
print(e)


UnsupportedOperation

每当你尝试读取仅打开用于写入的文件或写入仅打开用于读取的文件时都会引发错误​​io.UnsupportedOperation​​:

import io

try:
with open("journal3.txt", w) as file:
print(file.read())
except io.UnsupportedOperation as e:
print(e)


try:
with open("journal3.txt", r) as file:
file.write()
except io.UnsupportedOperation as e:
print(e)

换行符的问题

一些聪明的读者会记得,虽然UNIX使用​​\\n​​​作为行分隔符,但Windows使用​​\\r\\n​​。当我们读写文件时,这肯定很重要,对吧?

事实上,Python 在幕后为我们抽象了这一点。无论操作系统如何,在以文本模式写入文件时始终用作行分隔符!\\n


文件路径

到目前为止,我只使用了与代码相同文件夹中的文件,但这很少是我们想要的!我们需要能够构建文件路径。

问题是,所有系统上的文件路径都不相同。UNIX风格的系统,如macOS和Linux,使用UNIX文件路径约定,而Windows使用完全不同的方案。我们的解决方案必须对两者都有效,这意味着硬路径不是一个选项。

为了解决这个问题,Python 提供了两个模块:​​os​​​和​​pathlib​​。


创建路径

Python实际上提供了多个用于构建路径的类,这取决于你的特定需求。但是,在大多数情况下,你应该只使用​​pathlib.Path​​。

假设我想在当前用户的主文件夹中创建一个名为​​.dead_simple_python​​的特殊目录,然后将文件写入该位置。我会这样做:

首先,我创建一个​​Path()​​对象,指向最终所需的目录(而不是文件)。

在​​Path()​​​构造函数中,我将路径的每个部分作为单独的字符串传递。我可以使用类方法​​Path.home()​​来获取用户目录的路径。

from pathlib import Path
import os

file_path = Path(Path.home(), ".dead_simple_python")

接下来,我将使用​​file_path.exists()​​​检查路径是否已经存在。如果它不存在,我将使用​​os.makedirs​​函数用于创建路径中缺少的任何目录:

if not file_path.exists():
os.makedirs(file_path)

最后,我可以将文件名添加到已经拥有的路径对象中,然后打开该文件进行写入:

file_path = file_path.joinpath("journal4.txt")

with file_path.open(w) as file:
lines = [
"If youve ever taken a road trip \\n",
"through the Pacific Northwest, youve \\n",
"probably seen a bumper sticker for a \\n",
"place called Gravity Falls.\\n"
]
file.writelines(lines)

你会注意到我使用了​​file_path.open(w)​​​,而不是​​open(file_path, w)​​。从技术上讲,两者的作用完全相同,尽管成员函数是首选。


相对路径

​open("journal1.txt")​​之所以有效,是因为它是一个相对路径,从执行代码的目录开始。

如果我的代码所在的目录中有一个​​journals/​​目录,我可以使用它:

from pathlib import Path

file_path = Path("journals", "journal1.txt")

with file_path.open(r) as file:
print(file.read())

只要我不是从绝对路径开始,比如由​​Path.home()​​生成的路径,路径都是相对的。

但是如果我想向上移动一个目录而不是向下移动呢?你可能会尝试使用​​..​​​,但正如你可能猜到的,这并不能保证在所有操作系统上都是可移动的。相反,我可以使用​​os.pardir​​移动到上一个目录。

想象一下,我们有一个如下所示的目录结构:

example
├── code
└── read_file.py
└── journals
└── journal1.txt

如果在​​path_relative2/code​​​运行​​python read_file.py​​​,我可以通过以下方式访问​​journal1.txt​​:

from pathlib import Path
import os

file_path = Path(os.pardir, "journals", "journal1.txt")

with file_path.open(r) as file:
print(file.read())


总结

我们只是简单地介绍了如何处理文件,但希望这已经解开了​​open()​​​函数和​​Path​​对象的神秘面纱。下面是快速回顾: