一、字符编码
所有软件都是运行硬件之上的,与运行软件相关的三大核心硬件为 cpu、内存、硬盘。
- 软件运行前,软件的代码及其相关数据都是存放于硬盘中的
- 任何软件的启动都是将数据从硬盘中读入内存,然后cpu从内存中取出指令并执行
- 软件运行过程中产生的数据最先都是存放于内存中的,若想永久保存软件产生的数据,则需要将数据由内存写入硬盘
1.1 什么是字符编码
二进制数即由0和1组成的数字,例如010010101010。计算机是基于电工作的,电的特性即高低电平,人类从逻辑层面将高电平对应为数字1,低电平对应为数字0,这直接决定了计算机可以识别的是由0和1组成的数字
字符编码中的编码指的是翻译或者转换的意思,即将人能理解的字符翻译成计算机能识别的数字
1.2 字符编码表的发展史 (了解)
1.2.1 阶段一:一家独大
现代计算机起源于美国,所以最先考虑仅仅是让计算机识别英文字符,于是诞生了ASCII表
ASCII表的特点:
- 只有英文字符与数字的一一对应关系
- 一个英文字符对应1bytes,1bytes=8bit,8bit最多包含256个数字,可以对应256个字符,足够表示所有英文字符(美国人完全够用了)
计算机中最小的单位 bit 表示一个状态 0 或 1
8bit = 1个字节 bytes 1KB = 1024Bytes 1MB = 1024B 1GB = 1024MB 1T = 1024GB
why is 1024? 因为计算机只能是被二进制,故都是2的次方
1.2.2 阶段二:诸侯割据、天下大乱
为了让计算机能够识别中文和英文,中国人定制了GBK(在ASCII的基础上扩展)
GBK表的特点:
- 只有中文字符、英文字符与数字的一一对应关系
- 一个英文字符对应1Bytes,一个中文字符对应2Bytes
📌 补充说明:
1Bytes=8bit,8bit最多包含256个数字,可以对应256个字符,足够表示所有英文字符
2Bytes=16bit,16bit最多包含65536个数字,可以对应65536个字符,足够表示所有中文字符
为让计算机能够识别自己国家的字符外加英文字符,各个国家都制定了自己的字符编码表
1)日本 Shift_JIS表的特点:只有日文字符、英文字符与数字的一一对应关系
2)韩国 Euc-kr表的特点:只有韩文字符、英文字符与数字的一一对应关系
- 文本编辑存取文件的原理
文本文件内容全都为字符,无论存取都是涉及到字符编码问题
#1、存文本文件
人类通过文本编辑器输入的字符会被转化成ASCII格式的二进制存放于内存中,如果需要永久保存,则直接将内存中的ASCII格式的二进制写入硬盘
#2、读文本文件
直接将硬盘中的ASCII格式的二进制读入内存,然后通过ASCII表反解成英文字符
1.2.3 阶段三:分久必合
unicode
于1990年开始研发,1994年正式公布,具备两大特点:
- 存在所有语言中的所有字符与数字的一一对应关系,万国码,即兼容万国字符
- 与传统的字符编码的二进制数都有对应关系
文本编辑器输入任何字符都是最新存在于内存中,是unicode编码的,存放于硬盘中,则可以转换成任意其他编码,只要该编码可以支持相应的字符
# 英文字符可以被ASCII识别
英文字符--->unciode格式的数字--->ASCII格式的数字
# 中文字符、英文字符可以被GBK识别
中文字符、英文字符--->unicode格式的数字--->gbk格式的数字
# 日文字符、英文字符可以被shift-JIS识别
日文字符、英文字符--->unicode格式的数字--->shift-JIS格式的数字
⭐ 总结:字符编码表 经历了三大过程
- 天下统一:ASCII表,只有英文字符;
- 天下大乱:各个国家有自己不同的字符编码表,这些编码表都兼容(即 都是在ASCII基础上扩展的),故英文字符和数字永远不会乱码,而其他中文、韩文等会造成乱码现象
本质 —> 存在外存上是一种编码,CPU执行的是另一种编码- 统一的方式:Unicode 万国码
1.3 编码与解码
1)由字符转换成内存中的unicode,以及由unicode转换成其他编码的过程,都称为编码encode (保存数据)
2)由内存中的unicode转换成字符,以及由其他编码转换成unicode的过程,都称为解码decode (读取数据)
a = '我爱你'
b = a.encode('utf-8') # 按照utf-8的字符编码 去保存数据,变成二进制的数据
b = a.encode() # 不写,默认就是utf-8
print(b) # 结果:b'\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0'
c = b.decode('utf-8') # 将二进制的数据按照utf-8的字符编码 翻译成字符
print(c) # 结果:我爱你
1.4 utf-8的由来
乱码的本质:读取文件时 你的字符编码表 和 保存文件的字符编码表 不一致
如何避免:读取文件和保存文件用同一个字符编码即可
推荐使用:utf-8
这个编码
注意:如果保存到硬盘的是GBK格式二进制,当初用户输入的字符只能是中文或英文,同理如果保存到硬盘的是Shift_JIS格式二进制,当初用户输入的字符只能是日文或英文… 如果我们输入的字符中包含多国字符,那么该如何处理?
多国字符 —√—> 内存(unicode格式的二进制)—X—> 硬盘(GBK格式的二进制)
多国字符 —√—> 内存(unicode格式的二进制)—X—> 硬盘(Shift_JIS格式的二进制)
多国字符 —√—> 内存(unicode格式的二进制)—√—> 硬盘(???格式的二进制)
理论上是可以将内存中unicode格式的二进制直接存放于硬盘中的,但由于unicode固定使用2个字节来存储一个字符,如果多国字符中包含大量的英文字符时,使用unicode格式存放会额外占用一倍空间(英文字符其实只需要用一个字节存放即可),然而空间占用并不是最致命的问题,最致命地是当我们由内存写入硬盘时会额外耗费一倍的时间,所以将内存中的unicode二进制写入硬盘或者基于网络传输时必须将其转换成一种精简的格式,这种格式即utf-8(全称Unicode Transformation Format,即unicode的转换格式)
多国字符—√—》内存(unicode格式的二进制)——√—》硬盘(utf-8格式的二进制)
为何在内存中不直接使用utf-8呢?
# utf-8是针对Unicode的可变长度字符编码:一个英文字符占1Bytes,一个中文字符占3Bytes,生僻字用更多的Bytes存储
# unicode更像是一个过渡版本,我们新开发的软件或文件存入硬盘都采用utf-8格式,等过去几十年,所有老编码的文件都淘汰掉之后,会出现一个令人开心的场景,即硬盘里放的都是utf-8格式,此时unicode便可以退出历史舞台,内存里也改用utf-8,天下重新归于统一
1.5 字符编码的应用
字符编码就是为了存取字符时不发生乱码问题:
- 内存中固定使用unicode无论输入任何字符都不会发生乱码
- 我们能够修改的是存/取硬盘的编码方式,如果编码设置不正确将会出现乱码问题。乱码问题分为两种:存乱了,读乱了
- 存乱了:如果用户输入的内容中包含中文和日文字符,如果单纯以shift_JIS存,日文可以正常写入硬盘,而由于中文字符在shift_jis中没有找到对应关系而导致存乱了
- 读乱了:如果硬盘中的数据是shift_JIS格式存储的,采GBK格式读入内存就读乱了
⭐总结:
- 保证存的时候不乱:在由内存写入硬盘时,必须将编码格式设置为支持所输入字符的编码格式
- 保证取的时候不乱:在由硬盘读入内存时,必须采用与写入硬盘时相同的编码格式
1.5.1 python解释器执行文件的前两个阶段
执行py文件的前两个阶段就是python解释器读文本文件的过程,与文本编辑读文本文件的前两个阶段没人任何区别,要保证读不乱码,则必须将python解释器读文件时采用的编码方式设置为文件当初写入硬盘时的编码格式,如果没有设置,python解释器则才用默认的编码方式
python3中默认为utf-8,在python2中默认为ASCII,我们可以通过指定文件头来修改默认的编码
-
在文件首行写入包含#号在内的以下内容
# coding: 当初文件写入硬盘时采用的编码格式
解释器会先用默认的编码方式读取文件的首行内容,由于首行是纯英文组成,而任何编码方式都可以识别英文字符
1.5.2 python解释器执行文件的第三个阶段
设置文件头的作用是保证运行python程序的前两个阶段不乱码,经过前两个阶段后py文件的内容都会以unicode格式存放于内存中
在经历第三个阶段时开始识别python语法,当遇到特定的语法name = ‘上’(代码本身也都全都是unicode格式存的)时,需要申请内存空间来存储字符串’上’,这就又涉及到应该以什么编码存储‘上’的问题了
在Python3中,字符串类的值都是使用unicode格式来存储
由于Python2的盛行是早于unicode的,因此在Python2中是按照文件头指定的编码来存储字符串类型的值的(如果文件头中没有指定编码,那么解释器会按照它自己默认的编码方式来存储‘上’),所以,这就有可能导致乱码问题
# coding:utf-8
x = '上' # x的值为untf-8格式的二进制
print(x) # 打印操作是将x的值,即utf-8格式的二进制交给终端,当终端收到后发现并不是unicode(只有unicode才与字符有对应关系),所以终端会执行操作:utf-8二进制---解码--> unicode格式的二进制,解码的过程终端会采用自己默认的编码,而在pycharm的终端默认编码为utf-8、windows下的cmd终端的默认编码为gbk,所以该打印操作在pycharm中显示正常,而在windows下的cmd中则乱码
# 在windows下的cmd中运行效果如下
C:\Users\Administrator>python2 E:\aaa.py
涓
python2后推出了一种补救措施,就是在字符串类型前加u,则会将字符串类型强制存储unicode,这就与python3保持一致了,对于unicode格式无论丢给任何终端进行打印,都可以直接对应字符不会出现乱码问题
# coding:utf-8
x = u'上' # 即便文件头为utf-8,x的值依然存成unicode
1.5.3 字符串encode编码与decode解码的使用
# 1、unicode格式------编码encode-------->其它编码格式
x='上' # 在python3在'上'被存成unicode
res=x.encode('utf-8')
print(res,type(res))
# unicode编码成了utf-8格式,而编码的结果为bytes类型,可以当作直接当作二进制去使用
# (b'\xe4\xb8\x8a', <class 'bytes'>)
# 2、其它编码格式------解码decode-------->unicode格式
print(res.decode('utf-8'))
# '上'
二、文件操作
文件
是操作系统给你提供永久保存数据的形式
2.1 文件操作的基本流程
2.1.1 基本流程
# 1.打开文件:得到一个硬盘的区域,并返回一个文件对象赋值给一个变量f
# 由应用程序向操作系统发起系统调用open(...),操作系统打开该文件
f=open('dbl.txt','r',encoding='utf-8') # 默认打开模式就为r
"""
必须要有的东西:
1) 文件路径:需要操作哪一份文件
2) 操作模式:
w 覆盖写:若没有文件,则自动创建文件;若有文件,则删除以前内容,写新的
a 追加写:若没有文件,则自动创建文件;若有文件,则在以前内容后面接着写
(注:w,a 只能自动创建文件,不能自动创建目录)
r 读取:若没有文件,则报错;若文件存在,则读取文件内容
3) 可写可不写:
字符编码:只有 文本文件 的时候才有这个玩意儿
encoding = "utf-8"
(若文本文件没有指定字符编码,则会 默认使用你现在的 操作系统默认的字符编码;
Windows默认是GBK,故若没有encoding会乱码)
"""
# 2.操作文件:操作那块区域
# 调用文件对象下的读/写方法,会被操作系统转换为读/写硬盘的操作
f.write('字符串') # 1) 写入数据
data=f.read() # 2) 读取数据
"""
若模式是w/a 那就只能写数据,数据必须是字符串类型
若模式是r 那就只能读取数据
"""
# 3.关闭文件:回收那块区域
# 向操作系统发起关闭文件的请求,回收系统资源
f.close()
# 自动帮助关闭文件(推荐写法⭐)
with open('dbl.txt','r',encoding='utf-8') as f:
# 操作文件
pass
# 关闭了文件了
# 📌 扩展:可同时操作多个文件
# 注:多份文件的变量名不能一样
with open('1.txt','r',encoding='utf-8') as f, open('2.txt','r',encoding='utf-8') as f1:
# 操作文件
pass
2.1.3 指定操作文本文件的字符编码
f = open(...)
"""
是由操作系统打开文件,如果打开的是文本文件,会涉及到字符编码问题,
如果没有为open指定编码,则打开文本文件的默认编码是操作系统的默认编码,
在 windows 下是 gbk,在 linux 下是 utf-8。
"""
# 若要保证不乱码,文件以什么方式存的,就要以什么方式打开:
f = open('a.txt','r',encoding='utf-8')
2.2 文件的操作模式与方法
2.2.1 控制文件读写操作的模式
r
(默认的):只读; w
:只写 ; a
:只追加写
# 1)r 模式的使用
# r只读模式: 在文件不存在时则报错,
# 文件存在文件内指针直接跳到文件开头
with open('a.txt',mode='r',encoding='utf-8') as f:
res=f.read() # 会将文件的内容由硬盘全部读入内存,赋值给res
# 💡小练习:简单的登录功能
input_user = input("请输入用户名:").strip()
input_pwd = input("请输入密码:").strip()
with open('UserInfo.txt', 'r', encoding='utf-8') as f:
for line in f:
# 把输入的用户名和密码 与 读出内容 做比对
user, pwd = line.strip('\n').split(':')
if input_user == user and input_pwd == pwd:
print("登录成功")
break # 循环立即退出
elif input_user == user and input_pwd != pwd:
print("密码错误")
break
else: # 整份文件都循环完了,都没对比成功
print("用户不存在")
# 2)w 模式的使用
# w只写模式: 在文件不存在时会自动创建空文档,但不能创建目录
# 文件存在会清空文件,文件指针跑到文件开头
with open('b.txt',mode='w',encoding='utf-8') as f:
f.write('你好\n')
f.write('我好\n')
f.write('大家好\n')
f.write('111\n222\n333\n')
#强调:
# 1 在文件不关闭的情况下,连续的写入,后写的内容一定跟在前写内容的后面
# 2 如果重新以w模式打开文件,则会清空文件内容
# 3)a 模式的使用
# a只追加写模式: 在文件不存在时会创建空文档
# 文件存在 会将文件指针直接移动到文件末尾
with open('c.txt',mode='a',encoding='utf-8') as f:
f.write('44444\n')
f.write('55555\n')
"""
📌 强调 w 模式与 a 模式的异同:
1.相同点:在打开的文件不关闭的情况下,连续的写入,新写的内容总会跟在前写的内容之后
2.不同点:以 a 模式重新打开文件,不会清空原文件内容,会将文件指针直接移动到文件末尾,新写的内容永远写在最后
"""
# 💡小练习:简单的注册功能:
# 1. 不能覆盖写,有多个用户
# 2. 要有分隔符 用户与密码间: 用户与用户间\n
user = input('请输入用户名:').strip()
pwd = input('请输入密码:').strip()
with open('UserInfo.txt', mode='a', encoding='utf-8') as f:
# info='%s:%s\n' %(name,pwd)
info = f'{user}:{pwd}\n'
f.write(info)
print("注册成功")
open('文件路径','操作的模式')
open('文件路径','a/w/r') # 以追加写/覆盖写/读取的方式操作一份文本文件
open('文件路径','at/wt/rt') # 同上效果,以追加写/覆盖写/读取的方式操作一份文本文件
open('文件路径','ab/wb/rb') # 以追加写/覆盖写/读取的方式操作一份二进制文件
- 模式的使用 r+ w+ a+ :可读可写(在平时工作中,我们只单纯使用r/w/a,要么只读,要么只写,一般不用可读可写的模式)
- 读取数据:读取光标之后的数据内容
2.2.2 控制文件读写内容的模式
大前提: tb模式均不能单独使用,必须与r/w/a之一结合使用
文本文件 t
:只有文字和符号的文件
二进制文件 b
:本质上所有的文件都是二进制文件
t(默认的):文本模式 | b:二进制模式 |
---|---|
读写文件都是以字符串为单位的 | 读写文件都是以bytes/二进制为单位的 |
只能针对文本文件 | 可以针对所有文件 |
必须指定encoding参数 | 一定不能指定encoding参数 |
# 案例一:t 模式的使用
# t 模式:如果我们指定的文件打开模式为r/w/a,其实默认就是rt/wt/at
with open('a.txt',mode='rt',encoding='utf-8') as f:
res=f.read()
print(type(res)) # 输出结果为:<class 'str'>
with open('a.txt',mode='wt',encoding='utf-8') as f:
s='abc'
f.write(s) # 写入的也必须是字符串类型
# 强调:t模式只能用于操作文本文件,无论读写,都应该以 字符串 为单位,
# 存取硬盘本质都是二进制的形式,当指定t模式时,内部帮我们做了编码与解码
# 案例二: b 模式的使用
# b: 读写都是以二进制位单位,一定不能encoding编码
with open('1.mp4',mode='rb') as f:
data=f.read()
print(type(data)) # 输出结果为:<class 'bytes'>
with open('a.txt',mode='wb') as f:
msg="你好"
res=msg.encode('utf-8') # res为bytes类型
f.write(res) # 在b模式下写入文件的只能是bytes类型
#强调:b模式对比t模式
# 1.在操作纯文本文件方面t模式帮我们省去了编码与解码的环节,b模式则需要手动编码与解码,所以此时t模式更为方便
# 2.针对非文本文件(如图片、视频、音频等)只能使用b模式
# ⭐实现对任何文件的复制
with open(文件路径,'rb') as 变量名1,open(文件路径,'wb') as 变量名2:
变量名2.write(变量名1.read())
# 💡小练习: 编写拷贝工具
src_file=input('源文件路径: ').strip()
dst_file=input('目标文件路径: ').strip()
with open(r'%s' %src_file,mode='rb') as read_f,open(r'%s' %dst_file,mode='wb') as write_f:
for line in read_f:
# print(line)
write_f.write(line)
2.2.3 📌操作文件的方法
# 读操作⭐
f.read() # 读取所有内容,执行完该操作后,文件指针会移动到文件末尾
f.read(3) # 从光标开始读取3个字符的内容
f.readline() # 读取一行内容,光标移动到第二行首部
f.readlines() # 读取每一行内容,存放于列表中
# 强调:
# f.read()与f.readlines()都是将内容一次性读入内容,如果内容过大会导致内存溢出,若还想将内容全读入内存,则必须分多次读入,有两种实现方式:
# 方式一
with open('a.txt',mode='rt',encoding='utf-8') as f:
for line in f:
print(line) # 同一时刻只读入一行内容到内存中
print(line.strip('\n').split(':')) #同一时刻只读入一行内容到内存中,且去除换行,然后用 :进行分割,形成一个列表
# 方式二
with open('1.mp4',mode='rb') as f:
while True:
data=f.read(1024) # 同一时刻只读入1024个Bytes到内存中
if len(data) == 0:
break
print(data)
# 写操作
f.write() # 直接写
f.write('1111\n222\n') # 针对文本模式的写,需要自己写换行符
f.write('1111\n222\n'.encode('utf-8')) # 针对b模式的写,需要自己写换行符
f.writelines() # 将一个列表/字符串里的数据写入同一行
f.writelines(['333\n','444\n']) # 文件模式
f.writelines([bytes('333\n',encoding='utf-8'),'444\n'.encode('utf-8')]) #b模式
f.readable() # 文件是否可读
f.writable() # 文件是否可读
f.closed # 文件是否关闭
f.encoding # 如果文件打开模式为b,则没有该属性
f.flush() # 立刻将文件内容从内存刷到硬盘
f.name
2.3 📌主动控制文件内指针移动
#大前提:二进制文件b内指针的移动都是字节Bytes为单位的;文本文件t的read(n),n以字符为单位
with open('a.txt',mode='rt',encoding='utf-8') as f:
data=f.read(3) # 从光标开始读取3个字符
with open('a.txt',mode='rb') as f:
data=f.read(3) # 读取3个Bytes
"""
之前文件内指针的移动都是由读/写操作而被动触发的,
若想读取文件某一特定位置的数据,则需要用f.seek方法主动控制文件内指针的移动
f.seek(指针移动的字节数,模式控制):
"""
# 模式控制:
# 0: 默认的模式,该模式代表指针移动的字节数是以 文件开头 为参照的
# 1: 该模式代表指针移动的字节数是以 当前所在的位置 为参照的
# 2: 该模式代表指针移动的字节数是以 文件末尾的位置 为参照的
# 强调:其中0模式可以在t或者b模式使用,而1跟2模式只能在b模式下用
2.3.1 案例一: 0模式详解
# a.txt用utf-8编码,内容如下(abc各占1个字节,中文“你好”各占3个字节)
abc你好
# 0模式的使用
with open('a.txt',mode='rt',encoding='utf-8') as f:
f.seek(3,0) # 参照文件开头移动了3个字节
print(f.tell()) # 查看当前文件指针距离文件开头的位置,输出结果为3
print(f.read()) # 从第3个字节的位置读到文件末尾,输出结果为:你好
# 注意:由于在t模式下,会将读取的内容自动解码,所以必须保证读取的内容是一个完整中文数据,否则解码失败
with open('a.txt',mode='rb') as f:
f.seek(6,0)
print(f.read().decode('utf-8')) #输出结果为: 好
2.3.1 案例二: 1模式详解
# 1模式的使用
with open('a.txt',mode='rb') as f:
f.seek(3,1) # 从当前位置往后移动3个字节,而此时的当前位置就是文件开头
print(f.tell()) # 输出结果为:3
f.seek(4,1) # 从当前位置往后移动4个字节,而此时的当前位置为3
print(f.tell()) # 输出结果为:7
2.3.1 案例三: 2模式详解
# a.txt用utf-8编码,内容如下(abc各占1个字节,中文“你好”各占3个字节)
abc你好
# 2模式的使用
with open('a.txt',mode='rb') as f:
f.seek(0,2) # 参照文件末尾移动0个字节, 即直接跳到文件末尾
print(f.tell()) # 输出结果为:9
f.seek(-3,2) # 参照文件末尾往前移动了3个字节
print(f.read().decode('utf-8')) # 输出结果为:好
# 小练习:实现动态查看最新一条日志的效果
import time
with open('access.log',mode='rb') as f:
f.seek(0,2)
while True:
line=f.readline()
if len(line) == 0:
# 没有内容
time.sleep(0.5)
else:
print(line.decode('utf-8'),end='')
2.4 文件的修改
# 文件a.txt内容如下
张一蛋 山东 179 49 12344234523
李二蛋 河北 163 57 13913453521
王全蛋 山西 153 62 18651433422
# 执行操作
with open('a.txt',mode='r+t',encoding='utf-8') as f:
f.seek(9)
f.write('<妇女主任>')
# 文件修改后的内容如下
张一蛋<妇女主任> 179 49 12344234523
李二蛋 河北 163 57 13913453521
王全蛋 山西 153 62 18651433422
# 强调:
# 1、硬盘空间是无法修改的,硬盘中数据的更新都是用新内容覆盖旧内容
# 2、内存中的数据是可以修改的
文件修改的本质:
① 将硬盘中文件内容读入内存
② 然后在内存中修改完毕后
③ 再覆盖回硬盘
2.4.1 文件修改方式一
# 实现思路:将文件内容发一次性全部读入内存,然后在内存中修改完毕后再覆盖写回原文件
# 优点: 在文件修改过程中同一份数据只有一份
# 缺点: 会过多地占用内存
# 1.打开要修改的文件,读取文件全部内容 至内存中
with open('db.txt',mode='rt',encoding='utf-8') as f:
data=f.read()
# 2.再打开这份文件,将内存中数据写入该文件
with open('db.txt',mode='wt',encoding='utf-8') as f:
f.write(data.replace('哒卟了','dabul'))
2.4.2 文件修改方式二
# 实现思路:以读的方式打开原文件,以写的方式创建一个临时文件,一行行读取原文件内容,修改完后写入临时文件...,删掉原文件,将临时文件重命名原文件名
# 优点: 不会占用过多的内存
# 缺点: 在文件修改过程中同一份数据存了两份
import os
with open('db.txt',mode='rt',encoding='utf-8') as read_f,\
open('.db.txt.swap',mode='wt',encoding='utf-8') as wrife_f:
for line in read_f:
wrife_f.write(line.replace('SB','kevin'))
os.remove('db.txt')
os.rename('.db.txt.swap','db.txt')