第十四章:文件 I/O 和 Python 工具
在本章中,我们将详细讨论文件 I/O,即读取、写入和追加文件。我们还将讨论 Python 工具,这些工具使得操作文件和与操作系统交互成为可能。每个主题都有不同的复杂程度,我们将通过一个例子来讨论。让我们开始吧!
文件 I/O
我们讨论文件 I/O 有两个原因:
- 在 Linux 操作系统的世界中,一切都是文件。与树莓派上的外围设备交互类似于读取/写入文件。例如:在第十二章中,通信接口,我们讨论了串口通信。您应该能够观察到串口通信类似于文件读写操作。
- 我们在每个项目中以某种形式使用文件 I/O。例如:将传感器数据写入 CSV 文件,或者读取 Web 服务器的预配置选项等。
因此,我们认为讨论 Python 中的文件 I/O 作为一个单独的章节会很有用(详细文档请参阅:docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files),并讨论它在开发树莓派 Zero 应用程序时可能发挥作用的示例。
从文件中读取
让我们创建一个简单的文本文件read_file.txt,其中包含以下文本:我正在使用树莓派 Zero 学习 Python 编程,并将其保存到代码示例目录(或您选择的任何位置)。
要从文件中读取,我们需要使用 Python 的内置函数:open来打开文件。让我们快速看一下一个代码片段,演示如何打开一个文本文件以读取其内容并将其打印到屏幕上:
if __name__ == "__main__": # open text file to read file = open('read_line.txt', 'r') # read from file and store it to data data = file.read() print(data) file.close()
让我们详细讨论这段代码片段:
- 读取文本文件内容的第一步是使用内置函数
open打开文件。需要将所需的文件作为参数传递,并且还需要一个标志r,表示我们打开文件以读取内容(随着我们讨论每个读取/写入文件时,我们将讨论其他标志选项)。 - 打开文件时,
open函数返回一个指针(文件对象的地址),并将其存储在file变量中。
file = open('read_line.txt', 'r')
- 这个文件指针用于读取文件的内容并将其打印到屏幕上:
data = file.read() print(data)
- 读取文件的内容后,通过调用
close()函数关闭文件。
运行前面的代码片段(可与本章一起下载的read_from_file.py)使用 IDLE3 或命令行终端。文本文件的内容将如下打印到屏幕上:
I am learning Python Programming using the Raspberry Pi Zero
读取行
有时,有必要逐行读取文件的内容。在 Python 中,有两种选项可以做到这一点:readline()和readlines():
readline(): 正如其名称所示,这个内置函数使得逐行读取成为可能。让我们通过一个例子来复习一下:
if __name__ == "__main__": # open text file to read file = open('read_line.txt', 'r') # read a line from the file data = file.readline() print(data) # read another line from the file data = file.readline() print(data) file.close()
当执行前面的代码片段(可与本章一起下载,文件名为read_line_from_file.py)时,read_line.txt文件被打开,并且readline()函数返回一行。这一行被存储在变量 data 中。由于该函数在程序中被调用两次,输出如下:
I am learning Python Programming using the Raspberry Pi Zero. This is the second line.
每次调用readline函数时都会返回一个新行,并且当到达文件结尾时会返回一个空字符串。
readlines(): 这个函数逐行读取文件的全部内容,并将每一行存储到一个列表中:
if __name__ == "__main__": # open text file to read file = open('read_lines.txt', 'r') # read a line from the file data = file.readlines() for line in data: print(line) file.close()
由于文件的行被存储为一个列表,可以通过对列表进行迭代来检索它:
data = file.readlines() for line in data: print(line)
前面的代码片段可与本章一起下载,文件名为read_lines_from_file.py。
写入文件
按照以下步骤进行写入文件:
- 写入文件的第一步是使用写入标志
w打开文件。如果作为参数传递的文件名不存在,将创建一个新文件:
file = open('write_file.txt', 'w')
- 文件打开后,下一步是将要写入的字符串作为参数传递给
write()函数:
file.write('I am excited to learn Python using Raspberry Pi Zero')
- 让我们将代码放在一起,我们将一个字符串写入文本文件,关闭它,重新打开文件并将文件的内容打印到屏幕上:
if __name__ == "__main__": # open text file to write file = open('write_file.txt', 'w') # write a line from the file file.write('I am excited to learn Python using Raspberry Pi Zero \n') file.close() file = open('write_file.txt', 'r') data = file.read() print(data) file.close()
- 前面的代码片段可与本章一起下载(
write_to_file.py)。 - 当执行前面的代码片段时,输出如下所示:
I am excited to learn Python using Raspberry Pi Zero
追加到文件
每当使用写入标志w打开文件时,文件的内容都会被删除,并重新打开以写入数据。还有一个叫做a的替代标志,它使得可以将数据追加到文件的末尾。如果打开的文件(作为打开的参数)不存在,这个标志也会创建一个新文件。让我们考虑下面的代码片段,我们将一行追加到上一节中的文本文件write_file.txt中:
if __name__ == "__main__": # open text file to append file = open('write_file.txt', 'a') # append a line from the file file.write('This is a line appended to the file\n') file.close() file = open('write_file.txt', 'r') data = file.read() print(data) file.close()
当执行前面的代码片段(可与本章一起下载的append_to_file.py)时,字符串This is a line appended to the file将被追加到文件的文本末尾。文件的内容将包括以下内容:
I am excited to learn Python using Raspberry Pi Zero This is a line appended to the file
寻找
一旦文件被打开,文件 I/O 中使用的文件指针会从文件的开头移动到文件的末尾。可以将指针移动到特定位置并从该位置读取数据。当我们对文件的特定行感兴趣时,这是非常有用的。让我们考虑上一个例子中的文本文件write_file.txt。文件的内容包括:
I am excited to learn Python using Raspberry Pi Zero This is a line appended to the file
让我们尝试跳过第一行,只读取第二行,使用seek:
if __name__ == "__main__": # open text file to read file = open('write_file.txt', 'r') # read the second line from the file file.seek(53) data = file.read() print(data) file.close()
在前面的例子中(可与本章一起下载的seek_in_file.py),seek函数用于将指针移动到字节53,即第一行的末尾。然后文件的内容被读取并存储到变量中。当执行这个代码片段时,输出如下所示:
This is a line appended to the file
因此,seek 使得移动文件指针到特定位置成为可能。
读取 n 个字节
seek函数使得将指针移动到特定位置并从该位置读取一个字节或n个字节成为可能。让我们重新阅读write_file.txt,并尝试读取句子I am excited to learn Python using Raspberry Pi Zero中的单词excited。
if __name__ == "__main__": # open text file to read and write file = open('write_file.txt', 'r') # set the pointer to the desired position file.seek(5) data = file.read(1) print(data) # rewind the pointer file.seek(5) data = file.read(7) print(data) file.close()
前面的代码可以通过以下步骤来解释:
- 第一步,使用
read标志打开文件,并将文件指针设置为第五个字节(使用seek)——文本文件内容中字母e的位置。 - 现在,我们通过将文件作为参数传递给
read函数来从文件中读取一个字节。当整数作为参数传递时,read函数会从文件中返回相应数量的字节。当没有传递参数时,它会读取整个文件。如果文件为空,read函数会返回一个空字符串:
file.seek(5) data = file.read(1) print(data)
- 在第二部分中,我们尝试从文本文件中读取单词
excited。我们将指针的位置倒回到第五个字节。然后我们从文件中读取七个字节(单词excited的长度)。 - 当执行代码片段时(可与本章一起下载的
seek_to_read.py),程序应该打印字母e和单词excited:
file.seek(5) data = file.read(7) print(data)
r+
我们讨论了使用r和w标志读取和写入文件。还有另一个叫做r+的标志。这个标志使得可以对文件进行读取和写入。让我们回顾一个例子,以便理解这个标志。
让我们再次回顾write_file.txt的内容:
I am excited to learn Python using Raspberry Pi Zero This is a line appended to the file
让我们修改第二行,改为:This is a line that was modified。代码示例可与本章一起下载(seek_to_write.py)。
if __name__ == "__main__": # open text file to read and write file = open('write_file.txt', 'r+') # set the pointer to the desired position file.seek(68) file.write('that was modified \n') # rewind the pointer to the beginning of the file file.seek(0) data = file.read() print(data) file.close()
让我们回顾一下这个例子是如何工作的:
- 这个例子的第一步是使用
r+标志打开文件。这使得可以对文件进行读取和写入。 - 接下来是移动到文件的第 68 个字节
- 在这个位置将
that was modified字符串写入文件。字符串末尾的空格用于覆盖第二句原始内容。 - 现在,文件指针已设置到文件的开头,并读取其内容。
- 当执行前面的代码片段时,修改后的文件内容将打印到屏幕上,如下所示:
I am excited to learn Python using Raspberry Pi Zero This is a line that was modified
还有另一个a+标志,它可以使数据追加到文件末尾并同时进行读取。我们将留给读者使用到目前为止讨论的示例来弄清楚这一点。
我们已经讨论了 Python 中读取和写入文件的不同示例。如果没有足够的编程经验,可能会感到不知所措。我们强烈建议通过本章提供的不同代码示例进行实际操作。
读者的挑战
使用a+标志打开write_file.txt文件(在不同的示例中讨论),并向文件追加一行。使用seek设置文件指针并打印其内容。您可以在程序中只打开文件一次。
使用with关键字
到目前为止,我们讨论了可以用于以不同模式打开文件的不同标志。我们讨论的示例遵循一个常见模式——打开文件,执行读/写操作,然后关闭文件。有一种优雅的方式可以使用with关键字与文件交互。
如果在与文件交互的代码块执行过程中出现任何错误,with关键字会确保在退出代码块时关闭文件并清理相关资源。让我们通过一个示例来回顾with关键字:
if __name__ == "__main__": with open('write_file.txt', 'r+') as file: # read the contents of the file and print to the screen print(file.read()) file.write("This is a line appended to the file") #rewind the file and read its contents file.seek(0) print(file.read()) # the file is automatically closed at this point print("Exited the with keyword code block")
在前面的示例(with_keyword_example)中,我们跳过了关闭文件,因为with关键字在缩进的代码块执行完毕后会自动关闭文件。with关键字还会在由于错误离开代码块时关闭文件。这确保了资源在任何情况下都能得到适当的清理。接下来,我们将使用with关键字进行文件 I/O。
configparser
让我们讨论一些在使用树莓派开发应用程序时特别有用的 Python 编程方面。其中一个工具是 Python 中提供的configparser。configparser模块(docs.python.org/3.4/library/configparser.html)用于读取/写入应用程序的配置文件。
在软件开发中,配置文件通常用于存储常量,如访问凭据、设备 ID 等。在树莓派的上下文中,configparser可以用于存储所有使用的 GPIO 引脚列表,通过 I²C 接口接口的传感器地址等。让我们讨论三个示例,学习如何使用configparser模块。在第一个示例中,我们将使用configparser创建一个config文件。
在第二个示例中,我们将使用configparser来读取配置值,在第三个示例中,我们将讨论修改配置文件的最终示例。
示例 1:
在第一个示例中,让我们创建一个配置文件,其中包括设备 ID、使用的 GPIO 引脚、传感器接口地址、调试开关和访问凭据等信息:
import configparser if __name__ == "__main__": # initialize ConfigParser config_parser = configparser.ConfigParser() # Let's create a config file with open('raspi.cfg', 'w') as config_file: #Let's add a section called ApplicationInfo config_parser.add_section('AppInfo') #let's add config information under this section config_parser.set('AppInfo', 'id', '123') config_parser.set('AppInfo', 'gpio', '2') config_parser.set('AppInfo', 'debug_switch', 'True') config_parser.set('AppInfo', 'sensor_address', '0x62') #Let's add another section for credentials config_parser.add_section('Credentials') config_parser.set('Credentials', 'token', 'abcxyz123') config_parser.write(config_file) print("Config File Creation Complete")
让我们详细讨论前面的代码示例(可与本章一起下载作为config_parser_write.py):
- 第一步是导入
configparser模块并创建ConfigParser类的实例。这个实例将被称为config_parser:
config_parser = configparser.ConfigParser()
- 现在,我们使用
with关键字打开名为raspi.cfg的配置文件。由于文件不存在,将创建一个新的配置文件。 - 配置文件将包括两个部分,即
AppInfo和Credentials。 - 可以使用
add_section方法创建两个部分,如下所示:
config_parser.add_section('AppInfo') config_parser.add_section('Credentials')
- 每个部分将包含不同的常量集。可以使用
set方法将每个常量添加到相关部分。set方法的必需参数包括参数/常量将位于的部分名称,参数/常量的名称及其对应的值。例如:id参数可以添加到AppInfo部分,并分配值123如下:
config_parser.set('AppInfo', 'id', '123')
- 最后一步是将这些配置值保存到文件中。这是使用
config_parser方法write完成的。一旦程序退出with关键字下的缩进块,文件就会关闭:
config_parser.write(config_file)
我们强烈建议尝试自己尝试代码片段,并将这些片段用作参考。通过犯错误,您将学到很多,并可能得出比这里讨论的更好的解决方案。
执行上述代码片段时,将创建一个名为raspi.cfg的配置文件。配置文件的内容将包括以下内容所示的内容:
[AppInfo] id = 123 gpio = 2 debug_switch = True sensor_address = 0x62 [Credentials] token = abcxyz123
示例 2:
让我们讨论一个示例,我们从先前示例中创建的配置文件中读取配置参数:
import configparser if __name__ == "__main__": # initialize ConfigParser config_parser = configparser.ConfigParser() # Let's read the config file config_parser.read('raspi.cfg') # Read config variables device_id = config_parser.get('AppInfo', 'id') debug_switch = config_parser.get('AppInfo', 'debug_switch') sensor_address = config_parser.get('AppInfo', 'sensor_address') # execute the code if the debug switch is true if debug_switch == "True": print("The device id is " + device_id) print("The sensor_address is " + sensor_address)
如果配置文件以所示格式创建,ConfigParser类应该能够解析它。实际上并不一定要使用 Python 程序创建配置文件。我们只是想展示以编程方式同时为多个设备创建配置文件更容易。
上述示例可与本章一起下载(config_parser_read.py)。让我们讨论一下这个代码示例是如何工作的:
- 第一步是初始化名为
config_parser的ConfigParser类的实例。 - 第二步是使用实例方法
read加载和读取配置文件。 - 由于我们知道配置文件的结构,让我们继续阅读位于
AppInfo部分下可用的一些常量。可以使用get方法读取配置文件参数。必需的参数包括配置参数所在的部分以及参数的名称。例如:配置id参数位于AppInfo部分下。因此,该方法的必需参数包括AppInfo和id:
device_id = config_parser.get('AppInfo', 'id')
- 现在配置参数已读入变量中,让我们在程序中使用它。例如:让我们测试
debug_switch变量(用于确定程序是否处于调试模式)并打印从文件中检索到的其他配置参数:
if debug_switch == "True": print("The device id is " + device_id) print("The sensor_address is " + sensor_address)
示例 3:
让我们讨论一个示例,我们想要修改现有的配置文件。这在需要在执行固件更新后更新配置文件中的固件版本号时特别有用。
以下代码片段可与本章一起下载,文件名为config_parser_modify.py:
import configparser if __name__ == "__main__": # initialize ConfigParser config_parser = configparser.ConfigParser() # Let's read the config file config_parser.read('raspi.cfg') # Set firmware version config_parser.set('AppInfo', 'fw_version', 'A3') # write the updated config to the config file with open('raspi.cfg', 'w') as config_file: config_parser.write(config_file)
让我们讨论一下这是如何工作的:
- 与往常一样,第一步是初始化
ConfigParser类的实例。使用read方法加载配置文件:
# initialize ConfigParser config_parser = configparser.ConfigParser() # Let's read the config file config_parser.read('raspi.cfg')
- 使用
set方法更新必需参数(在先前的示例中讨论):
# Set firmware version config_parser.set('AppInfo', 'fw_version', 'A3')
- 使用
write方法将更新后的配置保存到配置文件中:
with open('raspi.cfg', 'w') as config_file: config_parser.write(config_file)
读者的挑战
使用示例 3 作为参考,将配置参数debug_switch更新为值False。重复示例 2,看看会发生什么。
读取/写入 CSV 文件
在本节中,我们将讨论读取/写入 CSV 文件。这个模块(docs.python.org/3.4/library/csv.html)在数据记录应用程序中非常有用。由于我们将在下一章讨论数据记录,让我们回顾一下读取/写入 CSV 文件。
写入 CSV 文件
让我们考虑一个场景,我们正在从不同的传感器读取数据。这些数据需要记录到一个 CSV 文件中,其中每一列对应于来自特定传感器的读数。我们将讨论一个例子,其中我们在 CSV 文件的第一行记录值123、456和789,第二行将包括值Red、Green和Blue:
- 写入 CSV 文件的第一步是使用
with关键字打开 CSV 文件:
with open("csv_example.csv", 'w') as csv_file:
- 下一步是初始化 CSV 模块的
writer类的实例:
csv_writer = csv.writer(csv_file)
- 现在,通过创建一个包含需要添加到行中的所有元素的列表,将每一行添加到文件中。例如:第一行可以按如下方式添加到列表中:
csv_writer.writerow([123, 456, 789])
- 将所有内容放在一起,我们有:
import csv if __name__ == "__main__": # initialize csv writer with open("csv_example.csv", 'w') as csv_file: csv_writer = csv.writer(csv_file) csv_writer.writerow([123, 456, 789]) csv_writer.writerow(["Red", "Green", "Blue"])
- 当执行上述代码片段(与本章一起提供的
csv_write.py可下载)时,在本地目录中创建了一个 CSV 文件,其中包含以下内容:
123,456,789 Red,Green,Blue
从 CSV 文件中读取
让我们讨论一个例子,我们读取上一节中创建的 CSV 文件的内容:
- 读取 CSV 文件的第一步是以读模式打开它:
with open("csv_example.csv", 'r') as csv_file:
- 接下来,我们初始化 CSV 模块的
reader类的实例。CSV 文件的内容被加载到对象csv_reader中:
csv_reader = csv.reader(csv_file)
- 现在 CSV 文件的内容已加载,可以按如下方式检索 CSV 文件的每一行:
for row in csv_reader: print(row)
- 将所有内容放在一起:
import csv if __name__ == "__main__": # initialize csv writer with open("csv_example.csv", 'r') as csv_file: csv_reader = csv.reader(csv_file) for row in csv_reader: print(row)
- 当执行上述代码片段(与本章一起提供的
csv_read.py可下载)时,文件的内容将逐行打印,其中每一行都是一个包含逗号分隔值的列表:
['123', '456', '789'] ['Red', 'Green', 'Blue']
Python 物联网入门指南(五)(2)https://developer.aliyun.com/article/1507234