一、I/O操作概述
I/O在计算机中是指Input/Output,也就是Stream(流)的输入和输出。这里的输入和输出是相对于内存来说的,Input Stream(输入流)是指数据从外(磁盘、网络)流进内存,Output Stream是数据从内存流出到外面(磁盘、网络)。程序运行时,数据都是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方(通常是磁盘、网络操作)就需要使用 I/O接口。
那么这个IO接口是由谁提供呢?高级编程语言中的IO操作是如何实现的呢?
操作系统是个通用的软件程序,其通用目的如下:
- 硬件驱动
- 进程管理
- 内存管理
- 网络管理
- 安全管理
- I/O管理
操作系统屏蔽了底层硬件,向上提供通用接口。因此,操作I/O的能力是由操作系统的提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来供开发者使用,Python也不例外。
二、文件读写实现原理与操作步骤
1. 文件读写实现原理
文件读写就是一种常见的IO操作。那么根据上面的描述,可以推断python也应该封装操作系统的底层接口,直接提供了文件读写相关的操作方法。事实上,也确实如此,而且Java、PHP等其他语言也是。
那么我们要操作的对象是什么呢?我们又如何获取要操作的对象呢?
由于操作I/O的能力是由操作系统提供的,且现代操作系统不允许普通程序直接操作磁盘,所以读写文件时需要请求操作系统打开一个对象(通常被称为文件描述符--file descriptor, 简称fd),这就是我们在程序中要操作的文件对象。
通常高级编程语言中会提供一个内置的函数,通过接收"文件路径"以及“文件打开模式”等参数来打开一个文件对象,并返回该文件对象的文件描述符。因此通过这个函数我们就可以获取要操作的文件对象了。这个内置函数在Python中叫open(), 在PHP中叫fopen()。
2. 文件读写操作步骤
不同的编程语言读写文件的操作步骤大体都是一样的,都分为以下几个步骤:
1)打开文件,获取文件描述符
2)操作文件描述符--读/写
3)关闭文件
只是不同的编程语言提供的读写文件的api是不一样的,有些提供的功能比较丰富,有些比较简陋。
需要注意的是:文件读写操作完成后,应该及时关闭。一方面,文件对象会占用操作系统的资源;另外一方面,操作系统对同一时间能打开的文件描述符的数量是有限制的,在Linux操作系统上可以通过ulimit -n
来查看这个显示数量。如果不及时关闭文件,还可能会造成数据丢失。因为我将数据写入文件时,操作系统不会立刻把数据写入磁盘,而是先把数据放到内存缓冲区异步写入磁盘。当调用close方法时,操作系统会保证把没有写入磁盘的数据全部写到磁盘上,否则可能会丢失数据。
三、文件操作
文件包含两种类型:文本文件和二进制文件。文本文件有UTF-8编码,内容容易显示和阅读。
二进制文件有0和1比特组成,没有统一的字符编码。
无论文件创建为那种格式,都可以用“文本文件格式”或“二进制格式”打开,打开后的操作有所不同。
python对于文件的操作:打开文件→读取/写入→关闭文件
f = open("python.txt","rb") # 以二进制文件的格式打开
print(f.readline())
f.close()
b'\xe6\x96\x87\xe6\x9c\xac\xe6\x96\x87\xe4\xbb\xb6\xe4\xb8\x8e\xe4\xba\x8c\xe8\xbf\x9b\xe5\x88\xb6\xe6\x96\x87\xe4\xbb\xb6'
f = open("python.txt","rt") # 以文本文件的格式打开
print(f.readline())
f.close()
文本文件与二进制文件
【拓展】注意,实际应用中,我们经常获取文件的具体位置。此时,为了避免出现的各种error(比如文件找不到Error), 有几点注意:
(1)使用关键字 r , 限定文件路径格式识别
with open(r"C:\Users\xxxx\Desktop\e_python\py.log",mode='r', buffering= -1, encoding = 'utf-8') as f:
for line in f:
print (line)
if '蜀' in line:
#break
pass
else:
print('文件中所有行,均打印完毕!')
(2)使用 \\ 格式
with open("C:\\Users\\xxxx\\Desktop\\e_python\\py.log",mode='r', buffering= -1, encoding = 'utf-8') as f:
(3)使用 / 格式
with open("C:/Users/xxxx/Desktop/e_python/py.log",mode='r', buffering= -1, encoding = 'utf-8') as f:
(4)关于文件名字
window 下,文件名字一般不带后缀,所以我们要在代码中使用完整的文件名+文件后缀格式
3.1 打开方式模式
文件打开模式 | 描述 |
---|---|
r | 以只读模式打开文件,并将文件指针指向文件头;如果文件不存在会报错 |
w | 以只写模式打开文件,并将文件指针指向文件头;如果文件存在则将其内容清空,如果文件不存在则创建 |
a | 以只追加可写模式打开文件,并将文件指针指向文件尾部;如果文件不存在则创建 |
r+ | 在r的基础上增加了可写功能 |
w+ | 在w的基础上增加了可读功能 |
a+ | 在a的基础上增加了可读功能 |
b | 读写二进制文件(默认是t,表示文本),需要与上面几种模式搭配使用,如ab,wb, ab, ab+(POSIX系统,包括Linux都会忽略该字符) |
x | 创建写模式,当文件不存在时会创建,当文件存在时,会报错 |
思考1: r+、w+和a+都可以实现对文件的读写,那么他们有什么区别呢?
- r+会覆盖当前文件指针所在位置的字符,如原来文件内容是"Hello,World",打开文件后写入"hi"则文件内容会变成"hillo, World"
- w+与r+的不同是,w+在打开文件时就会先将文件内容清空,不知道它有什么用
- a+与r+的不同是,a+只能写到文件末尾(无论当前文件指针在哪里)
思考2: 为什么要定义这些模式呢?为什么不能像我们用word打开一篇文档一样既可以读,又可以写,还可修改呢?
- 跟安全有关,有这种观点的大部分是做运维的朋友,他们认为这就像linux上的rwx(读、写、执行)权限。
- 跟操作系统内核管理I/O的机制有关,有这种观点的大部分是做C开发的,特别是与内核相关的开发人员。为了提高读写速度,要写入磁盘的数据会先放进内存缓冲区,之后再回写。由于可能会同时打开很多文件,当要回写数据时,需要遍历以打开的文件判断是否需要回写。他们认为如果打开文件时指定了读写模式,那么需要回写时,只要去查找以“可写模式”打开的文件就可以了。
3.2 文件的读取方法
方法 | 含义 |
---|---|
f.read | 从文件中读取整个文件的内容 |
f.readline | 从文件中读取一行内容 |
f.readlines | 从文件中读取所有行,以每行元素形成一个列表 |
f.seek | 改变当前文件操作指针的位置,括号内值为2,表示文章末尾。值为0,表示文章开头 |
方法 | 描述 |
---|---|
read() | 一次读取文件所有内容,返回一个str |
read(size) | 每次最多读取指定长度的内容,返回一个str;在Python2中size指定的是字节长度,在Python3中size指定的是字符长度 |
readlines() | 一次读取文件所有内容,按行返回一个list |
readline() | 每次从文件中只读取一行内容 |
此外,还要两个与文件指针位置相关的方法
方法 | 描述 |
---|---|
seek(n) | 将文件指针移动到指定字节的位置 |
tell() | 获取当前文件指针所在字节位置 |
下面来看下操作实例
3.2.1. 读取指定长度的内容
Python2
with open('song.txt', 'r') as f:
print(f.read(12).decode('utf-8'))
输出结果:
匆匆那年
结果说明:Python2中read(size)方法的size参数指定的要读取的字节数,而song.txt文件是UTF-8编码的内容,一个汉字占3个字节,因此12个字节刚好是4个汉字。
Python3
with open('song.txt', 'r', encoding='utf-8') as f:
print(f.read(12))
输出结果:
匆匆那年我们 究竟说
结果说明:Python3中read(size)方法的size参数指定的要读取的字符数,这与文件的字符编码无关,就是返回12个字符。
3.3.2. 读取文件中的一行内容
Python2
with open('song.txt', 'r', encoding='utf-8') as f:
print(f.readline())
Python3
with open('song.txt', 'r') as f:
print(f.readline().decode('utf-8'))
输出结果都一样:
匆匆那年我们 究竟说了几遍 再见之后再拖延
3.2.3. 遍历打印一个文件中的每一行
这里我们只以Python3来进行实例操作,Python2仅仅是需要在读取到内容后进行手动解码而已,上面已经有示例。
方式一:先一次性读取所有行到内存,然后再遍历打印
with open('song.txt', 'r', encoding='utf-8') as f:
for line in f.readlines():
print(line)
输出结果
-
匆匆那年我们 究竟说了几遍 再见之后再拖延
-
可惜谁有没有 爱过不是一场 七情上面的雄辩
-
匆匆那年我们 一时匆忙撂下 难以承受的诺言
-
只有等别人兑现
这种方式的缺点与read()方法是一样的,都是会消耗大量的内存空间。
方式二:通过迭代器一行一行的读取并打印
with open('song.txt', 'r', encoding='utf-8', newline='') as f:
for line in f:
print(line)
输出结果
-
匆匆那年我们 究竟说了几遍 再见之后再拖延
-
可惜谁有没有 爱过不是一场 七情上面的雄辩
-
匆匆那年我们 一时匆忙撂下 难以承受的诺言
-
只有等别人兑现
另外,发现上面的输出结果中行与行之间多了一个空行。这是因为文件每一行的默认都有换行符,而print()方法也会输出换行,因此就多了一个空行。去掉空行也比较简单:可以用line.rstrip()
去除字符串右边的换行符,也可以通过print(line, end='')避免print方法造成的换行。
file类的其他方法:
方法 | 描述 |
---|---|
flush() | 刷新缓冲区数据,将缓冲区中的数据立刻写入文件 |
next() | 返回文件下一行,这个方法也是file对象实例可以被当做迭代器使用的原因 |
truncate([size]) | 截取文件中指定字节数的内容,并覆盖保存到文件中,如果不指定size参数则文件将被清空; Python2无返回值,Python3返回新文件的内容字节数 |
write(str) | 将字符串写入文件,没有返回值 |
writelines(sequence) | 向文件写入一个字符串或一个字符串列表,如果字符串列表中的元素需要换行要自己加入换行符 |
fileno() | 返回一个整型的文件描述符,可以用于一些底层IO操作上(如,os模块的read方法) |
isatty() | 判断文件是否被连接到一个虚拟终端,是则返回True,否则返回False |
3.2.4. 举例
现以该文件来进行读取展示,
(1)
f = open('a.txt','rt')
print(f.read())
文本文件
二进制文件
文本文件与二进制文件
(2)
f = open('a.txt','rt')
print(f.readlines())
['文本文件\n', '二进制文件\n', '文本文件与二进制文件']
(3)
f = open('a.txt','rt')
print(f.readline())
文本文件
逐行读取内容并进行操作。
f = open('a.txt','rt')
f.seek(0)
for line in f:
print(line)
f.close()
文本文件
二进制文件
文本文件与二进制文件
文件打开后,对文件的读写有一个读取指针,当从文件中读入内容后,读取指针将向前进,再次读取的内容将从指针的新位置开始。
(1)
f = open('abc.txt','w')
f.write("文本文件\n")
f.write("二进制文件\n")
f.write("文本文件与二进制文件\n")
f.close()
程序运行结果如下:注意写入的每一行后面要加上换行符。否则所有文字都连接起来。
(2)
f = open('abc.txt','w')
ls = ['文本文件\n','二进制文件\n','文本文件与二进制文件\n']
f.writelines(ls)
f.close()
四、数据的组织方式
组织数据可以分为一维数据、二维数据和高维数据。
4.1. 一维数据
a = ['北京市','上海市','重庆市','天津市']
print(len(a))
4
一维数据的存储方式为用特殊字符来分隔各数据,例如空格、逗号、换行。
其中:使用逗号分隔的为csv格式
将列表对象输出为csv格式,采用字符串的join()格式最为方便。
一维数据的写入
ls = ['北京','上海','重庆','天津']
f = open('read.csv','w')
f.write(','.join(ls)+"\n")
f.close()
运行结果如下:
一维数据的读取
f = open('read.csv','r')
ls = f.read().strip('\n').split(',')
f.close()
print(ls)
['北京', '上海', '重庆', '天津']
4.2. 二维数据
二维数据可以看作许多条一维数据组成,用csv格式存储。整个csv表格代表一个二维数据。
二维数据的写入
ls = [ ['指标', '2014年', '2015年', '2016年'],
['居民消费价格指数', '102', '101.4', '102'],
['食品', '103.1', '102.3', '104.6'],
['烟酒及用品', '994', '102.1', '101.5'],
['衣着', '102.4', '102.7', '101.4'],
['家庭设备用品', '101.2', '101', '100.5'],
['医疗保健和个人用品', '101.3', '102', '101.1'],
['交通和通信', '99.9', '98.3', '98.7'],
['娱乐教育文化', '101.9', '101.4', '101.6'],
['居住', '102', '100.7', '101.6'], ]
f = open('a.csv','w')
for row in ls:
f.write(",".join(row)+'\n')
f.close()
运行结果如下:
二维文件的写入
ls = []
f = open('a.csv','r')
for line in f:
ls.append(line.strip('\n').split(','))
f.close()
print(ls)
[['指标', '2014年', '2015年', '2016年'],
['居民消费价格指数', '102', '101.4', '102'],
['食品', '103.1', '102.3', '104.6'],
['烟酒及用品', '994', '102.1', '101.5'],
['衣着', '102.4', '102.7', '101.4'],
['家庭设备用品', '101.2', '101', '100.5'],
['医疗保健和个人用品', '101.3', '102', '101.1'],
['交通和通信', '99.9', '98.3', '98.7'],
['娱乐教育文化', '101.9', '101.4', '101.6'],
['居住', '102', '100.7', '101.6']]
常见错误汇总:
错误1:
“UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0x80 in position 42: illegal multibyte se”