字符编码
1 字符编码分类
1.1 ASCII码
1. 只支持英文字符串和特殊字符
2. 采用8位二进制数(8个bit位)的后七个bit位表示一个英文字符串或特殊字符, 最多可以表示128个英文字符和特殊字符. 第一个bit位用作保留位.
1.2 GBK编码
1. 支持英文字符, 特殊字符, 中文字符
2. 统一用2个字节表示所有的中英文和特殊符号. 可以表示2**16个字符
2. 对于英文和字符, 采用后七个bit为表示, 前面的bit位都用0表示
3. 对于中文, 用16个比特位表示
1.3 Unicode
1. 现阶段, 计算机内存中, 统一使用Unicode编码, 所有的字符格式, 都可以和Unicode互转.
2. ASCII, GBK和utf-8等编码, 表示的是字符从内存往硬盘存和从硬盘读取到内存时是怎么转换的
比如, 使用ASCII, 那么需要在编辑器选择ASCII编码, 这样编辑文本时, 无论输入任何语言的字符, 都不会乱码, 因为输入是先输入到内存, 内存统一使用unicode
而打开文本时, 也要选择用ASCII编码打开, 这样才不会出现乱码.
3. 兼容万国字符, 与每个国家的文字都有对应关系
4. 采用2个bytes, 16个bits位来表示一个中文字符串, 个别生僻字符会采用4bytes或者8bytes
1.4 utf-8
1. 支持万国字符
2. 1个bytes表示英文, 3个bytes表示中文, 生僻字符会用4个bytes表示. 特殊字符也是占一个字节.
2 人类字符, 内存字符存储以及硬盘字符存储的对应关系
2.1 存数据阶段
2.2 取数据阶段
内存固定使用unicode,我们可以改变的是存入硬盘和读取文本时采用的格式.
目前, 无论是存和取, 都应该使用utf-8的格式
3 文本文件存取乱码问题
文本文件存取乱码问题
存乱了:解决方法是,文本编辑器的编码格式应该设置成支持文件内字符的格式
取乱了:解决方法是,文件是以什么编码格式存入硬盘的,就应该以什么编码格式读入内存,在文本编辑器中设置编码格式和存盘格式一致
4 解决Python解释器读文件时不乱码
Python解释器默认读文件的编码
Python3默认:utf-8
Python2默认:ASCII
指定文件头修改默认的编码:
在py文件的首行写:
# coding:gbk
通过文件头来告诉Python解释器用哪种编码来读取文件
而对于文件头, 解释器会用自己默认的编码读取. 因为无论ASCII还是utf-8都可以读取英文, 因此, 文件头解释器会靠默认的编码去读取
保证运行Python程序前两个阶段不乱码的核心法则:
指定文件头
# coding:文件当初存入硬盘时所采用的编码格式
文件是哪种编码格式存到硬盘的, Python解释器就该用哪种编码格式去读取
读取格式可以在文件中通过文件头来指定, 而存到硬盘的格式是取决于文本编辑器的设置
5 Python解释器识别语法时如何不乱码
识别语法时, 是在内存执行的, 因此, Python文件读取到内存后, 使用什么格式编码就会影响语法分析
内存中都是统一的unicode格式, 因此, 文件读取到内存后, 也要存成unicode模式
Python3的str类型在内存默认直接存成unicode格式,无论如何都不会乱码
保证Python2的str类型不乱码
x=u'上' # 强制Python2将字符串类型存成unicode, Python2的中文字符串要加上u'字符串', 而英文可以不加, 但最好加上
- 通过Python2执行, 如果中文字符串不加u, 会乱码
# coding:utf-8
x = '上'
print(x)
- 通过Python2执行, 给中文字符串加上u, 则不会乱码
# coding:utf-8
x = u'上'
print(x)
Python2解释器有两种字符串类型:str、unicode
# str类型
x='上' # 字符串的值会按照文件头指定的编码格式存入变量值的内存空间
# unicode类型
x=u'上' # 强制存成unicode
6 编码与解码
编码指的是从unicode --> utf-8
解码指的是从uft-8 ---> unicode
# coding: utf-8
x = "哈哈哈"
res1 = x.encode("gbk") # 将unicode格式, 编码成gbk
print(res1) # b'\xb9\xfe\xb9\xfe\xb9\xfe'
print(type(res1)) # 字符转换成字节类型, Python解释器并不会把bytes类型转成人类字符, 所以只会显示bytes类型
res2 = res1.decode("gbk") # 将gbk格式, 解码为unicode
print(res2) # 哈哈哈 # 解码成unicode, 解释器会把unicode自动转成人类字符, 所以print会显示人类字符
# 编码与解码通常在新老平台对接时使用, 老平台如果用的是Python2, 那么新平台在于其传数据时, 就要涉及编码和解码
7 总结:
Python3: 用pycharm, 无需任何操作, pycharm默认使用utf-8存, 而python3解释器, 默认用utf-8. 对于字符串, python3都直接存成unicode
Python2: 文件头, 用utf-8, 字符串, 用u'字符串'
解决2和3版本兼容性:
1. 所有的代码, 都指定文件头, # coding: utf-8
2. 所有的代码, 字符串都用u"字符串"
3. 所有的代码都存成utf-8格式
编码encode: 人类字符 --> Unicode(内存) --> uft-8,gbk,ascii(硬盘bytes类型)
解码decode: uft-8,gbk,ascii(硬盘bytes类型) --> Unicode(内存) --> 人类字符
内存数据如果需要存到本地或者远程硬盘, 需要经过encode编码, 转成utf-8, 然后存到硬盘, encode后, 得到bytes类型
文件操作部分
1、什么是文件
文件是操作系统提供给用户/应用程序操作硬盘的一种虚拟的概念/接口
用户/应用程序(open())
操作系统(文件)
计算机硬件(硬盘)
2、为何要用文件
用户/应用程序可以通过文件将数据永久保存到硬盘中
即操作文件就是操作硬盘
用户/应用程序直接操作的是文件,对文件进行的所有的操作,都是
在向操作系统发送系统调用,然后再由操作系统将其转换成具体的硬盘操作
3、open()功能的模式介绍
3.1 控制文件读写内容的模式:t和b
注意: t和b不能单独使用,必须跟r/w/a连用
- t文本(默认的模式, 如果指定了某种读写操作模式, 但没有指定读写内容模式, 默认就是t模式)
1. 读写都以str(unicode)为单位的. t模式自动把读入内存的字符根据指定的解码编码格式转成Unicode, 把写入磁盘的文本从Unicode转成指定的编码格式二进制
2. 文本文件, 只有文本文件才涉及字符编码, 因此, open()的数据来源必须是文件.
3. 因为Python中, 数据在内存是存成unicode格式, 必须指定encoding='utf-8', 把内存中的str(unicode), 编码成utf-8, 存到硬盘.
- b二进制/bytes
3.2 控制文件读写操作的模式
- r只读模式, 默认的操作模式
- w只写模式
- a只追加写模式
- +:r+、w+、a+
4 文件操作基本流程
4.1 打开文件
# windows路径分隔符问题, \a,\n等在Python中都是特殊的转义字符, 如果在文件路径中存在会被当做转义
open('C:\a.txt\nb\c\d.txt')
# 解决方案一:推荐, 在文件路径前加r(rawstring), 表示使用原生字符串, 不会转义
open(r'C:\a.txt\nb\c\d.txt')
# 解决方案二: 用'/'表示路径分隔符, open()功能会自动识别转换
open('C:/a.txt/nb/c/d.txt')
f=open(r'C:\Users\David\PycharmProjects\pythonProject\s14\day11\练习.py',mode='rt')
# f的值是一种变量,占用的是Python应用程序的内存空间
print(f)
>> <_io.TextIOWrapper name='C:\\Users\\David\\PycharmProjects\\pythonProject\\s14\\day11\\练习.py' mode='rt' encoding='cp1252'>
open()除了创建一个变量值占用Python解释器的内存空间,
还会向操作系统请求打开文件, 而操作系统会将打开的文件映射到硬盘空间.
所以, 通过应用程序对文件进行操作时, 先是由应用程序向操作系统发起读或写请求,
再由操作系统将请求转换为磁盘上的操作.
站在变量角度, f=open(), open()的返回值赋予给f, 会占用应用程序的内存
站在文件角度, f=open()会打开文件, 打开的文件是由操作系统维护, 因此占用操作系统的内存
x=int(10) # 并不涉及文件的概念, 只是纯内存的操作
代开文件路径两种方式:
绝对路径: open('C:\a.txt\nb\c\d.txt')
相对路径: open('d.txt'), 要求打开的文件, 和Python程序代码文件在同级的目录下
4.2 操作文件
读/写文件,应用程序对文件的读写请求都是在向操作系统发送系统调用,然后由操作系统控制硬盘把输入读入内存、或者写入硬盘
f=open(r'C:\Users\David\PycharmProjects\pythonProject\s14\day11\练习.py',mode='rt', encoding='utf-8')
# open()一个文件, 会把文件对象赋值给f,
res=f.read() # f.read()会通过文件对象, 把内容从硬盘读到内存
# read()是从文件指针当前位置开始往后读
print(type(res))
>>
print(res)
>>
f=open(r'C:\Users\David\PycharmProjects\pythonProject\s14\day11\练习.py',mode='rt', encoding='utf-8')
res=f.read() # f.read()就是把变量f赋的文件内容从硬盘读出来到内存, 赋值给res
print(res)
4.3 关闭文件
f=open(r'C:\Users\David\PycharmProjects\pythonProject\s14\day11\练习.py',mode='rt', encoding='utf-8')
res=f.read()
f.close() # 回收操作系统资源, 因为操作系统能同时打开的文件数是有限的. 如果不手动关闭文件, 操作系统会把长时间没有用的文件关闭, 但是在没关闭之前, 文件虽然没人在用, 但是文件依然处于打开状态.
print(f) # 回收了操作系统资源后, f变量还是存在的
>> <_io.TextIOWrapper name='C:\\Users\\David\\PycharmProjects\\pythonProject\\s14\\day11\\练习.py' mode='rt' encoding='utf-8'>
f.read() # 变量f存在,但是不能再读了, 这时f剩的只是文件对象, 而真正的文件已经被f.close()了
res = f.read()
print(res)
>> res = f.read()
ValueError: I/O operation on closed file.
del f # 回收应用程序资源, 变量一般不需要手动删除, 因为Python的垃圾回收机制会做这件事
# 一定是先回收操作系统的资源, 再回收内存资源, 如果先把内存资源的变量名f回收了, 那么f.close()就找不到f了
4.4 with上下文管理
- 准备两个文件, a.txt, b.txt. a.txt存aaaa, b.txt存bbbb
# 打开文本文件a.txt, 使用读模式, 并且赋值给f1.
# 注意: with 不能直接把open()的结果通过"="赋值给变量, 而是用as f1
# with会自动执行f1.close(), 在with子代码块运行完后, 关闭文件
with open('a.txt', mode = 'rt') as f1:
res = f1.read()
print(res)
>> aaaa
# with可以同时打开多个文件
with open('a.txt', mode = 'rt') as f1, open('b.txt', mode = 'rt') as f2:
res1 = f1.read()
res2 = f2.read()
print(res1) >> aaaa
print(res2) >> bbbb
with open(r'a.txt', mode='rt') as f1, \
open(r"b.txt", mode='rt') as f2:
res1 = f1.read()
res2 = f2.read()
print(res1)
print(res2)
- open()一个文件, 得到的返回值, 是文件对象, 也可以叫文件句柄, 可以理解为对真正的磁盘上的文件的一种引用, 用来控制文件
4.5 指定字符编码
- 创建一个新文件, c.txt, 存汉字, 哈哈哈哈
- 创建文件后, c.txt在硬盘上是utf-8格式的二进制, 因为Pycham默认用utf-8
with open('c.txt', mode = 'rt') as f1:
res1 = f1.read() # f1.read()本质是从磁盘中读取原本存的格式的二进制, 这里就是utf-8格式的二进制. 之后, open()中的t模式, 会强制把读入到内存的内容, 解码为Unicode, 因此, 需要指定解码的编码格式
print(res1)
>> 哈哈哈哈
res1 = f1.read()
# 执行f1.read(), 会把目标文件, 从硬盘读入到内存
# 文件c.txt在硬盘是utf-8格式的, 读入到内存后也是utf-8格式
# 但是, 因为读取的是文本文件,是t模式, 所以在内存中要求读写的字符串都是unicode格式, 因此t模式会将read()读入的utf-8解码成unicode
# 解码要按照文件存成的字符编码去解码
# 但是, 如果命令中没有告诉open()用哪种字符编码格式进行解码操作, 那么open()会用操作系统默认的字符编码,去解码
# 没有指定encoding参数, 操作系统会使用自己默认的字符编码, 去解码
# Linux, Mac, utf-8
# Windows, 默认要看语言设置, 在dos命令行通过chcp命令查看活动代码页, 然后和活动代码页对照
# 因此, 如果操作系统默认不是utf-8, 那么解码的时候就是用另一个字符编码去解码utf-8, 就会出现乱码
with open('c.txt', mode = 'rt', encoding='utf-8') as f1: # 告诉open()用utf-8去解码
res1 = f1.read()
print(res1, type(res1))
>> 哈哈哈哈
# encoding='utf-8'就是规定, 无论编码还是解码, 都用utf-8的字符编码去执行. 文本读入到内存,用utf-8去解码, 写入到磁盘用utf-8去编码
5 文本模式(t模式)下的文件读写操作模式
5.1 r模式
- 默认的操作模式
- 只读模式
# 当open()要读的文件不存在时, r模式会报错, 找不到文件或目录
# 当open()要读的文件存在时, r模式打开文件时, 文件指针跳到最开始位置
with open('c.txt', mode = 'rt', encoding='utf-8') as f1:
res = f1.read()
# f1.read()在没有指定任何参数的情况下, 会把文件内容, 从当前指针的位置一下读到结尾
# r模式打开文件时, 指针默认就是在文件最开始, 所以执行f1.read()的结果就是, 一次性把文件全部的内容, 都读入到硬盘. 因此读大文件时, 会造成内存被文件大量占用
print(res)
>> 哈哈哈哈
- r模式小问题
with open('c.txt', mode = 'rt', encoding='utf-8') as f1:
print("第一次读".center(50,'*'))
res = f1.read()
print(res)
print("第二次读".center(50,'*'))
res = f1.read()
print(res)
>>
***********************第一次读***********************
哈哈哈哈
***********************第二次读***********************
# 第二次读c.txt文件时, 并没有结果返回, 这是因此,r模式会一次性从文件开始读到结尾, 读完, 文件指针就停在了文件结尾
# 而由于第一次读完, 文件没有关闭就开始读第二次, 那么第二次读时就是从文件末尾开始读, 那肯定是没有内容的, 所以没有返回值
with open('c.txt', mode = 'rt', encoding='utf-8') as f1:
print("第一次读".center(50,'*'))
res = f1.read()
print(res)
with open('c.txt', mode='rt', encoding='utf-8') as f1:
print("第二次读".center(50,'*'))
res = f1.read()
print(res)
>>
***********************第一次读***********************
哈哈哈哈
***********************第二次读***********************
哈哈哈哈
# 这时, 第一次读完c.txt, 返回哈哈哈哈后, 再次打开c.txt, 由于文件是存在的, 那么r模式下, 文件指针会移动到文件最开始, 从最开始一直读到结尾, 最后返回哈哈哈哈
补充: 如果文件中有特殊字符, 比如换行, 那么r模式也会读取并返回
- r模式案例: 利用with open()读取文本中的用户名和密码, 和用户输入的用户名和密码作比较, 如果相等, 返回登录成功, 不相等, 返回登录失败
- 先创建一个文本文件, 保存用户名和密码
# 注意, 文件不要有多余的空行和空格,如果有, 那么r模式读文件时, 会把空行和空格都读出来, 然后用split()去做分隔, 会造成数据错误
user.txt
admin:password
#版本1: 仅实现单用户的验证
_username = input("username: ").strip() # 将用户输入用strip()处理, 去除左右的空格
_password = input("password: ").strip()
with open("user.txt", mode='rt',encoding='utf-8') as f:
info = f.read()
username, password = info.split(':') # 将info中的数据, 用split()以":"做分隔, 分隔后的结果是个列表, 然后将列表解压, 把值赋给对应的变量
if _username == username and _password == password:
print("登录成功")
else:
print("登录失败")
#版本2: 实现多用户验证
# 先在user.txt中添加多个用户名和密码
admin:password
admin1:password1
admin2:password2
admin3:password3
********代码*********
_username = input("username: ").strip()
_password = input("password: ").strip()
with open("user.txt", mode='rt', encoding='utf-8') as f:
for line in f: # 这里不能用info = f.read(), 然后用for循环去遍历info, 因为info里存的是字符串, for循环遍历字符串会打印每一个字符
# 打开文件后, 得到一个f的文件对象, 之后就是利用相应的功能去操作这个f对象, f.read()就是读, 而用for循环就是遍历这个文件对象, 以每行为一次for循环单位
# print(line,end='')
# 这里因为本身文本每一个结尾都是换行符, 而且print默认会在结尾加换行符, 所以, 需要用end='', 把print结尾的换行符转行成空, 只留文本中每一行末尾的换行符
# username, password = line.split(':')
# 这里也不能直接使用split()去做分隔, 因为, 上面print(line,end='')的结果的每一行后面都是有换行符\n的
# 如果这里直接用split(), 那么得到的结果就是 admin, password\n, 这样去和用户输入作比较就会出问题
# info = line.strip('\n').split(':')
# 使用strip去除每行最后的换行符, strip去掉的是字符左右的空白字符, 包括空格, 换行符, 等. 这样info中存的就是一个个列表, 由用户名和密码组成
username, password = line.strip('\n').split(':') # 这样就从文件中, 得到了所有的用户名和密码对应关系
if _username == username and _password == password:
print("登录成功")
break
else: # 这里的else一定是作用在for循环, 而不是if条件判断, 因为需要把整个文件都遍历完, 发现没有符合的账号密码, 才返回登录失败
print("登录失败")
# 不过这时, 如果登录的账号正好在文件的最后一行, 那么需要把整个文件每一个都遍历一遍, 如果账号密码信息众多, 会有效率问题, 之后可以考虑, 把用户名和密码转成字典格式, 如果输入的用户名在整个文本里存在, 那么就根据用户名作为key去取密码, 然后判断密码是否相等, 如果用户名本身就不存在, 那么直接返回登录失败
_username = input("请输入用户名: ").strip()
_password = input("请输入密码: ").strip()
d1 = dict()
with open(r'user.txt', mode='rt', encoding='utf-8') as f:
for line in f:
res = line.strip('\n').split(':')
# print(res)
d1[res[0]] = res[1]
# print(d1)
for i in range(len(d1)):
if _username in d1:
if _password == d1[_username]:
print("登录成功")
break
else:
print("用户名或密码错误")
5.2 w模式
- 只写模式
- t模式下, 只能写字符串内容, b模式下, 写bytes类型
# 当文件不存在时, 会创建空文件
with open('d.txt', mode = 'wt', encoding= 'utf-8') as f:
pass
# 当文件存在时, w模式会清空文件
d.txt
哈哈哈哈
啦啦啦啦
with open('d.txt', mode = 'wt', encoding= 'utf-8') as f:
pass
>> 清空文件, 然后指针位于文件开始位置
- 只写, 不能读, 不能使用read()
with open('d.txt', mode = 'wt', encoding= 'utf-8') as f:
f.read()
>> io.UnsupportedOperation: not readable
- 写数据, 用write()
with open('d.txt', mode = 'wt', encoding= 'utf-8') as f:
f.write("hahaha")
# 会把write()中的数据写入到文件, 并且指针处于行尾, 而且默认是不换行的
# 如果想要换行, 在写入的内容后加换行符即可'\n'
with open('d.txt', mode = 'wt', encoding= 'utf-8') as f:
f.write("hahaha\n")
# 需要注意的是, 此时, 再次写入数据, 因为文件是存在的, 所以会清空文件, 再把新的内容写进去, 所以, w模式不支持追加, 而是每次新输入都把源文件的数据清空,文件指针放到首位, 把新的内容写进去
补充1: w模式, 在没有关闭文件的情况下, 连续写入数据, 新数据会接着上一次文件指针的位置, 接着写入, 并不会清空源文件
with open('d.txt', mode = 'wt', encoding= 'utf-8') as f:
f.write("ha\n")
f.write("haha\n")
f.write("haha\ha\n")
# 这时, 文件会依次写入数据
ha
haha
haha\ha
补充2: w模式, 每次关闭文件后, 连续写入, 那么每次都会把文件先清空, 文件指针回到文件开头, 然后写入数据, 因此, 对于重要的文件, 千万不要用w模式打开写入
5.3 a模式
-只追加写, 不能读
# 在文件不存在时, 会创建空文件, 文件指针处于文件开始位置
with open('e.txt', mode = 'at', encoding= 'utf-8') as f:
pass
# 在文件存在时, 打开文件后, 会把文件指针移动到文件末尾, 然后写入新的数据
with open('e.txt', mode = 'at', encoding= 'utf-8') as f:
f.write("哈哈哈哈")
# 如果文件指针始终处于一行的结尾, 并且新写入的数据没有换行符, 那么数据就一直在同一行追加
# 在文件打开不关时, 连续追加数据时, 和w模式下, 不关文件连续追加数据一样, 都是在文件指针末尾追加数据
a模式, 每次打开文件时, 不会清空文件, 因为它是把文件指针移动到文件结尾, 然后写入数据
w模式, 每次打开文件时, 都会清空文件, 因为它是每次都把文件的指针移动到文件开始的位置, 所以会清空原有内容
补充: w和a模式的使用场景
w模式因为每次打开都会清空文件, 因此, 适用于创建新文件的场景
a模式因为每次打开不会清空文件, 而是追加写文件, 适用于日志文件, 等需要记录数据并且追加数据的场景
- a模式案例: 账号注册功能
_username = input("请输入用户名: ")
_password = input("请输入密码: ")
with open("user_register.txt", mode = 'at', encoding = "utf-8") as f:
f.write("{username}:{password}\n".format(username=_username, password=_password))
# 这样每次运行, 都会把用户输入的用户名和密码, 以固定的格式存到"user_register.txt"里, 实现账号信息持久保存
admin1:password1
admin2:password2
admin3:password3
- w模式案例: 实现文本文件复制
# 思路1:
with open("user_register.txt", mode = 'rt', encoding = "utf-8") as f1:
info = f1.read() # 先把整个文件都读到内存, 赋值给info变量
# print(info)
with open("user_register_backup.txt", mode = 'wt', encoding= "utf-8") as f2:
f2.write("{info}".format(info=info)) # 之后把info变量保存的内容全部写到新文件
# 思路2, 把读文件和写文件写到一条命令
with open("user_register.txt", mode = 'rt', encoding = "utf-8") as f1, \
open("user_register_backup.txt", mode = 'wt', encoding = "utf-8") as f2:
res = f1.read()
f2.write(res)
# 实现复制脚本
_src_file = input("请输入源文件的绝对路径: ").strip()
_dst_file = input("请输入目标文件的绝对路径: ").strip()
with open(r"{src_file}".format(src_file=_src_file), mode = 'rt', encoding = "utf-8") as f1, \
open(r"{dst_file}".format(dst_file=_dst_file), mode = 'wt', encoding = "utf-8") as f2:
res = f1.read()
f2.write(res)
# 利用for循环把每行数据读出来, 再写入到新文件
_src_file = input("请输入源文件的绝对路径: ").strip()
_dst_file = input("请输入目标文件的绝对路径: ").strip()
with open(r"{src_file}".format(src_file=_src_file), mode = 'rt', encoding = "utf-8") as f1, \
open(r"{dst_file}".format(dst_file=_dst_file), mode = 'wt', encoding = "utf-8") as f2:
for line in f1: # 使用for循环, 将f1的每一行读出来, 写到f2里, 写进去一行, 引用计数就为0, 会被解释器清空, 内存中永远只有一行数据, 对内存压力小
f2.write(line)
5.4 +模式
"+" 不能单独使用, 必须配合r, w, 或a模式一起使用
r+t:
r+模式处理文件, 特性与限制取决于r模式
r+模式处理文件时, 文件不存在, 会直接报错, 因为是以r为基础, r模式下, 打开不存在的文件就是直接报错
with open('g.txt', mode = 'r+t', encoding= 'utf-8') as f:
...
>> FileNotFoundError: [Errno 2] No such file or directory: 'g.txt'
注意: r+模式下, 如果源文件非空, 那么使用write()写数据时, 会从文件开始位置依次覆盖, 因为r模式打开文件时, 会把文件指针移动到文件最开始
- w+t
w+模式处理文件时, 如果文件不存在, 则按照w的特性, 会创建文件
with open('g.txt', mode = 'w+t', encoding= 'utf-8') as f:
...
>> 创建g.txt文件
w+模式处理文件时, 如果文件存在, 则按照w的特性, 会清空文件
with open('g.txt', mode = 'w+t', encoding= 'utf-8') as f:
...
# 先向g.txt写入一些数据, 然后执行with open(), 由于g.txt文件已经存在, 那么按照w的特性, 会把g.txt文件清空
注意: w+虽然可以读文件了, 但是只要打开文件, 内容就会被清空.
- a+t
a+模式处理文件是, 如果文件不存在, 则创建文件, 如果文件存在, 则把文件指针移动到文件最末端
w+和a+的特性:
- 在文件不关闭的情况下, 使用w+或者a+连续写入数据后, 如果接着想要在不关闭文件的情况下用read()读数据是读不出来的, 因为w+和a+写完数据后, 文件指针会停在文件末尾, 而read()是从文件指针的当前位置开始读, 因此永远是读不出来内容的
with open('g.txt', mode = 'w+t', encoding= 'utf-8') as f:
f.write("哈哈哈\n")
f.write("哈哈哈\n")
f.write("哈哈哈\n")
f.write("哈哈哈\n")
f.write("哈哈哈\n")
print(f.read())
>> 空, 但是g.txt是有内容的, a+模式效果一样, 写入数据后, 文件指针都是停在文件末尾, read()又是从文件指针当前位置开始读, 所以读不出来数据
- 对于已存在的文件:
a模式打开文件, 文件指针是在文件末尾的, 读不出来内容, w模式打开文件会清空, 所以也读不出来内容
总结
1、什么是文件
用户/应用程序(f=open(),获得文件对象/文件句柄)
操作系统(文件)
计算机硬件(硬盘)
2、为何要用文件
用于应用程序操作硬盘,永久保存数据,或者从硬盘读数据
3、如何用
f=open(r'C:\new_dir\a.txt',mode='r+t',encoding='utf-8')
# f.read()就是把硬盘的二进制数据, 读到内存, 可以赋值给变量, 拿到整个文件的内容, 并不涉及字符编码转换
# t会告诉open()功能将二进制转换成字符文本, 但前提是这些二进制数据, 原本就是由字符文本转换成的二进制
# 然后open()并不知道这些二进制是什么格式的二进制, 因此open()功能会用默认的字符编码也就是系统的字符编码去打开文件,把这些文件当成是系统的字符编码去打开, 因此在open()功能要指定encoding, 告诉open()功能用什么字符编码去解码打开文件
# f.read() 读出来的就是unicode, python打印出来时会转成utf-8
res=f.read() # 读出硬盘的二进制(utf-8)->t控制将二进制转换成unicode->字符
print(res)
# f.write()
f.close()
print(f) # f.close()把文件关闭后, f还是存在的, 因为f是python的一个变量, 但是f.read()就不存在了, 因为文件已经关闭了
f.read() # 抛出异常
with open(...) as f,open(...) as f1:
code1
code2
code3
4. open()打开文件是没有效率问题的, 因为只是产生一个文件句柄, 发起系统调用去打开文件, 并没有读和写操作, 只有读和写操作是涉及I/O. 真正的效率问题是出现在读和写阶段, 也就是f.read()和f.write()
5. 复制文件脚本
_src_file = input("请输入源文件的绝对路径: ").strip()
_dst_file = input("请输入目标文件的绝对路径: ").strip()
with open(r"{src_file}".format(src_file=_src_file), mode = 'rt', encoding = "utf-8") as f1, \
open(r"{dst_file}".format(dst_file=_dst_file), mode = 'wt', encoding = "utf-8") as f2:
#res = f1.read() # 这种方法, 会瞬间将文件内容全部读出来到内存, 赋值给res, 如果文件量大, 会占用大量内存空间
#f2.write(res)
for line in f1: # 使用for循环, 将f1的每一行读出来, 写到f2里, 写进去一行, 引用计数就为0, 会被解释器清空, 内存中永远只有一行数据, 对内存压力小, 但是要写的数据量是不变的
f2.write(line)
5.5 x模式
x模式(控制文件操作的模式): 只写模式, 不可读, 执行open()时, 如果文件存在则报错, 如果文件不存在则创建
with open(r'aa.txt', mode = 'xt', encoding ='utf-8') as f1:
pass
文件存在时, 则报错
FileExistsError: [Errno 17] File exists: 'aa.txt'
x模式的问题是, 只能在文件不存在的时候, 进行写操作, 一旦文件创建出来了, 打开就会报错, 也就无法后续修改
6 二进制模式(b模式)下的文件读写操作模式
b: binary模式
读写都是以bytes为单位, 不加解码格式, 读出来的都是b'内容'格式的bytes类型, 写入也是
可以针对所有文件
b模式下, 一定不能指定encoding参数, b模式和encoding没关系, 不做解码操作.
b模式更通用
b模式下, 执行f.read(), 会把硬盘内容读入内存, 但是不会做任何转换, 只是二进制. 而t模式转换成unicode, 然后有解释器转换成指定的解码类型
Python解释器会将这些二进制转换成bytes类型
# aaaa.txt
哈哈哈哈aaaa
with open(r'aaaa.txt', mode = 'rb') as f1:
res = f1.read() # b模式下, 会把文件直接从硬盘加载到内存, 不做任何转换, 文本如果是以utf-8存到硬盘, 那么b模式读到内存, 就是utf-8格式的二进制, 那么赋值给res打印后也是utf-8格式的二进制, Python解释器再把它们以16进制显示出来, 如果是英文字母就直接显示英文, 如果是中文则以16进制显示
print(res)
>> b'\xe5\x93\x88\xe5\x93\x88\xe5\x93\x88\xe5\x93\x88aaaa\r\n'
如果需要读取文本字符, 那么需要解码, 由utf-8解码成可读字符
print(res.decode('utf-8'))
>> 哈哈哈哈aaaa
b模式下, 写入字符数据, 要先把字符数据转成bytes类型, 才能用f.write()去写入
with open(r'f.txt', mode='wb') as f:
info = "哈哈哈aaa".encode('utf-8') # 用encode, 将输入的字符, 转成utf-8格式的二进制. 之后b模式读取的时候, 也要用utf-8.
f.write(info)
f.txt
哈哈哈aaa
6.1 b模式和t模式的对比
b模式更通用, 可以处理所有类型的文件, 包括文本文件, 视频, 图片等
t模式只能处理文件文件
在处理文本文件时, t模式更方式, 因为t模式会自动解码, 将读到内存中的二进制解码成可读字符
而如果使用b模式去处理文本文件, 还需要手动执行编码和解码操作
6.2 b模式案例
- b模式实现文件拷贝工具
src_file=input('源文件路径>>: ').strip()
dst_file=input('源文件路径>>: ').strip()
with open(r'{}'.format(src_file),mode='rb') as f1,\
open(r'{}'.format(dst_file),mode='wb') as f2:
# res=f1.read() # 内存占用过大
# f2.write(res)
for line in f1: # 复制文件, 并不涉及显示文件内容, 只是把从源文件读到内存的二进制, 原封不动存到新的文件, 因此, 不涉及编码和解码
f2.write(line)
- 循环读取文件两种方式
- t模式for循环读取文本文件
对于文本文件来说, 区分不同的行的依据就是换行符/n
使用for循环对f文件句柄进行遍历时, 就是按照换行符取每一行数据
d.txt
哈哈哈哈
h=aaaa
with open(r'd.txt', mode='rt', encoding='utf-8') as f:
for line in f:
print(line,end='') # print会默认带换行符,因此要指定空字符结尾, 避免出现空行. 而文本文件每一行结尾的换行符如果要去掉, 需要用strip. 但是f只是文件句柄, 所以要先把line付给一个变量, 然后对变量进行strip操作. 这样就会把文本文件每行的换行符去掉
>>
哈哈哈哈
h=aaaa
- b模式循环读取任意格式的文件
b模式for循环读取任意格式的文件, 也会以换行符区分不同的行
方式一:自己控制每次读取的数据的数据量
with open(r'test.jpg',mode='rb') as f:
while True:
res=f.read(1024) # 1024字节, b模式读写都是字节为单位. 每次都是从当前位置往后读1024字节
if len(res) == 0:
break # 当长度为0时, 终止循环
print(len(res))
方式二:以行为单位读,当一行内容过长时会导致一次性读入内存的数据量过大, 此时应该使用while循环, 指定每次读的字节长度
with open(r'g.txt',mode='rt',encoding='utf-8') as f:
for line in f:
print(len(line),line)
with open(r'g.txt',mode='rb') as f:
for line in f:
print(line)
with open(r'test.jpg',mode='rb') as f:
for line in f:
print(line)
7 文件操作的其他方法
- 读相关操作
7.1 readline()
一次只读一行, 以换行符为分隔符
with open(r'a.txt', mode = 'rt', encoding = 'utf-8') as f1:
res1 = f1.readline()
res2 = f1.readline()
print(res1, end='')
print(res2, end='')
>>
嘿嘿嘿ccc
哦哦哦bbb
利用while循环
while True:
res = f1.readline()
if len(res) == 0:
break
print(res,end='')
>>
嘿嘿嘿ccc
哦哦哦bbb
哈哈哈aaa
7.2 readlines()
读多行, 并且以列表的形式返回
with open(r'a.txt', mode = 'rt', encoding = 'utf-8') as f1:
res = f1.readlines()
print(res)
>> ['嘿嘿嘿ccc\n', '哦哦哦bbb\n', '哈哈哈aaa']
f.read()与f.readlines()都是将内容一次性读入内存,如果内容过大会导致内存溢出,若想将内容全读入内存,要用循环
- 写相关操作
7.3 writelines()
write()本身就可以写入多行, 只要用\n换行符即可
with open(r'a.txt', mode = 'wt', encoding = 'utf-8') as f1:
f1.write('111\n222\n333')
>>
111
222
333
writelines()作用是: 将列表中的元素, 写入到文件里
with open(r'a.txt', mode = 'wt', encoding = 'utf-8') as f1:
l = ['111\n','222','333']
for line in l: # for循环会遍历列表中的每个元素, 然后再写入到文件里, 但是如果想把每个元素写到单独一行, 那么列表的元素需要有个'\n', 否则都会写到以一行
f1.write(line)
>>
111
222333
writelines()可以实现上面相同的功能
with open(r'a.txt', mode = 'wt', encoding = 'utf-8') as f1:
l = ['111\n','222','333']
f1.writelines(l)
>>
111
222333
- 补充: t模式下, 只能写入字符串数据, b模式写的是bytes类型
with open(r'h.txt', mode = 'wt', encoding='utf-8') as f1:
f1.write(str(444))
>>
444
用b模式写入
with open(r'a.txt', mode = 'wb') as f1:
l = ['111\n'.encode('utf-8'),'222'.encode('utf-8'),'333'.encode('utf-8')] # 需要对字符进行encode编码, encode是字符编码, 只对字符串有效, 对其他类型无效
f1.writelines(l)
>>
111
222333
纯英文字符或数字, 直接在字符前加b即可, 不用.encode()
因为所有的字符编码都是兼容英文和数字的, .encode()得到的结果就是b''
with open(r'a.txt', mode = 'wb') as f1:
l = [b'aaa',b'bbb',b'ccc',b'444']
f1.writelines(l)
>>
aaabbbccc444
'上'.encode('utf-8') 等同于bytes('上',encoding='utf-8')
7.4 flush()
将产生的数据立即写入磁盘
with open(r'h.txt', mode = 'wb') as f1:
f1.write(b'444') # f1.write()是将内存的数据提交给操作系统, 而操作系统多久写磁盘要看策略.
f1.flush() # 执行f1.flush()就是告诉操作系统, 立即写入磁盘
7.5 其他方法
readable() 判断文件是否可读
writeable() 判断文件是否可写
closed 判断文件是否已经被关闭
with open(r'h.txt', mode='wb') as f1:
print(f1.readable()) # False w模式下不可读
print(f1.writable()) # True w模式可写
print(f1.closed) # False with没执行完, 文件处于打开状态
print(f1.closed) # True with执行完, 文件自动关闭
8 文件指针移动
指针移动的单位都是以bytes/字节为单位
只有一种情况特殊:
t模式下的read(n),n代表的是字符个数
a.txt
aaa您好
with open(r'a.txt', mode = 'rt', encoding='utf-8') as f1:
res = f1.read(4) # read(n)以字符为单位读取
print(res)
>>
aaa您
seek() 用来控制文件指针的移动
f.seek(n,模式):n指的是移动的字节个数, 如果移动的字节个数, 会把字符分开, 那么就无法执行, 解释器会报错. 比如文件只存了一个汉字, 但是代码中写了移动2个字节, 这时就会报错
模式0:参照物是文件开头位置
f.seek(9,0) # 0模式参照文件开头, 9表示移动9个字节, 此时指针处于第九个字节的位置
f.seek(3,0) # 虽然此前执行了f.seek(9,0), 但是0模式是以文件开头为参照物, 所以再执行(3,0)还是从文件开头开始, 此时指针处于字节3
模式1:参照物是当前指针所在位置
f.seek(9,1) # 9 如果以r模式打开文件, 一开始指针处于文件开头, 移动9个字节, 此时处于9
f.seek(3,1) # 12 再执行(3,1), 以当前的9为参照物再移动3个字节, 因此, 此时处于12字节
模式2:参照物是文件末尾位置,应该倒着移动
假设文件一共12个字节
f.seek(-9,2) # 3
f.seek(-3,2) # 9
强调:只有0模式可以在t下使用,1、2必须在b模式下用
f.tell() 获取文件指针当前位置
8.1 案例
准备文件
with open(r'a.txt', mode = 'rb') as f1:
f1.seek(9,0)
print(f1.tell()) >> 9 # 0模式以文件开头为参照, 移动9个字节, 所以当前处于字节9
f1.seek(3,0)
print(f1.tell()) >> 3 # 0模式下, 指针再次回到文件开头, 移动3个字节, 当前处于字节3
with open(r'a.txt', mode='rb') as f1:
f1.seek(9,0)
res = f1.read() # 移动9个字节, 然后把后续文件都读出来
print(f1.tell()) # 读完全部文件, 指针位于文件末尾, 该文件共31个字节, 其中每行9个字节字符, 另外要算上第一行和第二行结尾的/r/n, 每个特殊符号占一个字节, 共4个字节, 加起来31
print(res)
>>
31
b'\r\nbbb\xe5\x93\x88\xe5\x93\x88\r\nccc\xe5\x93\xa6\xe5\x93\xa6' # 读到第九个字节, 指针位于您好后面, 之后就是第一行的/r/n.
当文件指针移动到某一个位置后, 执行解码操作, 那么解出来的是指针后面的字节
with open(r'a.txt', mode = 'rb') as f1:
f1.seek(3,0)
res = f1.read()
print(res.decode('utf-8'))
>>
您好
bbb哈哈
ccc哦哦
with open(r'a.txt', mode = 'rb') as f1:
f1.seek(-9,2)
print(f1.tell()) >> 22
f1.seek(-3,2)
print(f1.tell()) >> 28
补充:
得到bytes类型的三种方式:
bytes类型可以理解为二进制
1、字符串编码之后的结果为bytes类型
'上'.encode('utf-8')
bytes('上',encoding='utf-8')
2、b'必须是纯英文或数字字符'得到的结果为bytes类型
3、b模式下打开文件,f.read()读出的内容也是bytes类型
8.2 seek()应用, 实现tail -f功能
- 准备数据写入程序
input.py
with open(r'a.txt', mode = 'at', encoding ='utf-8') as f: # 输入一定要a模式, 追加写
f.write('哈哈哈\n')
- 准备tail -f程序
output.py
import time
with open(r'a.txt', mode = 'rb') as f: # 使用b模式, 更通用
# r模式, 每次打开文件, 指针都是位于文件开始, 而tail程序需要关注的是新的内容, 所以要实现打开文件后, 把指针移动到文件末尾
f.seek(0,2) # 2模式每次打开文件后, 将指针移动到文件末尾, 并且正向读0个字节, 这样只要文件没有输入数据, 那么每次循环后文件指针还是原地不动
while True:
output = f.readline() # 每次读完一行, 指针还是在文件末尾
if len(output) == 0:
time.sleep(0.5)
else:
print(output.decode('utf-8'),end='')
- 准备文件
a.txt
- 执行输出output.py程序
C:\python38\python.exe C:/Users/David/PycharmProjects/pythonProject/s14/day11/output.py
执行output.py后, 指针会停留在文件末尾, 不断的去循环读取a.txt文件, 一旦有内容进来, 就会输出
- 执行input.py, 查看output.py结果
C:\python38\python.exe C:/Users/David/PycharmProjects/pythonProject/s14/day11/output.py
哈哈哈
8.3 文件修改的两种方式
硬盘的数据都是存在扇区上的, 每次写数据, 都是将原有的数据覆盖.
当想在文件中修改或插入数据时, 会把旧的数据覆盖, 而不会把后续的内容向后移, 因为一旦移了一个字节, 那么后续所有的硬盘上的内容, 都要后移.
平时我们通过文本编辑器修改文件时, 实际上修改的是内存的数据, 因此打开文件后, 文本编辑器会通过系统调用把文件内容全部读入内存, 之后的修改都是修改内存的内容
修改后, 点击保存, 那么文件在磁盘上的内容, 会被内存中存储的新内容整体覆盖
注意: 修改文件内容, 一定是先把文件从硬盘读到内存, 在内存做修改, 然后写回硬盘
8.3.1 类文本编辑器式修改
准备文件
a.txt
哈哈哈
嘿嘿嘿
哦哦哦
with open(r'a.txt', mode = 'rt', encoding='utf-8') as f:
res = f.read() # 将文件读到内存, 赋值给res
data = res.replace('哈哈哈','hahaha') # 将res中的'哈哈哈'替换成'hahaha'再赋值给data
with open(r'a.txt', mode = 'wt', encoding='utf-8') as f1: # 利用w模式打开文件, 将文件清空, 重新把data的数据写入文件, 就完成了字符串的替换
f1.write(data)
# 此时, 一定要在读文件的with结束, 再以w模式打开文件, 如果r模式打开文件后, 紧接着w模式打开文件, 那么文件内容会被立即清空
利用这种方法, 每次做文件修改, 都是先w模式打开把文件清空, 再把要写的内容全部写入到文件
8.3.2 生成一个新文件, 将内容写到新文件, 然后删除旧文件
准备文件
a.txt
哈哈哈
嘿嘿嘿
哦哦哦
import os
with open(r'a.txt', mode = 'rt', encoding = 'utf-8') as f1, \ # 打开源文件
open(r'.a.txt.swap', mode = 'wt', encoding = 'utf-8') as f2: # 以w模式打开一个新的.swap临时文件
for line in f1: # 从源文件中读一行
f2.write(line.replace('哈哈哈','aaa')) # 将源文件中需要替换的字符做替换, 然后写入临时文件, 由于新的文件是以w模式开启, 并且一直没有关闭, 所以不会清空. 循环结束, 每一行的字符都会被替换
# 字符串.replace()当要替换的内容不存在时不会报错
os.remove('a.txt') # 修改完, 需要删除源文件
os.rename('.a.txt.swap','a.txt') # 然后将临时文件改名为源文件
aaa
嘿嘿嘿
哦哦哦
8.3.3 两种方式对比
第一种方式: 耗费内存空间, 因为read()是把数据一次性读入内容, 但是节省了硬盘空间, 因为硬盘上的数据只有一份
第二种方式: 耗费硬盘空间, 因为在文件修改期间, 需要生成临时文件, 但是内存空间是不浪费的, 每次都是读一行加载到内存, 做修改, 然后写入到新文件