Drollery Medieval drollery of a knight on a horse

🏆 欢迎来到本站: https://xuchangwei.com/希望这里有你感兴趣的内容

flowery border with man falling
flowery border with man falling

Python: 系统编程

Python 文件系统操作编程

主要内容:

  • 文件打开与文件描述符
  • 文件的编码与UNICODE
  • 二进制文件与文本
  • 文本文件的读写
  • 二进制文件的读写
  • 文件的迭代器读写方式
  • 文件读写的异常与 With 语句
  • 目录与文件遍历
  • 目录创建与删除
  • 目录与文件存在判定
  • 目录与文件属性查看
  • StringIO 和 ByteIO 使用
  • 高级文件操作模块 Shutil 使用
  • CSV 文件读写与解析
  • ini 配置文件的读写与解析

文件操作

冯诺依曼体系架构

image-20210215092720081.png

CPU由运算器和控制器组成:

  • 运算器,完成各种算数运算、逻辑运算、数据传输等数据加工处理
  • 控制器,控制计算机各部件协调运行
  • 存储器,用于记忆程序和数据,例如内存
  • 输入设备,将数据或程序输入到计算机中,例如键盘、鼠标
  • 输出设备,将数据或程序的处理结果展示给用户,例如显示器、打印机等

一般说IO操作,指的是文件IO,如果指的是网络IO,都会直接说网络IO磁盘

目前依然是文件持久化最重要设备。

1956年,IBM发明可以存储5MB的磁盘驱动器。磁盘使用磁性材料记录数据,使用NS极表示0或1。

现代磁盘使用温彻斯特磁盘驱动器,如下

温彻斯特磁盘拥有多个同轴金属盘片,盘片上涂上磁性材料,一组可移动磁头。运行时,磁头摆动从飞速旋转的盘片读取或写入数据。一般常见盘片 转速 为5400转/分钟、7200转/分钟等。磁头摆动定位数据过程称为 寻道 。转速、寻道都影响磁盘读写性能。

磁头不和盘片接触,磁盘内部也不是真空,磁头悬浮在磁盘上方。一组磁头从0编号。

盘片Platter: 涂满磁性材料的盘片,早期是金属的,现在多为玻璃。

磁道Track:盘片划分同心圆,同心圆间距越小,一般来说存储容量越大。磁道编号由外向内从0开始。

扇区Sector:一个磁道上能够存储的0或1信号太多了,所以需要进一步划分成若干弧段,每一个弧段就是一个扇区。通常一个扇区为512字节。每个扇区都有自己的编号。

柱面Cylinder:从垂直方向看,磁道的同心圆就构成了圆柱。编号由外向内从0开始。

img_20241230_175531.png

注意:盘片内圈扇区面积小,外圈面积大。但扇区都是512字节。外圈磁密度低,内圈磁密度高。

如今磁盘,可以做到磁密度一致,所以磁盘划分,外圈可以划分更多扇区,这样可以存储更多数据。这种分区弧长一致的分配扇区方法称为ZBR(Zoned-bit recording),按照半径分成不同的磁道组称为Zone。这样,转动同样的角度,外圈读取数据更多。

img_20241230_180053.png

簇Cluster:操作系统为了优化IO,使用磁盘空间时最小占用的多个扇区。一般簇大小为4-64个扇区(2KB-32KB)。所以,一个文件就有了物理占用空间大小即实际分得的磁盘空间,和逻辑占用大小即文件本身的实际字节数。往往实际占用空间都比文件本身大。越是小文件,越浪费空间。

文件IO常用操作

  • open 打开
  • read 读取
  • write 写稿
  • close 关闭
  • readline 行读取
  • readlines 多行读取
  • seek 文件指针操作
  • tell 指针位置

打开操作

open 方法

open(
    file, #文件对象,路径
    mode='r', #模式
    buffering=-1, #缓冲区
    encoding=None, #编码
    errors=None, #出错怎么办
    newline=None, #遇到换行符怎么办
    closefd=True, #是否关闭文件描述符
    opener=None, #打开的是谁
)

打开一个文件,返回一个文件对象(流对象)和文件描述符。打开文件失败,则返回异常

基本使用:创建一个文件test,然后打开它,用完 关闭

# coding: utf-8
#open('test') #报错,当前路径不存在,FileNotFoundError: [Errno 2] No such file or directory: 'test'

f = open('test')
#windows <_io.TextIOWrapper name='test' mode='r' encoding='cp936'>
#linux <_io.TextIOWrapper name='test' mode='r' encoding='UTF-8'>
print(f.read()) #读取文件
f.close #使用完后关闭

# open成功后,一定可以拿到一个文件对象,就可以对它进行增删改查, 文件操作,读写
#文件对象,默认是文本的打开方式
#encoding='cp936' #windows默认GBK,codepage代码页 936 等价认为是GBK
#encoding='UTF-8' #linux系统默认都是用utf-8 中文大多数3字节

文件操作中,最常用的操作就是读和写。

文件访问的模式有两种:文本模式和二进制模式。不同模式下,操作函数不尽相同,表现结果也不一样。

注:windows中使用codepage代码页,可以认为每一个代码页就是一张编码表。cp936等同于GBK。

open的参数
file

打开或要创建的文件。如果不指定路径,默认是当前路径。

mode模式

模式描述字符意义:

  • r 缺省模式,只读打开。文件不存在,立即报错。文件存在,从头开始读,文件指针初始在开头即索引0的位置。
  • w 只写打开,不支持读取。文件存在,文件内容将被清空,从头写;文件不存在,文件被创建,从头写。白纸一张,从头写,初始位置在0
  • x 创建并写入一个新文件,不支持读取。文件存在,抛异常;文件不存在不报错,新文件被创建
  • a 只写打开,追加内容,不支持读取。文件存在,追加写入,文件不存在,创建文件,从头写
  • b 二进制模式。字节流,将文本按照字节理解,与字符编码无关。二进制模式操作时,字节操作使用bytes类型。不能单独使用,必须和rwax四种之一配合使用,不影响主模式。
  • t 缺省模式,文本模式。text,默认可以不写。不能单独使用,必须和wrax配合使用,不影响主模式
  • + 读或写打开后,使用+来增加缺失的写或读的能力。必须和主模式(rwax)配合使用,不影响主模式。
    • r+ => rt+ 只读文本模式打开,补充写能力。文件指针在开头。
    • w+ => wt+ 只写文本模式打开,补充读能力。w能力不变,会清空所有内容

在上面的例子中,可以看到默认是文本打开模式,且是只读的。

# coding: utf-8
#mode rwxabt+

#r 只读模式
f = open('D:/tmp/test')
print(f) #: <_io.TextIOWrapper name='D:/tmp/test' mode='r' encoding='cp936'>
print(f.readable()) #: True 可读的 r readonly
print(f.read()) #文件空的
#f.write('abc') #报错 io.UnsupportedOperation: not writable
f.close()


f = open('D:/tmp/test', 'r') #只读
#f.write('abc') #报错 io.UnsupportedOperation: not writable
f.close()

#w写模式
f = open('D:/tmp/test1', 'w') #test1 不存在
print(f.write('abc')) #返回值3,3表示写入的字符数?字节数?
#print(f.write('啊')) #返回1,目前返回值是表示字符数
#f.read() #报错,w模式是只写
f.close()

f = open('D:/tmp/test1', 'w') #test1存在,w打开,内容没了
f.write('xyz')
f.close()

#a模式
f = open('D:/tmp/test1', 'a') #test1存在
#f.read() #a模式不支持读取
f.write('123') #追加写入
f.close()


f = open('D:/tmp/test2', 'a') #test1不存在,创建文件,从头写
#a模式,不管文件存在与否,都从尾部追加写入
f.write('123\n')
f.write('789')
f.close()

#x模式
#f = open('D:/tmp/test', 'x') #x模式 exsits,文件存在,报错
f = open('D:/tmp/testx', 'x') #x模式 exsits,文件不存在,文件被创建
#f.read() #x模式不支持读取
f.write('testx test')
f.close()

#t模式 text
f = open('D:/tmp/testx', 'r')
#字符和编码有关,字符串的世界都是由编码的
#rwax 默认都用文本模式打开 rt wt at xt,不能独立使用
print(f) #: <_io.TextIOWrapper name='D:/tmp/testx' mode='r' encoding='cp936'>
f.close()

#b模式,binnary, 二进制
f = open('D:/tmp/testx', 'rb') #ab, wb
print(f) # encoding 字节的世界都是0和1组成的字节,和编码无关
#: <_io.BufferedReader name='D:/tmp/testx'>
print(f.read()) #返回字节 : b'testx test'
f.close()

f = open('D:/tmp/testx', 'wb') #ab, wb
f.write(b'xyz') #返回一个值,写入的字节数
f.write('zhongwen'.encode()) #缺少编码,utf-8,二进制模式,跟字符无关
f.close()

#+模式
#+ 附加的能力,必须和主模式(rwax)配合使用,不影响主模式。r+ w+ a+ x+
f = open('D:/tmp/testx', 'r+') 
f.read()
f.write('xyz') #可写
f.read()  #写入时,什么都读不到
f.seek(0) #用了seek会立即写入磁盘
f.read()  #可以读到 
f.close()

f = open('D:/tmp/testx', 'w+') 
f.read()  #写入时,什么都读不到
f.write('xyz') #可写
f.close()
#注意文件指针
文件指针

文件指针,指向当前字节位置。

  • mode=r,指针起始在0
  • mode=a,指针起始在EOF

tell()显示指针当前位置

seek(offset[,whence]) 移动文件指针位置。offset偏移多少字节,whence从哪里开始。

文本模式下

  • whence 0 缺省值,表示从头开始,offset只能正整数
  • whence 1 表示从当前位置,offset只接受0
  • whence 2 表示从EOF开始,offset只接受0

二进制模式下

  • whence 0 seek(0) seek(0, 0) seek(x) x >=0
  • whence 1 表示从当前位置,offset可正可负,不能左超界
  • whence 2 表示从EOF开始,offset可正可负,不能左超界
# coding: utf-8

f = open('D:/tmp/test', 'w+')
print(f.tell()) #: 0
print(f.write('abc')) #: 3  写入了3个字符
print(f.tell()) #: 3 指针移动了,返回的一直是字节的索引
print(f.write('de')) #: 2  写入了2个字符
print(f.tell()) #: 5 指针移动了,当前位置在5
f.close()

#以文本方式打开 
f = open('D:/tmp/test')
print(f.tell()) #: 0
print(f.read(2)) #: ab
print(f.tell()) #: 2
print(f.read(1)) #: c
print(f.tell()) #: 3
f.seek(0) #设置指针位置
print(f.read(1)) #: a
print(f.tell()) #: 1
print(f.read()) #bcde 从当前位置到结尾
f.close() 

#seek
f = open('D:/tmp/test')
f.seek(0, 0) #第一个0 表示偏移,第二个0 表示whence 0表示从头开始,拉回到开头。whence 0 是默认的
print(f.read()) #abcde
f.seek(0)
print(f.read(2)) #ab
f.seek(4, 0)
print(f.read()) #e
f.seek(0, 2) #跳到EOF
#f.seek(-2, 0) # 报错,超出左界,不可以
f.seek(20, 0) #向右超界允许


#二进制模式 seek
f = open('D:/tmp/test', 'rb')
f.seek(2)
#f.seek(-2) #f.seek(-2, 0) 由于左超界,所以报错
f.seek(-2, 1) #当前位置向左走2, 但不能超界,否则报错
print(f.tell()) #0
f.seek(100, 1)
print(f.read()) #b'' 二进制世界,没有什么乱码,是字节就读

二进制模式支持任意起点的偏移,从头、从尾、从中间位置开始。

向后seek可以超界,但是向前seek的时候,不能超界,否则抛异常

buffering缓冲区

buffering=-1 使用默认缓冲区大小。如果是二进制模式,使用 io.DEAFAULT_BUFFER_SIZE 值,默认是4096或8192.如果是文本模式,如果是终端设备,是行缓存方式,如果不是,则使用二进制模式的策略。

0 文本模式不支持;二进制模式无缓冲区, 立即写入磁盘,相当于把文件当做一个FIFO队列用。
1,只在文本模式使用,表示使用行缓冲。意思就是见到换行符\n就flush写入磁盘
  - 文本模式,如果缓冲区撑破了,被迫写入磁盘
  - 二进制模式,1无效,使用的默认缓冲区
- 大于1, 用于指定buffer的大小
  - 文本模式,默认缓冲区
  - 二进制模式,指定大小字节的缓冲区

buffer 缓冲区

缓冲区一个内存空间,一般来说是一个FIFO队列,到缓冲区满了或者达到阈值,数据才会flush到磁盘。

fush() 将缓冲区数据写入磁盘

close() 关闭前会调用flush()

io.DEAFAULT_BUFFER_SIZE 缺省缓冲区大小,字节

先看二进制模式

# coding: utf-8
# 缓冲区大小为0
f = open('D:/tmp/test', 'w+b', 0) #文本模式不支持,只在二进制模式,立即写入磁盘
f.write(b'a') 
f.write(b'b')
print(f.read()) #返回空,但文本已被写入内容
f.seek(0)
print(f.read()) #: b'ab'
f.close()


# 缓冲区大小为1
f = open('D:/tmp/test', 'w+', 1) #只在文本模式,使用行缓冲。见到换行符 \n 就flush
f.write('a')
f.write('a')
print(f.read())
f.write('\n\t\n')
print(f.read())
f.seek(0)
print(f.read())
f.close()

#如果缓冲区撑破了,被迫写入磁盘
f = open('D:/tmp/test', 'w+', 1) #只在文本模式,使用行缓冲。见到换行符 \n 就flush
f.write('a' * 8100)
f.write('b' * 100)
f.close()
import io
print(io.DEFAULT_BUFFER_SIZE) #: 8192


f = open('D:/tmp/test', 'wb+', 1)  #对二进制来讲,1无效,
f.write(b'a')
f.close()


# 设置缓冲区大小
f = open('D:/tmp/test', 'wb+', 2)  #对二进制来讲
f.write(b'a')
f.write(b'b')
f.close()
buffering=-1
t和b,都是io.DEAFAULT_BUFFER_SIZE

buffering=0
b关闭缓冲区
t不支持

bufferin=1
t行缓冲,遇到换行符才flush

buffering>1
b模式表示缓冲大小。缓冲区的值可以超过io.DEAFAULT_BUFFER_SIZE,直到设定的值超出后才把缓冲区flush
t模式,是io.DEAFAULT_BUFFER_SIZE字节,flush完后把当前的字符串也写入磁盘。

一般来说,只需要记得:

  • 文本模式,一般都用默认缓冲区大小
  • 二进制模式,是一个个字节的操作,可以指定buffer大小
  • 一般来说,默认缓冲区大小是个比较好的选择,除非明确知道,否则不调整它
  • 一般编程中,明确知道需要写磁盘了,都会手动调用一次flush,而不是等到自动flush或者close的时候。
encoding

编码,仅文本模式使用

None 表示使用缺省编码,依赖操作系统。windows 下缺省 GBK(0xB0A1), Linux下缺省UTF-8(0xE5 95 8A)

errors

什么样的编码错误将被捕获

None和'strict' 表示有编码错误将抛出ValueError异常;'ignore'表示忽略

newline

文本模式中,换行的转换。取值可以为None、''空串、'\r'、'\n'、'\r\n'

读时

  • None,缺省值,表示'\r'、'\n'、'\r\n' 都被转换为 '\n'
  • ''表示不会自动转换通用换行符,常见换行符都可以认为是换行
  • 其它合法换行字符表示按照该字符分行

写时

  • None,缺省值,表示'\n'都会被替换为系统缺省行分隔符os.linesep
  • '\n' 或''空串表示'\n' 不替换
  • 其它合法换行字符表示文本中的'\n'换行符会被替换为指定的换行字符
# coding: utf-8
#newline 新行, \n \r\n
txt = 'python\rpip\nxx.com\r\nok'
print(txt)

#写入
txt = 'python\rpip\nxx.com\r\nok'
f = open('D:/tmp/test', 'w+')
f.write(txt) #做了一些事,newline 
#txt = 'python\rpip\r\nxx.com\r\r\nok'
#None,把\n换成当前操作系统的默认换行符,windows \r\n
f.close()

#读取
f = open('D:/tmp/test') #newline=None
t = f.read()
print(t.encode()) #: b'python\npip\nxx.com\n\nok'
# None,\r \n \r\n => \n 把常见换行符换成\n
f.seek(0)
print(f.readlines())#: ['python\n', 'pip\n', 'xx.com\n', '\n', 'ok']
f.close


f = open('D:/tmp/test', newline='') #newline='' 什么都不干,按照各系统换行处理
t = f.read()
print(t.encode()) #: b'python\rpip\r\nxx.com\r\r\nok'
f.seek(0)
print(f.readlines()) #: ['python\r', 'pip\r\n', 'xx.com\r', '\r\n', 'ok']
f.close


f = open('D:/tmp/test', newline='\n') #newline='\n' 什么都不干,文件处理按\n处理
t = f.read()
print(t.encode()) #: b'python\rpip\r\nxx.com\r\r\nok'
f.seek(0)
print(f.readlines()) #: ['python\rpip\r\n', 'xx.com\r\r\n', 'ok']
f.close

f = open('D:/tmp/test', newline='\r') #newline='\r' 什么都不干,文件处理按\r处理
t = f.read()
print(t.encode()) #: b'python\rpip\r\nxx.com\r\r\nok'
f.seek(0)
print(f.readlines()) #: ['python\r', 'pip\r', '\nxx.com\r', '\r', '\nok']
f.close


f = open('D:/tmp/test', newline='\r\n') #newline='\r\n' 什么都不干,文件处理按\r\n处理
t = f.read()
print(t.encode()) #: b'python\rpip\r\nxx.com\r\r\nok'
f.seek(0)
print(f.readlines()) #: ['python\rpip\r\n', 'xx.com\r\r\n', 'ok']
f.close
其它

closefd 关闭文件描述符,True表示关闭它。False会在文件关闭后保持这个描述符。fileobj.fileno()查看

# coding: utf-8

#open(
#    file, #文件对象,路径
#    mode='r', #模式
#    buffering=-1, #缓冲区
#    encoding=None, #编码
#    errors=None, #出错怎么办
#    newline=None, #遇到换行符怎么办
#    closefd=True, #是否关闭文件描述符
#    opener=None, #打开的是谁
#)

f = open('D:/tmp/test')
print(f.fileno()) #文件描述符
f.close() #关闭后,文件描述符关闭
print(f.closed) #是否关闭
#: 3
#: True

read

read(size=-1)

size表示读取的多少字符或字节;负数或者None表示读取到EOF

行读取

readline(size=-1)

一行行读取文件内容。size设置一次能读取行内几个字符或字节

readlines(hint=-1)

读取所有行的列表。指定hint则返回指定的列表。

常用的读取方式

# coding: utf-8
#按行迭代
f = open('D:/tmp/test') #返回可迭代对象,惰性的

for line in f:
    print(line.strip())

f.close()

write

write(s),把字符串s写入到文件中并返回字符的个数

writelines(lines),将字符串列表写入文件。

# coding: utf-8
f = open('d:/tmp/test', 'w+')

lines = ['abc', '123\n', 'cici'] #提供换行符
f.writelines(lines) #writelines 但是不解决换行问题

f.seek(0)
print(f.read())
f.close()

#: abc123
#: cici

close

flush 并关闭文件对象

文件已经关闭,再次关闭没有任何效果

其他

  • seekable() 是否可seek
  • readable() 是否可读
  • writeable() 是否可写
  • closed 是否已经关闭

上下文管理

问题的引出

文件打开会占文件描述符。

在Linux中,执行

# coding: utf-8

#打开2000个文件
lst = []
for _ in range(2000):
    lst.append(open('test'))
# OSError: [Error 24] Too many open files: 'test'

print(len(lst))

lsof 列出打开的文件。 yum install lsof 安装

ps aux |grep python

lsof -p 9255|grep test| wc -l

ulimit -a
open file  1024 # 能打开的文件描述符数,默认1024
上下文管理

上下文管理:

  • 使用with关键字,上下文管理针对的是with后的对象
  • 可以使用as关键字
  • 上下文管理的语句块并不会开启新的作用域

文件对象上下文管理

  • 进入with时,with后的文件对象是被管理对象
  • as子句后的标识符,指向with后的文件对象
  • with语句块执行完的时候,会自动关闭文件对象

解决

# coding: utf-8

f = open('d:/tmp/test')
try:  #尝试捕获try异常
    print(f.closed)
    f.write('abc')
    print(f.read())
finally:
    f.close()

print(f.closed)

with使用

with 对象 [as 标识符]:
    pass 
# coding: utf-8

with open('d:/tmp/test') as f: #在文件对象with它,as f,f 就是该文件对象 f = open('d:/tmp/test')
    print(f)
    print(f.closed) #是不关闭
    print(f.read())
    #with语句块结束了,自动调用该文件对象的close方法,出错时也会调用close方法

f = open('d:/tmp/test')
with f:
    print(f)
    print(f.closed)
    print(f.read())
    #哪怕出现异常,离开with时,依然调用with后文件对象的close方法

print(f.closed) #True
  • 对象类似于文件对象的IO对象,一般来说都需要在不使用的时候关闭、注销,以释放资源
  • IO被打开的时候,会获得一个文件描述符。计算机资源是有限的,所以操作系统都会做限制。就是为了保护计算机的资源不要被完全耗尽,计算资源是很多程序共享的,不是独占的。
  • 一般情况下,除非特别明确的知道当前资源情况,否则不要盲目提高资源的限制值解决问题。

StringIO和BytesIO

StringIO

  • io模块中的类
    • from io import StringIO
  • 内存中,开辟的一个 文本模式 的buffer,可以像文件对象一样操作它
  • 当close方法被调用的时候,这个buffer会被释放

getvalue()获取全部内容。跟文件指针没有关系。

好处

一般来说,磁盘的操作比内存的操作要慢得多,内存足够的情况下,一般的优化思路是少 落地 ,减少磁盘IO的过程,可以大大提高程序的运行效率。

BytesIO

  • io模块中类
    • from io import BytesIO
  • 内存中,开辟的一个二进制模式的buffer,可以像文件对象一样操作它
  • 当close方法被调用的时候,这个buffer会被释放
# coding: utf-8
from io import StringIO, BytesIO

s = StringIO() #IO缓冲区,文本
print(s.seekable(), s.readable(), s.writable())
print(s.read())
print(s.write('abc'))
print(s.read()) #没读到,文件指针指到最后了
s.seek(0)
print(s.readlines()) #['abc']
#print(s.fileno()) #没有文件描述符,没有使用。内存中东西
print(s.read()) #上面读完就读不出东西
print('-' * 30)
print(s.getvalue()) #内存中建立类文件对象,无视指针,直接获取全部数据
s.close()
 #True True True
#
#3
#
#['abc']
#
#------------------------------
#abc



s = BytesIO() #IO缓冲区,字节
print(s.seekable(), s.readable(), s.writable())
print(s.read())
print(s.write(b'abc'))
print(s.read()) #没读到,文件指针指到最后了
s.seek(0)
print(s.readlines()) #[b'abc']
#print(s.fileno()) #没有文件描述符,没有使用。内存中东西
print(s.read()) #上面读完就读不出东西
print('-' * 30)
print(s.getvalue()) #内存中建立类文件对象,无视指针,直接获取全部数据
s.close()
#True True True
#b''
#3
#b''
#[b'abc']
#b''
#------------------------------
#b'abc'


#支持上下文
s = BytesIO() #IO缓冲区,字节
with s:
    print(s.seekable(), s.readable(), s.writable())
    print(s.read())
    print(s.write(b'abc'))
    print(s.read()) #没读到,文件指针指到最后了
    s.seek(0)
    print(s.readlines()) #[b'abc']
    #print(s.fileno()) #没有文件描述符,没有使用。内存中东西
    print(s.read()) #上面读完就读不出东西
    print('-' * 30)
    print(s.getvalue()) #内存中建立类文件对象,无视指针,直接获取全部数据
#s.close()
print(s.closed)

flie-like对象

  • 类文件对象,可以像文件对象一样操作
  • socket对象、标准输入输出对象(stdin、stdout) 都是类文件对象
# coding: utf-8
from sys import stderr, stdout #0 stdin; 1 stdout; 2 stderr

f = stdout #可写
print(f.seekable(), f.writable(), f.readable())
f.write('abc')
print('xzy') #默认调用stdout
#: True True False
#: abcxzy
# coding: utf-8
from sys import stderr, stdout #0 stdin; 1 stdout; 2 stderr
f1 = stderr
print(f1.seekable(), f1.writable(), f1.readable())
f1.write('123')
print('456', file=stderr)

#False True False
#123456
>>> help(print)
Help on built-in function print in module builtins:

print(*args, sep=' ', end='\n', file=None, flush=False)
    Prints the values to a stream, or to sys.stdout by default.

    sep
      string inserted between values, default a space.
    end
      string appended after the last value, default a newline.
    file
      a file-like object (stream); defaults to the current sys.stdout.
    flush
      whether to forcibly flush the stream.
with open('d:/tmp/test', 'w') as f:
    print('abcdef\n1234', file=f) #写到文件中
    #f.close()

正则表达式基础

概述

正则表达式,Regular Expression,缩写为regex、regexp、RE等

正则表达式是文本处理极为重要的技术,用它可以对字符串按照某种规则进行检索、替换。

1970年代,Unix之父Ken Thompson将正则表达式引入到Unix中文本编辑器ed和grep命令中,由此正则表达式普及开来。

1980年后,perl语言对HenrySpencer编写的库,扩展了很多新的特性。1997年开始,Philip Hazel开发出了PCRE (Perl Compatible Regular Expressions),它被PHP和HTTPD等工具采用。

正则表达式应用极其广泛,shell中处理文本的命令、各种高级编程语言都支持正则表达式。

参考

测试:

正则表达式是一个特殊的字符序列,用于判断一个字符串是否与我们所设定的字符序列是否匹配,也就是说检查一个字符串是否与某种模式匹配。

正则表达式可以包含普通或者特殊字符。绝大部分普通字符,比如 'A' , 'a' , 或者 '0' ,都是最简单的正则表达式。它们就匹配自身。你可以拼接普通字符,所以 cici 匹配字符串 'cici' .

一些字符,如 '|' or '(' ,是特殊的。特殊字符要么代表普通字符的类别,要么影响它们周围的正则表达式的解释方式。正则表达式模式字符串可能不包含空字节,但可以使用 \number 符号指定空字节,例如 '\x00' .

重复修饰符 ( * , + , ? , {m,n} , 等) 不能直接嵌套。这样避免了非贪婪后缀 ? 修饰符,和其他实现中的修饰符产生的多义性。要应用一个内层重复嵌套,可以使用括号。 比如,表达式 (?:a{6})* 匹配6个 'a' 字符重复任意次数。

分类

  • BRE 基本正则表达式,grep、sed、vi等软件支持。vim有扩展
  • ERE 扩展正则表达式,egrep(grep -E) 、sed -r等
  • PCRE 几乎所有高级语言都是PCRE的方言或变种。Python从1.6开始使用SRE正则表达式引擎,可以认为是PCRE的子集,见模块re。

基本语法

元字符

metacharacter https://www.cnblogs.com/f-ck-need-u/p/9621130.html#%E5%85%83%E5%AD%97%E7%AC%A6

. 匹配除换行符外的任意一个字符。如.
[abc] 字符集合,只能表示一个字符位置。匹配所包含的任意一个字符。如[abc]匹配plain中的'a'
[^abc] 字符集合,只能表示一个字符位置。匹配除去集合内字符的任意一个字符。如 [^abc]可以匹配plain中的'p'、'l'、'i' 或者'n'
[a-z] 字符范围,也是个集合,表示一个字符位置 ;匹配所有包含的任意字符。常用[A-Z][0-9]
[^a-z] 字符范围,也是个集合,表示一个字符位置;匹配除去集合内字符的任意一个字符。
\b 匹配单词的边界。如\bb在文本中找到单词中b开头的b字符
\B 不匹配单词的边界。如t\B 包含t的单词但是不以t结尾的t字符,例如write; \Bb不以b开头的含有b的单词,例如able
\d [0-9]匹配1位数字
\D [^0-9]匹配1位数字 
\s 匹配1位空白字符,包含换行符、制表符、空格 [\f\r\n\t\v]
\S 匹配1位非空白字符
\w 匹配[a-zA-Z0-9_]
\W 匹配\w之外的字符

范例:使用正则工具或者在线正则网站测试

abc xyz
123
中文
. #非多行和单行下,默认匹配除换行符之外任意一个字符
\w\w #匹配2个字符
\d #匹配1个数字
[abc ] #匹配abc和空格中任意一个字符
[abc\s] #匹配abc和空格字符中任意一个字符
\w\s\w #匹配c x和z换行1和3换行中
[\d\s\d] #相当于[\d\s] 匹配数字和空白字符中任意字符
\b\w\w\b #即作词首又作词尾,匹配中文
img_20250103_141447.png
img_20250103_141659.png
转义

凡是在正则表达式中有特殊意义的符号,如果想使用它的本意,请使用\转义。

反斜杠自身,得使用

\r、\n还是转义后代表回车、换行

重复

https://www.cnblogs.com/f-ck-need-u/p/9621130.html#%E5%85%83%E5%AD%97%E7%AC%A6

* 表示前面的正则表达式会重复0次或多次。如e\w*单词中e后面可以有非空白字符
+ 表示前面的正则表达式重复至少1次。如e\w+单词中e后面至少有一个非空白字符
? 表示前面的正则表达式会重复0次或1次。如e\w?单词中e后面至多有一个非空白字符
{n} 重复固定的n次。如e\w{1} 单词中e后面只能有一个非空白字符
{n,} 重复至少n次。如 e\w{1,} => e\w+  e\w{0,} => e\w* e\w{0,1} => e\w?
{n,m} 重复n到m次。如e\w{1,10}单词中e后面至少1个,至多10个非空白字符

范例:使用正则工具或者在线正则网站测试

abc xyz
123
中文
\w\w?  #匹配一个字符,后面可有可无一个字符
\w\w+  #匹配一个字符,后面至少有一个字符的字符串。可匹配abc, xyz, 123, 中文

x|y 匹配x或者y

took food wood foot 使用 w|food 或者 (w|f)ood

img_20250103_175739.png
捕获
  • (pattern)

    使用小括号指定一个子表达式,也叫分组;

    捕获后会自动分配组号 从1开始 可以改变优先级

  • \数字

    匹配对应的分组

    例: (very) \1 匹配very very,但捕获的组group是very

  • (?:pattern)

    如果仅仅为了改变优先级,就不需要捕获分组

    例: (?:w|f)ood

    'industr(?:y|ies)等价 'industry|industries'

  • (?<name>exp) 或者 (?'name'exp)

    命名分组捕获,但是可以通过name访问分组。 如果不命名的话是从1开始编号的。

    Python语法必须是(?P<name>exp)

img_20250103_180723.png
Figure 7: (pattern)分组
img_20250103_180854.png
Figure 8: (?:pattern) 不捕获分组
img_20250103_181430.png
img_20250103_181531.png
img_20250104_125323.png
Figure 11: (?<name>exp)命名分组捕获
断言

"环视"锚定,即lookaround anchor,也称为”零宽断言”,它表示匹配的是位置,不是字符。

#零宽断言
- (?=exp)  从左向右的顺序环视,又叫零宽度正预测先行断言。断言exp一定在匹配的右边出现,也就是说断言后面一定跟个exp
- (?<=exp) 从右向左的逆序环视,又叫零宽度正回顾后发断言。断言exp一定出现在匹配的左边出现,也就是说前面一定有个exp前缀

#负向零宽断言
- (?!exp) 顺序环视的取反,又叫零宽度负预测先行断言。断言exp一定不会出现在右侧,也就是说断言后面一定不是exp
- (?<!exp) 逆序环视的取反,又叫零宽度负回顾后发断言。断言exp一定不能出现在左侧,也就是说断言前面一定不能是exp
- (?#comment) 注释

关于”环视”锚定,最需要注意的一点是匹配的结果不占用任何字符,它仅仅只是锚定位置。所以断言只是个条件。

另外,无论是哪种锚定,都是从左向右匹配再做回溯的(假设允许回溯),即使是逆序环视。

范例:测试字符串为wood took foot food

# 顺序环视
f(?=oo) #匹配f,但f后面一定有oo出现

# 逆序环视,这里能逆序匹配成功,靠的是锚定括号后面的ood\ook
(?<=f)ood #匹配ood,ook前一定有t出现
(?<=t)ook #匹配ook,ook前一定有t出现

# 顺序否定环视
foo(?!d) #匹配foo,但foo后面一定不是d
\d{3}(?!\d) #匹配3位数字,断言3位数字后面一定不能是数字

# 逆序否定环视
(?<!f)ood #匹配ood,但ood的左边一定不是f

#注释
f(?=oo)(?#这个后断言不捕获)
img_20250104_133231.png
Figure 12: (?=…)从左向右顺序环视
img_20250104_134019.png
Figure 13: (?!…) 顺序环视的取反
img_20250104_133518.png
Figure 14: (?<=…) 从右向左逆序环视
img_20250104_133834.png
Figure 15: (?<!…) 逆序环视的取反

注意: 断言会不会捕获呢?也就是断言占不占分组号呢?

断言不占分组号。断言如同条件,只是要求匹配必须满⾜断言的条件。

分组和捕获是同一个意思。

使用正则表达式时,能用简单表达式,就不要复杂的表达式。

范例:

import re

a = 'wood took foot food'

findall = re.findall('f(?=oo)', a)
print(findall) # ['f', 'f']
贪婪与非贪婪

默认是贪婪模式,也就是说尽量多匹配更长的字符串。

非贪婪很简单,在重复的符号后面加上一个?问号,就尽量的少匹配了。

代码 说明 举例
*? 匹配任意次,但尽可能少重复 
+? 匹配至少1次,,但尽可能少重复 
?? 匹配0次或1次,,但尽可能少重复 
{n,}? 匹配至少n次,但尽可能少重复 
{n,m}? 匹配至少n次,至多m次,但尽可能少重复 
very very happy 使用v.*y和v.*?y

#so very very vary vary sorry vys
v.*y #贪婪,尽可能多匹配。匹配very very vary vary sorry vy
v.*?y #非贪婪, 尽可能短。匹配very、 very、 vary、 vary、vy
v.+?y #非贪婪,尽可能短。匹配very、 very 、vary、vary
v.??y #非贪婪,匹配0次或1次。匹配vy
v.{2,}?y #非贪婪,匹配2次。匹配very、 very 、vary、vary

如果要使用非贪婪,则加一个 ? ,上面的例子修改如下:

import re

a = 'java*&39android##@@python'
# 贪婪与非贪婪
re_findall = re.findall('[a-z]{4,7}?', a)
print(re_findall)

输出结果如下:

['java', 'andr', 'pyth']
引擎选项
IgnoreCase 匹配时忽略大小写。如re.I re.IGNORECASE

Singleline 单行模式,即整个测试文本被认为是单行。可以匹配所有字符,包括\n。如re.S re.DOTALL

Multiline 多行模式,^行首、$行尾。如re.M re.MULTLINE

IgnorePatternWhitespace 忽略表达式中的空白字符,如果要使用空白字符用转义,#可以用来做注释。 如re.X re.VERBOSE
  • 默认模式:将整个测试字符串看做一个一行的大字符串,.不能代表\n
  • 单行模式:将整个测试字符串看做一个一行的大字符串,.可以匹配\n
  • 多行模式:终于可以把一行大字符串用\n分割成多行,^指的是行首,$行尾,.行为不变
单行模式:
. 可以匹配所有字符,包括换行符
^ 表示整个字符串的开头,$整个字符串的结尾

多行模式
. 可以匹配除了换行符之外的字符,多行不影响.点号
^ 表示行首,$行尾,只不过这里的行是每一行

默认模式:可以看做待匹配的文本是一行,不能看做多行,.点号不能匹配换行符,^和$表示行首和行尾,而行首行尾就是整个字符的开头和结尾
单行模式:基本和默认模式一样,只是.点号终于可以匹配任意一个字符包括换行符,这时所有文本就是一个长长的只有一行的字符串。^就是这一行字符串的行首,$就是这一行的行尾。
多行模式:重新定义了行的概念,但不影响.点号的行为,^和$还是行首行尾的意思,只不过因为多行模式可以识别换行符了。“开始”指的是\n后紧接着下一个字符;“结束”指的是\n前的字符,注意最后一行结尾可以没有\n

简单讲,单行模式只影响.点号行为,多行模式重新定义行影响了^和$

注意:字符串看不见的换行符,\r\n会影响e$的测试,e$只能匹配e\n

范例

so very very sorry
vys ray

v.*y 匹配,整个看做一行,从very 到sorry,遇到\n不能突破,后面再匹配vys ray
so =very very sorry=
=vys ray=

v.*y 单行模式匹配,可以突破\n
=very very sooryvys ray=

^v.*y 单行模式匹配。 空
^v.*y 默认匹配。 空

v.*y 多行模式匹配
so =very very sorry=
=vys ray=

^v.*y 多行模式匹配
=vys ray=

^v.*y 单行和多行模式匹配
=vys ray=


^v.*y$ 单行模式匹配。 空

正则习题

匹配1个0~999之间的任意数字

1
12
885
9999
102
02
003
4d
\d                      1位数
[1-9]?\d                1-2位数
^[1-9]?\d\d?            1-3位数
^([1-9]\d\d?|\d)        1-3位数
^([1-9]\d\d?|\d)$       1-3位数的行
^([1-9]\d\d?|\d)\r?$
^([1-9]\d\d?|\d)(?!\d)  数字开头1-3位数且之后不能是数字,注意9999

IP地址

匹配合法的IP地址

192.168.1.150
0.0.0.0
255.255.255.255
17.16.52.100
172.16.0.100
400.400.999.888
001.022.003.000
257.257.255.256
IPV4 
(\d{1,3}\.){3}\d{1,3}
([0-2]?\d\d?\.){3}
((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}
((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)
(?<![0-9])(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))(?![0-9])

提取文件名

选出含有ftp的链接,且文件类型是gz或者xz的文件名

ftp://ftp.astron.com/pub/file/file-5.14.tar.gz
ftp://ftp.gmplib.org/pub/gmp-5.1.2/gmp-5.1.2.tar.xz
ftp://ftp.vim.org/pub/vim/unix/vim-7.3.tar.bz2
http://anduin.linuxfromscratch.org/sources/LFS/lfs-packages/conglomeration//iana-etc/iana-etc-2.30.tar.bz2
http://anduin.linuxfromscratch.org/sources/other/udev-lfs-205-1.tar.bz2
http://download.savannah.gnu.org/releases/libpipeline/libpipeline-1.2.4.tar.gz
http://download.savannah.gnu.org/releases/man-db/man-db-2.6.5.tar.xz
http://download.savannah.gnu.org/releases/sysvinit/sysvinit-2.88dsf.tar.bz2
http://ftp.altlinux.org/pub/people/legion/kbd/kbd-1.15.5.tar.gz
http://mirror.hust.edu.cn/gnu/autoconf/autoconf-2.69.tar.xz
http://mirror.hust.edu.cn/gnu/automake/automake-1.14.tar.xz
ftp.*\.(xz|gz)$
ftp.*/.+\.(xz|gz)$

(?<=.*ftp.*/)[^/\s]+\.(?:xz|gz)$

匹配邮箱地址

\w[\w-.]+@[\w-.]+
\w[\w-.]+@\w[\w-]*(\.[a-zA-z0-9]+)+
\w[\w-.]+@\w[\w-]*(\.\w+)+

匹配html标记

提取href中的链接url,提取文字“CiCi”

<a href='http://www.cici.com/index.html' target='_blank'>CiCi</a>
>(\w+)<  拿分组

拿href中内容
(?<=href\s*=\s*['"]?)[^\s'"<>]+  断言复杂
href\s*=\s*['"]?([^\s'"<>]+)  拿分组

匹配URL

http://www.cici.com/index.html
https://login.cici.com
file:///ect/sysconfig/network
(\w+)://[^\s'"]+

匹配二代中国身份证ID

321105700101003
321105197001010030
11210020170101054X

17位数字+1位校验码组成
前6位地址码,8位出生年月,3位数字,1位校验位(0-9或X)

地址码+出生日期码+顺序码+校验码

# 格式說明
# ┌──省份(第1-2位)
# │ ┌──城市(第3-4位)
# │ │ ┌──区县(第5-6位)
# │ │ │  ┌─出生年 月 日(第7-14位)
# │ │ │  │        ┌─顺序码(第15-17位,第17位代表性别,奇数为男性偶数为女性)
# │ │ │  │        │   ┌─校验码(第18位,校验正确性。第18位如果计算出来是10,身份号就变成19位号了,用X代替10,罗马数字X表示10)
# │ │ │  │        │   │
# xxxxxx xxxxxxxx xxx x
\d{17}[\dXx]|\d{15}

路径操作

路径操作模块

https://docs.python.org/3/library/os.path.html

# coding: utf-8
#os 模块常用函数 os.path模块
from os import path

#拼接
p1 = path.join('d:/tmp', 'conf', 'a/b') #拼接

print(p1, type(p1)) #b basename基名, d:/tmp/conf/a 路径名dirname
print(p1.split('/')) #str.split
print(path.split(p1)) #p1:str 一刀, replit, sep \ /
#: d:/tmp\conf\a/b <class 'str'>
#: ['d:', 'tmp\\conf\\a', 'b']
#: ('d:/tmp\\conf\\a', 'b')

#分隔
d, b = path.split(p1)  #切成路径名和基名
print(d, b) #: d:/tmp\conf\a b
print(path.split(d)) #: ('d:/tmp\\conf', 'a')
print(d, path.dirname(d), path.basename(d)) #: d:/tmp\conf\a d:/tmp\conf a

print(path.exists('d:/tmp')) #存在
print(path.exists('d:/tmp/test'))
print(path.splitdrive('d:/tmp/test')) #仅windows,区分驱动器
print(path.abspath('d:/tmp/test')) #对应的绝对路径
print(__file__) #当前文件自己的路径
print(path.isdir('d:/')) # d盘的/是目录吗
print(path.isfile('d:/tmp/test')) # 是文件吗
#: True
#: True
#: ('d:', '/tmp/test')
#d:\tmp\test
#<stdin>
#True
#True

#遍历,打印父目录
p = 'd:/tmp/test'
while p != path.dirname(p):
    p = path.dirname(p)
    print(p)
#d:/tmp
#d:/

3.4版本开始,建议使用pathlib模块,提供Path对象来操作。包括目录和文件。

Path类

从3.4开始python提供了pathlib模块,使用path类操作目录更加方便。

https://docs.python.org/3/library/pathlib.html

# coding: utf-8
from pathlib import Path #内部调用的os.path

#获取当前路径对象
print(Path(), Path(''), Path('.')) #会检查当前操作系统,并返回当前系统支持的操作对象  WindowsPath('.')
print(Path().absolute()) #绝对路径 WindowsPath('D:/project/pyprojs')
import os
print(os.path.abspath(''), os.path.abspath('.')) #绝对路径,相比Path类使用复杂些

#字符串显现方法,路径拼接
p = Path('/etc/', 'sysconfig', 'c/d')
print(p) #自动调整。windows 下: \etc\sysconfig\c\d
print(str(p), repr(p), bytes(p)) #p 内部调用的是repr : \etc\sysconfig\c\d WindowsPath('/etc/sysconfig/c/d') b'\\etc\\sysconfig\\c\\d'
#bytes(p) == str(p).encode()

print(Path('.', 'a', 'b/c', '', Path(''), Path('e/f'))) #WindowsPath('a/b/c/e/f')

print(Path('a') / 'b/c') # / 运算符重载 #: a\b\c
print(Path('a') / 'b/c' / '' / Path('d')) #: a\b\c\d
print('a/b' / Path('c', 'd', Path('e/f'))) #: a\b\c\d\e\f
#print('a/b' / 'c/d' / Path('e/f')) # 报错,字符不能相除
print('a/b' / ('c/d' / Path('e/f'))) #a\b\c\d\e\f
#str / Path => Path
#Path / Path => Path
#Path / str => Path
#str / str 错的

# 由哪些部分组成
p = ('a/b' / ('c/d' / Path('e/f')))
print(p.parts) #('a', 'b', 'c', 'd', 'e', 'f')

p1 = ('/a/b' / ('c/d' / Path('e/f')))
print(p1.parts) #绝对路径,多了根 ('\\', 'a', 'b', 'c', 'd', 'e', 'f')

print(p1.parent) #取父路径 \a\b\c\d\e
print(p1.parent.parent.parent.parent.parent.parent) #取父路径 \
print(p1.parent.joinpath('d1', 'd2/d3', Path('d4', 'd5'))) #路径拼接 \a\b\c\d\e\d1\d2\d3\d4\d5
l = p1.parents #惰性对象
print(list(l), p1) #父路径,是由近及远
#[WindowsPath('/a/b/c/d/e'), WindowsPath('/a/b/c/d'), WindowsPath('/a/b/c'), WindowsPath('/a/b'), WindowsPath('/a'), WindowsPath('/')] \a\b\c\d\e\f
初始化
# coding: utf-8
from pathlib import Path

p = Path() #当前目录,Path() Path('.') Path('')
p = Path('a', 'b', 'c/d') #当前目录下的 a/b/c/d
p = Path('/etc', Path('sysconfig'), 'network/ifcfg') #根下的etc目录
路径拼接

操作符 /

  • Path对象 / Path对象
  • Path对象 / Path对象
  • 字符串 / Path对象

joinpath

  • joinpath(*other) 在当前Path路径上连接多个字符串返回新路径对象
# coding: utf-8
from pathlib import Path

p1 = ('/a/b' / ('c/d' / Path('e/f')))
print(p1.parent) #取父路径 \a\b\c\d\e
print(p1.parent.joinpath('d1', 'd2/d3', Path('d4', 'd5'))) #路径拼接 \a\b\c\d\e\d1\d2\d3\d4\d5
分解

parts 属性,会返回目录各部分的元组

# coding: utf-8
from pathlib import Path

p = Path('/a/b/c/d')
print(p.parts) #最左边的是/是根目录
#: ('\\', 'a', 'b', 'c', 'd')
获取路径

str 获取路径字符串

bytes 获取路径字符串的 bytes

from pathlib import Path
p = Path('/etc')
print(str(p), bytes(p)) #: \etc b'\\etc'
父目录

parent 目录的逻辑父目录

parents 父目录惰性可迭代对象,索引0是直接的父

# coding: utf-8
from pathlib import Path
p = Path('/data/mysql/install/my.tar.gz')
print(p.parent) #: \data\mysql\install

for x in p.parents: #可迭代对象
    print(x)
#: \data\mysql\install
#: \data\mysql
#: \data
#: \


#相对路径
p = Path('data/mysql/install/my.tar.gz')  #路径系统当中,2特殊的目录
# . 引用,对当前目录自身的引用
# .. 对当前目录的父目录的引用
print(p.parent) #data\mysql\install

for x in p.parents: #可迭代对象
    print(x)
#data\mysql\install
#data\mysql
#data
#.

print(list(p.parents))
#[WindowsPath('data/mysql/install'), WindowsPath('data/mysql'), WindowsPath('data'), WindowsPath('.')]
目录组成部分

name、stem、suffix、suffixes、with_suffix(suffix)、with_name(name)

  • name 目录的最后一个部分
  • stem 目录最后一个部分,没有后缀
  • suffix 目录最后一个部分的扩展名
  • name = stem + suffix

suffixes 返回多个扩展名列表

  • with_suffix(suffix) 有扩展名则替换,无则补充扩展名
  • with_name(name)替换目录最后一个部分并返回一个新的路径
# coding: utf-8
from pathlib import Path
p = Path('/data/mysql/my.tar.gz')
print(p.exists()) #: False

print(p.name, p.parent) #路径成分 基名 父目录 : my.tar.gz \data\mysql
print(p.suffix, p.suffixes, p.stem) #文件扩展名 : .gz ['.tar', '.gz'] my.tar

print(p.with_name('config')) #换基名 : \data\mysql\config
print(p.parent.with_name('config')) #换基名 : \data\config
print(p.parent.parent / 'redis') #换基名: \data\redis
print(p.with_suffix('.xz')) #换扩展名 : \data\mysql\my.tar.xz
全局方法
  • cwd() 返回当前工作目录
  • home() 返回当前家目录
# coding: utf-8
from pathlib import Path

p = 'd:/tmp'
print(Path(p).cwd(), Path.cwd())
# 当前工作路径不会随着你路径的变化而变化,相当于常量,通过任何路径都可以方便调用到当前工作路径
#家目录也是一样,相当于常量
print(Path('abc').home(), Path.home())
判断方法
  • exists() 目录或文件是否存在
  • is_dir() 是否是目录,目录存在返回True
  • is_file() 是否是普通文件,文件存在返回True
  • is_symlink() 是否是软链接
  • is_socket() 是否是socket文件
  • is_block_device() 是否是块设备
  • is_char_device() 是否是字符设备
  • is_absolute() 是否是绝对路径

注意: 文件只有存在 ,才能知道它是什么类型文件

范例:

# coding: utf-8
from pathlib import Path

p1 = Path('/dev')
print((p1 / 'bus').is_dir())
print((p1 / 'console').is_char_device())
print((p1 / 'cdrom').is_symlink())
print((p1 / 'cdrom').is_file())
print((p1 / 'abcdea').is_file()) #不知道是谁,False. 内部调用了exits()目录或文件是否存在
print(p1.is_absolute())       
绝对路径
  • resolve() 非windows,返回一个新的路径,这个新路径就是当前Path对象的绝对路径。如果是软链接则直接被解析
  • absolute() 获取绝对路径

范例

# coding: utf-8

from pathlib import Path

p1 = Path('/dev')
print((p1 / 'cdrom').is_symlink())
print((p1 / 'cdrom').absolute())
print((p1 / 'cdrom').resolve()) #如果是软链接返回真实路径
#True
#/dev/cdrom
#/dev/sr0   
其它操作
  • rmdir() 删除空目录。没有提供判断目录为的方法
  • touch(mode=0o666, exist_ok=True) 创建一个文件
  • as_uri() 将路径返回成URI,例如'file:///etc/passwd'
  • mkdir(mode=0o777, parents=False, exist_ok=False)

    parents 是否创建父目录,True等同于mkdir -p。False时,父目录不存在,则抛出FileExistsError被忽略

    exist_ok 在3.5版本加入。False时,路径存在,抛出FileExistsError; True时 FileExistsError被忽略

  • iterdir() 迭代当前目录,不递归
# coding: utf-8
from pathlib import Path

p = Path('d:/tmp/mydir')
p.mkdir(parents=True, exist_ok=True)

print(p.is_file()) #返回False
print(p.exists()) #返回True
print(list(p.absolute().iterdir())) #返回[]  遍历目录,iterdir方法遍历,返回生成器对象,遍历目录不递归
(p / 'd1').mkdir(exist_ok=True) #创建目录
(p / 'c1/c2/c3').mkdir(parents=True, exist_ok=True) #创建目录,mkdir -p 
(p / 'c1/a.py').touch() #创建文件
(p / '../a.py').touch() #创建文件

#遍历父路径
for x in (p / 'd1').parent.iterdir():
    print(x)
#: d:\tmp\mydir\c1
#: d:\tmp\mydir\d1

范例:d:/tmp下的所有文件,不递归,判断是否为空目录,判断文件类型

# coding: utf-8
from pathlib import Path

p = Path('d:/tmp/c1/c2')

#可以用parents操作,也可以用parent操作
#print(list(p.parents))
#for x in p.parents[len(p.parents)-2].iterdir(): #不支持负索引
for x in p.parent.parent.iterdir():
    print(x, type(x))
    if x.is_dir():
        print('dir', x)
#        flag = False #不为空
#        for z in x.iterdir():
#            flag = True
#            break
#        print('Not empty' if flag else 'Empty')
        print('Not empty' if next(x.iterdir(), False) else 'Empty')  #next(迭代对象, False) 如果没有返回False
    elif x.is_file():
        print('file', x)
    else:
        print('other', x)
  • stat 相当于stat -L命令,跟踪软链接
  • lstat 使用方法同stat(),但如果是符号链接,则显示符号链接本身的文件信息。
# coding: utf-8

from pathlib import Path

p1 = Path('/dev/')
print((p1 / 'cdrom').is_symlink())

print((p1 / 'cdrom'))
print((p1 / 'cdrom').stat()) #会跟踪软链接,是真实路径的状态                                                                                                                                                              
print((p1 / 'cdrom').resolve())
print((p1 / 'cdrom').resolve().stat())
#True
#/dev/cdrom
#os.stat_result(st_mode=25008, st_ino=160, st_dev=5, st_nlink=1, st_uid=0, st_gid=24, st_size=0, st_atime=1735647858, st_mtime=1735647858, st_ctime=1735647858)
#/dev/sr0
#os.stat_result(st_mode=25008, st_ino=160, st_dev=5, st_nlink=1, st_uid=0, st_gid=24, st_size=0, st_atime=1735647858, st_mtime=1735647858, st_ctime=1735647858)
通配符
  • glob(pattern) 通配给定的模式,返回生成器对象
  • rglob(pattern) 通配给定的模式,递归目录,返回生成器对象
  • ? 代表一个字符
  • * 表示任意个字符
  • [abc]或[a-z]表示一个字符
# coding: utf-8
from pathlib import Path

#通配符,不是正则表达式
p1 = Path('d:/tmp')

print(p1.glob('*')) #当前目录下所有文件,返回可迭代对象: <generator object Path.glob at 0x0000027452864120>
print(p1.glob('*.py')) #当前目录下所有.py文件
print(p1.glob('*/*/*.py')) #当前目录下子目录下所有.py文件
print(p1.glob('**/*.py')) #当前目录下任意目录下所有.py文件
#? 一个
p1.glob('**/[a-z]*.??') #[a-z]*.?? 代表a到z任意一个字符后面跟任何字符,再接个.??
p1.rglob('**/[a-z]*.??') #递归glob,层级就不用写了
文件操作

Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)

使用方法类似内建函数open。返回一个文件对象

3.5增加了新函数

Path.read_bytes() 以 'rb' 读取路径对应文件,并返回二进制流。看源码

Path.read_text(encoding=None, errors=None) 以'rt'方式读取路径对应文件,返回文本

Path.write_bytes(date) 以'wb'方式写入数据到路径对应文本

Path.write_text(date, encoding=None, errors=None) 以'wt'方式写入字符串到路径对应文件

open()函数文件操作

# coding: utf-8
from pathlib import Path

p = Path('d:/tmp/a.py')

with open(p, 'w+') as f: #open文件参数可以是 Path对象、字符串、文件对象
    f.write('abc')
    f.seek(0)
    print(f.read())

Path类中的文件操作

# coding: utf-8
from pathlib import Path

p = Path('d:/tmp/a.py')

p.write_text('abc') #内部用了open(mode='w')
p.write_bytes(b'abc') #内部用了open(mode='wb')
print(p.read_text()) #open(mode='rt')
print(p.read_bytes()) #open(mode='rb')
#: abc
#: b'abc'

with p.open() as f: #self.open(mode, ...) open(file, mode)
    print(f.read())

os模块

操作系统平台

  • os.name windows是nt,linux是posix
  • os.uname() *nix支持
  • sys.platform windows显示win32, linux显示linux

os.listdir('d:/tmp') 返回指定目录内容列表,不递归

os也有open、read、write等方法,但是太底层,建议使用内建函数open、read、write,使用方式相似

os.stat(path, *, dir_fd=None, follow_symlinks=True) 本质上调用linux系统的stat

# coding: utf-8
import os
import sys

print(os.name, sys.platform)
#: nt win32

print(os.listdir('d:/tmp')) #返回字符串列表,就不递归
print(os.path.isdir('d:/tmp'))
print(os.stat('d:/tmp/test'))  #mode=33188
print(os.lstat('d:/tmp/test'))
print(oct(31888)) #八进制 0o76220 权限为rwww

os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)

os.chmod('test', 0o777)

os.chown(path, uid, git) 改变文件的属性、属组,但需要足够的权限

shutil模块

文件拷贝:使用打开2文件对象,源文件读取内容,写入目标文件中来完成拷贝过程。但是这样丢失stat数据信息(权限等),因为根本没有复制这些信息过去。

目录复制又怎么办呢?

Python提供了一个方便的库shutil(高级文件操作)

copy 复制

copyfileobj(fsrc, fdst[, length])

  • 文件对象的复制,fsrc和fdst是open打开的文件对象,复制内容。fdst要求可写。
  • length指定了表示buffer的大小
  • 阅读copyfileobj函数。

copyfile(src, dst, *, follow_symlinks=True)

  • 复制文件内容,不含元数据。src、dst为文件的路径字符串。
  • 本质上调用的就是copyfileobj,所以不带元数据二进制内容复制。

copymode(src, dst, *, follow_symlinks=True)

  • 仅复制权限

copystat(src, dst, *, follow_symlinks=True)

  • 复制文件权限和元数据

copy(src, dst, *, follow_symlinks=True)

  • 内部用了copyfile() 和 copymode()

copy2(src, dst, *, follow_symlinks=True)

  • 内部用了copyfile() 和 copystat()
# coding: utf-8
from pathlib import Path
import shutil

src = Path('d:/tmp/test')
dst = Path('d:/tmp/t.txt')

with open(src, 'rb') as fsrc:
    with open(dst, 'wb') as fdst:
        #从src读取,写入dst
        shutil.copyfileobj(fsrc, fdst)

shutil.copyfile(src,dst) #实际上是上面的实现。
shutil.copymode(src,dst) #复制权限
shutil.copystat(src,dst) #stat 全面元数据,包括mode
shutil.copy(src,dst) #copyfile copymode
shutil.copy2(src,dst) #copyfile copystat

copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, ignore_dangling_symlinks=False)

  • 递归复制目录。默认使用copy2,也就是带更多的元数据复制。
  • src, dst必须是目录,src必须存在,dst必须不存在。
  • ignore = func ,提供一个callable(src, names) -> ignored_names。 提供一个函数,它会被调用。 src是源目录,names是os.listdir(src)的结果,就是列出src中的文件名,返回值是要被过滤的文件的set类型数据。
# coding: utf-8
from pathlib import Path
import shutil

src = Path('d:/tmp/d1') #递归copy d1 子文件
dst = Path('d:/tmp/dx')

#shutil.copytree(src, dst) #源目录递归copy到dst,要求目标路径不能存在

def fn(src, files): #src, name表示src所有直接的文件
#    print(src, files) #d:/tmp/d1 ['b.txt', 'd2']
#    ignore_names = set() #集合解析式
#    for name in names: #不要copy .py文件 #文件名字符串列表
#        print(name, type(name))
#        if name.endswith('.py') or name.endswith('.txt'):
#            ignore_names.add(name)
#    return ignore_names #返回可迭代对象
    #return { name for name in names if name.endswith('.py') or name.endswith('.txt')}
    return set(filter(lambda name: name.endswith('.py') or name.endswith('.txt'), names)) #可迭代对象。filter返回一个迭代器,用完就没了。所以这里用set() 变为可迭代对象。

shutil.rmtree('d:/tmp/dx', ignore_errors=True) #删除目录
shutil.copytree(src, dst, ignore=fn) #源目录递归copy到dst,要求目标路径不能存在

rm 删除

shutil.rmtree(path, ignore_errors=False, onerror=None)

递归删除。如同rm -fr一样危险,慎用。

shutil.rmtree('d:/tmp') #类似rm -fr

move 移动

move(src, dst, copy_function=copy)

递归移动文件、目录到目标,返回目标。

本身使用的是 os.rename 方法。

如果不支持rename,如果是目录则copytree再删除源目录

默认使用copy2方法

shutil.move('d:/a', 'd:/aaa')
os.rename('d:/t.txt', 'd:/tmp/t')
os.rename('test3', 'd:/tmp/taa')

shutil还有打包功能。生成tar并压缩。支持zip, gz, bz, xz。

练习

  1. 指定一个源文件,实现copy到目标目录

    例如把/tmp/test.txt拷贝到/tmp/test1.txt

  2. 单词统计

    有一个文件(https://docs.python.org/zh-cn/3.13/library/os.path.html),对其进行单词统计,不区分大小写,并显示单词重复最多的10个单词

  3. 单词统计进阶

    在上一题的基础上,要求用户可以排除一些单词的统计,例如a, the, of等不应该出现在具有实际意义的统计中,应当忽略。要求全部代码使用函数封装,并调用完成

  4. 配置文件转换

    有一个配置文件test.ini内容如下,将其转换成json格式文件

    [DEFAULT]
    a = test
    
    [mysql]
    default-character-set=utf8
    a = 100
    
    [mysqld]
    datadir =/dbserver/data
    port = 33060
    character-set-server=utf8
    sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
    
  5. 复制目录

    选择一个已存在的目录作为当前工作目录,在其下创建a/b/c/d这样的子目录结构并在这些子目录的不同层级生成50个普通文件,要求文件名由随机4个小写字母构成。

    将a目录下所有内容复制到当前工作目录dst目录下,要求复制的普通文件的文件名必须是x, y, z开头。

    举例,假设工作目录/tmp,构建的目录结构是/tmp/a/b/c/d。在a, b, c, d目录中放入随机生成的文件,这些文件的名称也是随机生成的。最终把a目录下所有的目录也就是b, c, d目录,和文件名开头是x, y, z开头的文件。

指定一个源文件,实现copy到目标目录

例如把/tmp/test.txt拷贝到/tmp/test1.txt

# coding: utf-8
from os import path
import shutil

base = 'd:/tmp'
src = 'test.txt'
dst = 'test1.txt'

src = path.join(base, src)
dst = path.join(base,dst)

with open(src, 'w', encoding='utf-8') as f:
    f.write('\n'.join(['123','abc', '你好']))

def copy(src, dst):
    length = 16 * 1024
    with open(src, 'rb', ) as f1:
        with open(dst, 'wb') as f2:
            while True:
                buffer = f1.read(length)
                if not buffer:
                    break
                f2.write(buffer)

copy(src, dst) 
#shutil.copyfile(src,dst) #shutil.copyfileobj(fsrc,fdst)                
#shutil.copy(src,dst)
#shutil.copy2(src,dst)
复制目录

选择一个已存在的目录作为当前工作目录,在其下创建a/b/c/d这样的子目录结构并在这些子目录的不同层级生成50个普通文件,要求文件名由随机4个小写字母构成。

将a目录下所有内容复制到当前工作目录dst目录下,要求复制的普通文件的文件名必须是x, y, z开头。

举例,假设工作目录/tmp,构建的目录结构是/tmp/a/b/c/d。在a, b, c, d目录中放入随机生成的文件,这些文件的名称也是随机生成的。最终把a目录下所有的目录也就是b, c, d目录,和文件名开头是x, y, z开头的文件。

# coding: utf-8
#准备要用的目录

import shutil
from pathlib import Path

base = Path('d:/tmp')
sub  = Path('a/b/c/d')

#(base / sub).mkdir(parents=True, exist_ok=True) #路径,需要父路径。
#解一下parentes
#print(*sub.parents)  #: a\b\c a\b a .  相对路径有个点,要去掉
print(list(sub.parents)[:-1]) #: [WindowsPath('a/b/c'), WindowsPath('a/b'), WindowsPath('a')]

dirs = [sub] + list(sub.parents)[:-1]
print(dirs) #: [WindowsPath('a/b/c/d'), WindowsPath('a/b/c'), WindowsPath('a/b'), WindowsPath('a')]
# coding: utf-8
#随机文件

import shutil
from pathlib import Path
from string import ascii_lowercase
import random

base = Path('d:/tmp/mydir')
sub  = Path('a/b/c/d')

dirs = [sub] + list(sub.parents)[:-1]
#: [WindowsPath('a/b/c/d'), WindowsPath('a/b/c'), WindowsPath('a/b'), WindowsPath('a')]
#删除目录
if base.exists():
    shutil.rmtree(str(base))
#创建目录
(base / sub).mkdir(parents=True, exist_ok=True) #路径,需要父路径。

for i in range(20):
    name = "".join(random.choices(ascii_lowercase, k=4))
    #print(name)
    (base / random.choice(dirs) / name).touch()

#遍历所有文件
print(*list(base.rglob('*')), sep='\n')
# coding: utf-8
#复制目录

import shutil
from pathlib import Path
from string import ascii_lowercase
import random

base = Path('d:/tmp/mydir')
sub  = Path('a/b/c/d')

dirs = [sub] + list(sub.parents)[:-1]
#: [WindowsPath('a/b/c/d'), WindowsPath('a/b/c'), WindowsPath('a/b'), WindowsPath('a')]
#删除目录
if base.exists():
    shutil.rmtree(str(base))
#创建目录
(base / sub).mkdir(parents=True, exist_ok=True) #路径,需要父路径。

for i in range(20):
    name = "".join(random.choices(ascii_lowercase, k=4))
    #print(name)
    (base / random.choice(dirs) / name).touch()

#copy
headers = set('xyz')
def ignore_files(src, names):
    # return {name for name in names
    #         if name[0] not in headers and Path(src, name).is_file()}
    return set(filter(lambda name: name[0] not in headers and Path(src, name).is_file(), names))

shutil.copytree(str(base / 'a'), str(base / 'dst'), ignore=ignore_files)

#遍历所有文件
print(*(base / 'dst').rglob('*'), sep='\n')
密码强度
要求密码必须由10-15位,指定字符组成:
十进制数字
大写字母
小写字母
下划线
要求四种类型的字符都要出现才算合法的强密码

例如:Aatb32_67mnq,其中包含大写字母、小写字母、数字和下划线,是合格的强密码

^[a-zA-Z0-9_]{10,15}$
但是还是没有解决类似于11111111112这种密码问题,如果解决?

较好的思路:
1. 给一个列表,打标记[0,0,0,0]
2. 首先长度判断
3. 遍历密码原文字符串的字符,判断这个字符是哪一个分类,如果不是上面4类,直接返回非常密码。
   如果每一个字符都合格,属于哪个分类就在该分类上标记为1
4. 如果4类都有,则列表为[1,1,1,1],哪一种字符没出现过,就是0

这样既可以判断密码强弱,还知道了密码的强度等级。
配置文件转换

有一个配置文件test.ini内容如下,将其转换成json格式文件

[DEFAULT]
a = test

[mysql]
default-character-set=utf8
a = 100

[mysqld]
datadir =/dbserver/data
port = 33060
character-set-server=utf8
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

代码

import json
from configparser import ConfigParser

src = 'test.ini'
dst = 'test.json'

cfg = ConfigParser()
cfg.read(src)

d = {}

for section in cfg.sections(): #不带缺省值
    print(section, type(section))
    print(cfg.items(section))
    # ['mysql', 'mysqld']
    # mysql <class 'str'>
    # [('a', '100'), ('default-character-set', 'utf8')]
    # mysqld <class 'str'>
    # [('a', 'test'), ('datadir', '/dbserver/data'), ('port', '33060'), ('character-set-server', 'utf8'), ('sql_mode', 'NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES')]

    d[section] = dict(cfg.items(section))

print(d)
with open(dst, 'w', encoding='utf-8') as f:
    json.dump(d, f)

查看新文件内容

(.venv) PS D:\project\pyproj> python -m json.tool .\test.json
{
    "mysql": {
        "a": "100",
        "default-character-set": "utf8"
    },
    "mysqld": {
        "a": "test",
        "datadir": "/dbserver/data",
        "port": "33060",
        "character-set-server": "utf8",
        "sql_mode": "NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES"
    }
}
单词统计

有一个文件(https://docs.python.org/zh-cn/3.13/library/os.path.html),对其进行单词统计,不区分大小写,并显示单词重复最多的10个单词

from collections import defaultdict
import re

filename = 'sample.txt'

d = {}
#d = defaultdict(lambda :0)

#处理os.path.commonpath(['/usr/lib'
#line = "os.path.commonpath(['/usr/lib'"
def make_key1(line:str):
    #line = line.lower()
    pattern = r"""[~`!@#$%^&*()-_=+|\{}\[\]:;"'<>?/,.\s]+"""
    ret = re.split(pattern, line)
    return ret

def make_key2(line:str):
    #line = line.lower()
    chars = set(r"""~`!@#$%^&*()-_=+|\{}[]:;"'<>?/,.""")
    ret = []
    for c in line:
        if c in chars:
            ret.append(' ')
        else:
            ret.append(c)
    return ''.join(ret).split()

with open(filename, encoding='utf-8') as f:
    for line in f: #逐行处理
        #不区分大小写
        # line = line.lower()
        # words = line.split()
        # for word in words:
        # for word in map(lambda x:x.lower(), make_key1(line)):
        for word in map(str.lower, make_key1(line)):
            # print(word)
            #d[word] += 1
            d[word] = d.get(word, 0) + 1

print(sorted(d.items(), key=lambda item:item[1], reverse=True) )

# for k in d.keys():
#     if k.find('path') > -1:
#         print(k)
print(*filter(lambda k: k.find('path') > -1, d.keys()), sep='\n')


def make_key3(line:str):
    #line = line.lower()
    chars = set("""~`!@#$%^&*()-_=+|\{}[]:;"'<>?/,.\n\r\t""")
    ret = []
    start = 0
    length  = len(line)
    for i,c in enumerate(line):
        if c in chars:
            if start == i:
                start = i + 1
                continue
            ret.append(line[start:i])
            start = i + 1 #假定下一个字符是OK的
    else:
        if start < length:
            ret.append(line[start:])
    return ret
line = "os.path.commonpath(['/usr/lib'"
print(make_key3(line))


def make_key4(line:str):
    #line = line.lower()
    chars = set("""~`!@#$%^&*()-_=+|\{}[]:;"'<>?/,.\n\r\t""")
    # ret = []
    start = 0
    length  = len(line)
    for i,c in enumerate(line):
        if c in chars:
            if start == i:
                start = i + 1
                continue
            # ret.append(line[start:i])
            yield line[start:i]
            start = i + 1 #假定下一个字符
    else:
        if start < length:
            # ret.append(line[start:])
            yield line[start:]

line = "os.path.commonpath(['/usr/lib'"
print(*make_key4(line))
from collections import defaultdict
import re

filename = 'sample.txt'


#d = defaultdict(lambda :0)

#处理os.path.commonpath(['/usr/lib'
#line = "os.path.commonpath(['/usr/lib'"
regex = re.compile(r"""[~`!@#$%^&*() -_=+|\{}\[\]:;"'<>?/,.\s]+""")
def make_key1(line:str):
    #line = line.lower()
    ret = regex.split(line)
    return ret

chars = set("""~`!@#$%^&*()-_=+|\{}[]:;"'<>?/,. \n\r\t""")

def make_key2(line:str):
    #line = line.lower()
    ret = []
    for c in line:
        if c in chars:
            ret.append(' ')
        else:
            ret.append(c)
    return ''.join(ret).split()

def make_key3(line:str):
    #line = line.lower()
    ret = []
    start = 0
    length  = len(line)
    for i,c in enumerate(line):
        if c in chars:
            if start == i:
                start = i + 1
                continue
            ret.append(line[start:i])
            start = i + 1 #假定下一个字符是OK的
    else:
        if start < length:
            ret.append(line[start:])
    return ret

def make_key4(line:str):
    start = 0
    length  = len(line)
    for i,c in enumerate(line):
        if c in chars:
            if start == i:
                start = i + 1
                continue
            yield line[start:i]
            start = i + 1 #假定下一个字符
    else:
        if start < length:
            yield line[start:]

def wordcount(filename, enconding='utf-8', ignore=None):
    d = {}
    with open(filename, encoding='utf-8') as f:
        for line in f: #逐行处理
            for word in map(str.lower, make_key4(line)):
                d[word] = d.get(word, 0) + 1
    return d

def topN(d:dict, n:int=10):
    return sorted(d.items(), key=lambda item:item[1], reverse=True)[:n]

t = topN(wordcount(filename))
print(t)
单词统计进阶

在上一题的基础上,要求用户可以排除一些单词的统计,例如a, the, of等不应该出现在具有实际意义的统计中,应当忽略。要求全部代码使用函数封装,并调用完成

import re

filename = 'sample.txt'

#处理os.path.commonpath(['/usr/lib'
#line = "os.path.commonpath(['/usr/lib'"
regex = re.compile(r"""[~`!@#$%^&*()-_=+|\{}\[\]:;"'<>?/,.\s]+""")
def make_key1(line:str):
    #line = line.lower()
    ret = regex.split(line)
    return ret

chars = set("""~`!@#$%^&*()-_=+|\{}[]:;"'<>?/,. \n\r\t""")

def make_key2(line:str):
    #line = line.lower()
    ret = []
    for c in line:
        if c in chars:
            ret.append(' ')
        else:
            ret.append(c)
    return ''.join(ret).split()

def make_key3(line:str):
    #line = line.lower()
    ret = []
    start = 0
    length  = len(line)
    for i,c in enumerate(line):
        if c in chars:
            if start == i:
                start = i + 1
                continue
            ret.append(line[start:i])
            start = i + 1 #假定下一个字符是OK的
    else:
        if start < length:
            ret.append(line[start:])
    return ret

def make_key4(line:str):
    start = 0
    length  = len(line)
    for i,c in enumerate(line):
        if c in chars:
            if start == i:
                start = i + 1
                continue
            yield line[start:i]
            start = i + 1 #假定下一个字符
    else:
        if start < length:
            yield line[start:]

def wordcount(filename, enconding='utf-8', ignore=None):
    d = {}
    if ignore is None: #stopwords
        ignore = set()
    with open(filename, encoding='utf-8') as f:
        for line in f: #逐行处理
            for word in map(str.lower, make_key4(line)):
                if word not in ignore:
                    d[word] = d.get(word, 0) + 1
    return d

def topN(d:dict, n:int=10):
    # return sorted(d.items(), key=lambda item:item[1], reverse=True)[:n]
    yield  from sorted(d.items(), key=lambda item:item[1], reverse=True)[:n]
t = topN(wordcount(filename, ignore={'the', 'is', 'a'}))
print(*t)

Python的正则表达式

Python使用re模块提供了正则表达式处理的能力

常量

多行模式
- re.M
- re.MULTILINE

单行模式
- re.S
- re.DOTALL

忽略大小写
- re.I
- re.IGNORECASE

忽略表达式中空白字符
- re.X
- re.VERBOSE

方法

编译
re.compile(pattern, flags=0)
  • 设定flags,编译模式,返回正则表达式对象regex
  • pattern就是正则表达式字符串,flags是选项,指代当前工作模式。正则表达式需要被编译,为了提高效率,这些编译后的结果被保存,下次使用同样的pattern的时候,就不需要再次编译
  • re的其他方法为了提高效率都调用了编译方法,就是为了提速

范例

import re

#预编译
re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
单次匹配

match

re.match(pattern, string, flags=0)
regex.match(string[, pos[, endpos]])
  • match匹配从字符串的开头匹配,索引为0的位置,regex对象match方法可以重设定开始位置和结束位置,返回match对象

search

re.search(pattern, string, flags=0)
regex.search(string[, pos[, endpos]])
  • search匹配从头搜索直到第一个匹配,regex对象search方法可以重设定开始位置和结束位置,返回match对象

fullmatch

re.fullmatch(pattern, string, flags=0)
regex.fullmatch(string[, pos[, endpos]])
  • 全长完全匹配,整个字符串和正则表达式匹配

范例:加强对match的理解

import re

s = """bottle\nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
# (0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, 'b')(8, 'a')(9, 'g')
# (10, '\n')(11, 'b')(12, 'i')(13, 'g')(14, '\n')(15, 'a')(16, 'p')(17, 'p')(18, 'l')(19, 'e')

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象

#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
r = re.match('a', s)
print(r) #None
r = re.match('^a', s)
print(r) #None 默认模式,一整行,没有a开头的
r = re.match('^a', s, re.M)
print(r) #None 从索引0开始匹配,所以匹配不上
r = re.match('a', s, re.M)
print(r) #None
#即便是 MULTILINE 多行模式, re.match() 也只匹配字符串的开始位置,而不匹配每行开始

r = re.match('b.+', s, re.S)
#匹配不上返回None;
#匹配上,得到match类型实例,span (start, end), math=xxx 匹配
print(type(r), r) #<class 're.Match'> <re.Match object; span=[0, 20), match='bottle\nbag\nbig\napple'>
print(r.start(), r.end()) #0 20 。匹配从头至尾
print(s[r.start():r.end()].encode()) #b'bottle\nbag\nbig\napple'
print(r.string.encode()) #原字符串内容,即s本身内容 b'bottle\nbag\nbig\napple'

r = re.match('b.+', s, re.M) #多行模式匹配,但match只做单次匹配,所以只拿到bottle
print(type(r), r) #<class 're.Match'> <re.Match object; span=(0, 6), match='bottle'>

#match内部用了编译,不需要反复做编译,那么我们可以先编译再match
#如果需要多次使用这个正则表达式的话,使用 re.compile() 和保存这个正则对象以便复用,可以让程序更加高效。
regex = re.compile('b.+', re.M)
print(regex.match(s)) # 单次匹配 <re.Match object; span=(0, 6), match='bottle'>
print(regex.match(s, pos=1)) #pos指定字符串索引位置。 从s的索引1位置即o开始匹配,没匹配到,返回None
print(regex.match(s, 7)) #从索引7位置开始匹配,能匹配到 <re.Match object; span=(7, 10), match='bag'>

regex = re.compile('b')
m = regex.match(s)
print(m) #默认模式,从索引0开始匹配,<re.Match object; span=(0, 1), match='b'>
regex = re.compile('^b')
print(regex.match(s)) #默认模式,从索引0开始匹配,<re.Match object; span=(0, 1), match='b'>
regex = re.compile('^a')
print(regex.match(s)) #默认模式,从索引0开始匹配,None
regex = re.compile('^a', re.M)
print(regex.match(s)) #多行模式,从索引0开始匹配,None
regex = re.compile('^a', re.M)
print(regex.match(s, 8)) #多行模式,从索引8开始匹配,但要求是行首,None
regex = re.compile('^a', re.M)
print(regex.match(s, 15)) #多行模式,从索引15开始匹配,是行首,<re.Match object; span=(15, 16), match='a'>
regex = re.compile('a', re.M)
print(regex.match(s, 8)) #多行模式,从索引8开始匹配,<re.Match object; span=(8, 9), match='a'>
regex = re.compile('^a',)
print(regex.match(s, 15)) #默认模式,从索引15开始匹配,但a不是行首,None

范例:search匹配

import re

s = """bottle\nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
# (0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, 'b')(8, 'a')(9, 'g')
# (10, '\n')(11, 'b')(12, 'i')(13, 'g')(14, '\n')(15, 'a')(16, 'p')(17, 'p')(18, 'l')(19, 'e')

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
r = re.search('b', s)
print(type(r), r) #<class 're.Match'> <re.Match object; span=(0, 1), match='b'>
r = re.search('a', s)
print(type(r), r) #<class 're.Match'> <re.Match object; span=(8, 9), match='a'>
r = re.search('a', s, re.M)
print(type(r), r) #<class 're.Match'> <re.Match object; span=(8, 9), match='a'>
r = re.search('^a', s, re.M)
print(type(r), r) #<class 're.Match'> <re.Match object; span=(15, 16), match='a'>
r = re.search('^a', s)
print(type(r), r) #默认模式,整个算一整行。<class 'NoneType'> None

#如果需要多次使用这个正则表达式的话,使用 re.compile() 和保存这个正则对象以便复用,可以让程序更加高效。
regex = re.compile('^a')
r = regex.search(s)
print(r) #None
print(regex.search(s, 15)) #默认模式,整个算一整行。不是a开头。并不是多行模式
regex = re.compile('^a', re.M)
print(regex.search(s, 15)) #多行模式,a是这一行的行首 <re.Match object; span=(15, 16), match='a'>

范例:fullmatch匹配

import re

s = """bottle\nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
# (0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, 'b')(8, 'a')(9, 'g')
# (10, '\n')(11, 'b')(12, 'i')(13, 'g')(14, '\n')(15, 'a')(16, 'p')(17, 'p')(18, 'l')(19, 'e')

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
#fullmatch 是单次匹配,要求指定区间全部匹配
r = re.fullmatch('b.+', s)
print(type(r), r) #<class 'NoneType'> None
r = re.fullmatch('bag', s)
print(type(r), r) #<class 'NoneType'> None
r = re.fullmatch('b.+', s, re.S)
print(type(r), r) #<class 're.Match'> <re.Match object; span=(0, 20), match='bottle\nbag\nbig\napple'>

#如果需要多次使用这个正则表达式的话,使用 re.compile() 和保存这个正则对象以便复用,可以让程序更加高效。
r = re.compile('bag')
print(r.fullmatch(s, 7)) #None
print(r.fullmatch(s, 7, 11)) #None  区间前包后不包 [7, 11)
print(r.fullmatch(s, 7, 10)) #<re.Match object; span=(7, 10), match='bag'>
print(r.fullmatch(s, 7, 9)) #None
r = re.compile('^bag')
print(r.fullmatch(s, 7, 10)) #默认模式,bag不是首行。None
r = re.compile('^bag', re.M)
print(r.fullmatch(s, 7, 10)) #多行模式,bag是行首,并且在指定区间内匹配<re.Match object; span=(7, 10), match='bag'>
全文搜索

findall

re.findall(pattern,string,flags=0)
regex.findall(string[, pos[, endpos]])
  • 对整个字符串,从左至右匹配,返回所有匹配项的列表,里面是str

finditer

re.finditer(pattern,string,flags=0)
regex.finditer(string[, pos[, endpos]])
  • 对整个字符串,从左至右匹配,返回所有匹配项,返回迭代器
  • 注意每次迭代返回的是match对象

范例:findall匹配

import re

s = """bottle\nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
# (0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, 'b')(8, 'a')(9, 'g')
# (10, '\n')(11, 'b')(12, 'i')(13, 'g')(14, '\n')(15, 'a')(16, 'p')(17, 'p')(18, 'l')(19, 'e')

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#findall 返回列表,返回匹配的字符串。跟平常理解的正则是一样的
r = re.findall('b', s)
print(type(r), r) #<class 'list'> ['b', 'b', 'b']
r = re.findall(r'b\w+', s)
print(type(r), r) #<class 'list'> ['bottle', 'bag', 'big']

#如果需要多次使用这个正则表达式的话,使用 re.compile() 和保存这个正则对象以便复用,可以让程序更加高效。
regex = re.compile(r'b\w+')
print(regex.findall(s, 1)) #['bag', 'big']
regex = re.compile(r'^b\w+')
print(regex.findall(s, 0)) #['bottle']

#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
#fullmatch 是单次匹配,要求指定区间全部匹配

范例:finditer匹配

import re

s = """bottle\nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
# (0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, 'b')(8, 'a')(9, 'g')
# (10, '\n')(11, 'b')(12, 'i')(13, 'g')(14, '\n')(15, 'a')(16, 'p')(17, 'p')(18, 'l')(19, 'e')

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#findall 返回列表,返回匹配的字符串。跟平常理解的正则是一样的
#finditer 返回一个迭代器,每一个元素都是match实例
r = re.finditer(r'b\w+', s)
print(type(r), r) #<class 'callable_iterator'> <callable_iterator object at 0x00000249B4052B30>
for x in r:
    print(type(x), x)
#<class 're.Match'> <re.Match object; span=(0, 6), match='bottle'>
#<class 're.Match'> <re.Match object; span=(7, 10), match='bag'>
#<class 're.Match'> <re.Match object; span=(11, 14), match='big'>

r = re.finditer(r'^b', s)
print(type(r), r) #<class 'callable_iterator'> <callable_iterator object at 0x00000179427D27A0>
for x in r:
    print(type(x), x)
#<class 're.Match'> <re.Match object; span=(0, 1), match='b'>

r = re.finditer(r'^b', s, re.M)
print(type(r), r) #<class 'callable_iterator'> <callable_iterator object at 0x0000024685441FF0>
for x in r:
    print(type(x), x)
#<class 're.Match'> <re.Match object; span=(0, 1), match='b'>
#<class 're.Match'> <re.Match object; span=(7, 8), match='b'>
#<class 're.Match'> <re.Match object; span=(11, 12), match='b'>

#如果需要多次使用这个正则表达式的话,使用 re.compile() 和保存这个正则对象以便复用,可以让程序更加高效。

#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
#fullmatch 是单次匹配,要求指定区间全部匹配
匹配替换

sub

re.sub(pattern, replacement, string, count=0, flags=0)
regex.sub(replacement, string, count=0)
  • 使用pattern对字符串string进行匹配,对匹配项使用replacement替换,返回的是str
  • replacement可是是string、bytes、function

subn

re.subn(pattern, replacement, string, count=0, flags=0)
regex.subn(replacement, string, count=0)
  • 同sub返回一个元祖(new_string, number_of_subs_made)

范例:sub和subn匹配替换

简单的用str.replace替换, "123".replace('12', 'ab')

import re

s = """bottle\nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
# (0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, 'b')(8, 'a')(9, 'g')
# (10, '\n')(11, 'b')(12, 'i')(13, 'g')(14, '\n')(15, 'a')(16, 'p')(17, 'p')(18, 'l')(19, 'e')

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#sub 模式替换,可以指定至多替换的次数,返回替换结果
r = re.sub(r'b\w+', 'abc', s)
print(type(r), r.encode()) #<class 'str'> b'abc\nabc\nabc\napple'
r = re.sub(r'b\w+', 'abc', s, count=1) #最多替换1次
print(type(r), r.encode()) #<class 'str'> b'abc\nbag\nbig\napple'
r = re.sub(r'b\w+', 'abc', s, count=10) #最多替换10次
print(type(r), r.encode()) #<class 'str'> b'abc\nabc\nabc\napple'

regex = re.compile(r'b\w+')
print(regex.sub('abc',s, 2).encode()) #b'abc\nabc\nbig\napple'

#subn 模式替换,可以指定至多替换的次数,返回元组(替换的结果, 替换的次数)
r = re.subn(r'b\w+', 'abc', s, count=10) #最多替换10次
print(type(r), r) #<class 'tuple'> ('abc\nabc\nabc\napple', 3) 返回元组,后面数字是替换的次数
r = re.subn(r'b\w+', 'abc', s, count=1) #最多替换1次
print(type(r), r) #<class 'tuple'> ('abc\nbag\nbig\napple', 1) 返回元组,后面数字是替换的次数

#findall 返回列表,返回匹配的字符串。跟平常理解的正则是一样的
#finditer 返回一个迭代器,每一个元素都是match实例

#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
#fullmatch 是单次匹配,要求指定区间全部匹配
分割字符串
re.split(pattern, string, maxsplit=0, flags=0)

范例:split分割匹配

import re

s = """bottle\n\t\t\t\t \nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
#(0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, '\t')(8, '\t')(9, '\t')
#(10, '\t')(11, ' ')(12, '\n')(13, 'b')(14, 'a')(15, 'g')(16, '\n')(17, 'b')(18, 'i')(19, 'g')
#(20, '\n')(21, 'a')(22, 'p')(23, 'p')(24, 'l')(25, 'e')

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#split 分割,指定分割符
print(s.split()) #默认使用 \s+ 分割 ['bottle', 'bag', 'big', 'apple']

print(re.split(r'\s+', s, maxsplit=1)) #['bottle', 'bag\nbig\napple']
print(re.split(r'\s+', s)) #['bottle', 'bag', 'big', 'apple']

s = """
os.path.abspath(path)
path. ([path)]
"""

print(re.split(r'\s', s)) #['', 'os.path.abspath(path)', 'path.', '([path)]', '']
print(re.split(r'[\s().\[\]{}]', s)) #['', 'os', 'path', 'abspath', 'path', '', 'path', '', '', '[path', ']', '']
print(re.split(r'[\s().\[\]{}]+', s)) #['', 'os', 'path', 'abspath', 'path', 'path', 'path', '']

#sub 模式替换,可以指定至多替换的次数,返回替换结果
#subn 模式替换,可以指定至多替换的次数,返回元组(替换的结果, 替换的次数)

#findall 返回列表,返回匹配的字符串。跟平常理解的正则是一样的
#finditer 返回一个迭代器,每一个元素都是match实例

#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
#fullmatch 是单次匹配,要求指定区间全部匹配
分组
  • 使用小括号的pattern捕获的数据被放到了组group中
  • match、search函数可以返回match对象
  • findall返回字符穿列表;finditer返回一个个match对象

如果pattern中使用了分组,如果有匹配的结果,会在match对象中:

  1. 使用group(N)方式返回对应分组,1到N是对应的分组,0返回整个匹配的字符串,N不写缺省为0
  2. 如果使用了命名分组,可以使用group(‘name’)的方式取分组
  3. 也可以使用groups()返回所有组
  4. 使用groupdict()返回所有命名的分组

范例:分组

import re

s = """bottle\n\t\t\t\t \nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
#(0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, '\t')(8, '\t')(9, '\t')
#(10, '\t')(11, ' ')(12, '\n')(13, 'b')(14, 'a')(15, 'g')(16, '\n')(17, 'b')(18, 'i')(19, 'g')
#(20, '\n')(21, 'a')(22, 'p')(23, 'p')(24, 'l')(25, 'e')
print()

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#split 分割,指定分割符
#sub 模式替换,可以指定至多替换的次数,返回替换结果
#subn 模式替换,可以指定至多替换的次数,返回元组(替换的结果, 替换的次数)

#findall 返回列表,返回匹配的字符串。跟平常理解的正则是一样的
#finditer 返回一个迭代器,每一个元素都是match实例

#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
#fullmatch 是单次匹配,要求指定区间全部匹配
#分组 group
m = re.match(r'b\w+', s) #分不分组要看正则表达式中有没有括号
print(m) #<re.Match object; span=(0, 6), match='bottle'>
print(m.groups()) #()  取分组

m = re.match(r'(b)(\w+)', s) #分不分组要看正则表达式中有没有括号
print(m) #<re.Match object; span=(0, 6), match='bottle'>
print(m.groups()) #('b', 'ottle') 取分组

#m = re.search(r'^(a)(\w+)', s) #匹配不到,报错

m = re.search(r'^(a)(\w+)', s, re.M) #分不分组要看正则表达式中有没有括号
print(m) #<re.Match object; span=(21, 26), match='apple'>
print(m.groups()) #('a', 'pple') 取分组

m = re.search(r'(a)(\w+)', s, re.M) #分不分组要看正则表达式中有没有括号
print(m) #<re.Match object; span=(14, 16), match='ag'>
print(m.groups()) #('a', 'g') 取分组
print(m.group()) #ag 组,组号,从1开始
print(m.group(0)) #m.group() 等价,组0表示<re.Match object; span=(14, 16), match='ag'>中match

m = re.search(r'a(\w+)', s, re.M) #分不分组要看正则表达式中有没有括号
print(m) #<re.Match object; span=(14, 16), match='ag'>
print(m.groups()) #('g',) 取分组
print(m.group()) #ag 组,组号,从1开始
print(m.group(0)) #m.group() 等价,组0表示<re.Match object; span=(14, 16), match='ag'>中match
print(m.group(1)) #g 组,组号,从1开始

m = re.search(r'a\w+', s, re.M) #分不分组要看正则表达式中有没有括号
if m: #如果匹配成功
    print(m) #<re.Match object; span=(14, 16), match='ag'>
    print(m.groups()) #() 取分组
    print(m.group()) #ag 组,组号,从1开始
    print(m.group(0)) #m.group() 等价,组0表示<re.Match object; span=(14, 16), match='ag'>中match

范例:命名分组

findall用法

import re

s = """bottle\n\t\t\t\t \nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
#(0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, '\t')(8, '\t')(9, '\t')
#(10, '\t')(11, ' ')(12, '\n')(13, 'b')(14, 'a')(15, 'g')(16, '\n')(17, 'b')(18, 'i')(19, 'g')
#(20, '\n')(21, 'a')(22, 'p')(23, 'p')(24, 'l')(25, 'e')
print()

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#split 分割,指定分割符
#sub 模式替换,可以指定至多替换的次数,返回替换结果
#subn 模式替换,可以指定至多替换的次数,返回元组(替换的结果, 替换的次数)

#findall 返回列表,返回匹配的字符串。跟平常理解的正则是一样的
#finditer 返回一个迭代器,每一个元素都是match实例

#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
#fullmatch 是单次匹配,要求指定区间全部匹配
#分组 group
m = re.search(r'a(?P<tail>\w+)', s, re.M) #分不分组要看正则表达式中有没有括号
if m: #如果匹配成功
    print(m) #<re.Match object; span=(14, 16), match='ag'>
    print(m.groups()) #('g',) 取分组
    print(m.group()) #ag 组,组号,从1开始
    print(m.group(0)) #m.group() 等价,组0表示<re.Match object; span=(14, 16), match='ag'>中match
    print()
    print(m.group(1))  # g 命名分组也有组号
    print(m.groupdict()) #{'tail': 'g'}
#分组 findall用法
m = re.findall(r'(b\w+)\s+(?P<name2>b\w+)\s+(?P<name3>b\w+)', s)
print(type(m), m) #m是什么类型 <class 'list'> [('bottle', 'bag', 'big')]
for x in m: #m有几项,能迭代几下?匹配了1下。
    print(type(x), x) #<class 'tuple'> ('bottle', 'bag', 'big')

m = re.findall(r'(b\w+)\s+\w+\s+b\w+', s)
print(type(m), m) #m是什么类型 <class 'list'> ['bottle']
for x in m: #m有几项,能迭代几下?匹配了1下。
    print(type(x), x) #<class 'str'> bottle

m = re.findall(r'(b\w+)\s+\w+\s+b(\w+)', s)
print(type(m), m) #m是什么类型<class 'list'> [('bottle', 'ig')]
for x in m: #m有几项,能迭代几下?匹配了1下。
    print(type(x), x) #<class 'tuple'> ('bottle', 'ig')

#取消分组看看
m = re.findall(r'b\w+\s+b\w+\s+b\w+', s)
print(type(m), m) #m是什么类型 <class 'list'> ['bottle\n\t\t\t\t \nbag\nbig']
for x in m: #m有几项,能迭代几下?匹配了1下。
    print(type(x), x.encode()) #<class 'str'> b'bottle\n\t\t\t\t \nbag\nbig'

finditer用法

import re

s = """bottle\n\t\t\t\t \nbag\nbig\napple"""
for i,c in enumerate(s, 1): #查看字符串索引对应关系
    print((i-1,c), end='\n' if i%10==0 else '')
#(0, 'b')(1, 'o')(2, 't')(3, 't')(4, 'l')(5, 'e')(6, '\n')(7, '\t')(8, '\t')(9, '\t')
#(10, '\t')(11, ' ')(12, '\n')(13, 'b')(14, 'a')(15, 'g')(16, '\n')(17, 'b')(18, 'i')(19, 'g')
#(20, '\n')(21, 'a')(22, 'p')(23, 'p')(24, 'l')(25, 'e')
print()

#预编译
#re.compile('\s', re.S | re.M) #预编译,单行或者多行模式,得到一个正则表达式对象
#split 分割,指定分割符
#sub 模式替换,可以指定至多替换的次数,返回替换结果
#subn 模式替换,可以指定至多替换的次数,返回元组(替换的结果, 替换的次数)

#findall 返回列表,返回匹配的字符串。跟平常理解的正则是一样的
#finditer 返回一个迭代器,每一个元素都是match实例

#match 单次匹配, 并不做全文搜索,而且要求必须是从头匹配 索引0
#search 是单次匹配,但是从index为0开始向后找,找到一次就算匹配到,找不到返回None。
#fullmatch 是单次匹配,要求指定区间全部匹配
#分组 group
#分组 findall用法
#分组 finditer用法
m = re.finditer(r'(b\w+)\s+(?P<name2>b\w+)\s+(?P<name3>b\w+)', s)
print(type(m), m) #m是什么类型 <class 'callable_iterator'> <callable_iterator object at 0x00000199F139DF90>
for x in m: #m有几项,能迭代几下?匹配了1下。
    print(type(x), x) #<class 're.Match'> <re.Match object; span=(0, 20), match='bottle\n\t\t\t\t \nbag\nbig'>
    print(x.group().encode()) #b'bottle\n\t\t\t\t \nbag\nbig'
    print(x.groups()) #('bottle', 'bag', 'big')
    print(x.groupdict()) #以命名方式提取分组 {'name2': 'bag', 'name3': 'big'}

m = re.match(r'(b\w+)\s+(?P<name2>b\w+)\s+(?P<name3>b\w+)', s)
print(type(m), m) #m是什么类型 <class 're.Match'> <re.Match object; span=(0, 20), match='bottle\n\t\t\t\t \nbag\nbig'>
print(m.group().encode()) #b'bottle\n\t\t\t\t \nbag\nbig'
print(m.groups()) #('bottle', 'bag', 'big')
print(m.groupdict()) #以命名方式提取分组 {'name2': 'bag', 'name3': 'big'}

csv与ini文件

csv文件

csv文件简介
  • 参看 RFC 4180 http://www.ietf.org/rfc/rfc4180.txt
  • 逗号分隔值Comma-Separated Values
  • CSV 是一个被行分隔符、列分隔符划分成行和列的文本文件
  • CSV不指定字符编码
  • 行分隔符为\r\n,最后一行可以没有换行符
  • 列分隔符常为逗号或者制表符
  • 每一行成为一条记录record
  • 字段可以使用双引号括起来,也可以不使用。如果字段中出现了双引号、逗号、换行符必须使用双引号括起来。如果字段的值是双引号,使用两个双引号表示一个转义
  • 表头可选,和字段列对齐就行
手动生成csv文件

为了理解csv格式

from pathlib import Path

p1 = Path('d:/tmp/mydir/my.csv')
p1.parent.mkdir(parents=True, exist_ok=True)

# """\ 中\斜杠表示续行符,近接着后面字符
# ""双双引号代表一个双引号
csv_body = """\
id,name,age,comment
1,zs,20, I'm 20
2,ls,18,"test string""1223."
3,ww,30,"
python

你好
"
"""

# with p1.open('w', encoding='utf-8') as f: # newline = None \n 转换成 OS对应的换行符
#     f.write(csv_body)
print(csv_body.encode())
#b'id,name,age,comment\n1,zs,20, I\'m 20\n2,ls,18,"test string""1223."\n3,ww,30,"\npython\n\n\xe4\xbd\xa0\xe5\xa5\xbd\n"\n'

p1.write_text(csv_body, encoding='utf-8')
csv模块
csv.reader(iterable, dialect='excel', *args, **kwargs)
  • 返回reader 对象,是一个行迭代器
  • 默认使用excel方言,如下:
    • delimiter 列分隔符,逗号
    • lineterminator 行分隔符\r\n
    • quotechar 字段的引用符号,缺省为 " 双引号
    • 双引号的处理
      • doublequote 双引号的处理,默认为True。如果碰到数据中有双引号,而quotechar也是双引号,True则使用2个双引号表示,False表示使用转义字符将作为双引号的前缀
      • escapechar 一个转义字符,默认为None
      • writer = csv.writer(f, doublequote=False, escapechar=’@’)遇到双引号,则必须提供转义字符
    • quoting 指定双引号的规则
      • QUOTE_ALL 所有字段
      • QUOTE_MINIMAL 特殊字符字段,Excel方言使用该规则
      • QUOTE_NONNUMERIC 非数字字段
      • QUOTE_NONE 都不使用引号
csv.writer(fileobj, dialect='excel', *args, **kwargs)
  • 返回DictWriter的实例
  • 主要方法有writerow、writerows
writerow(iterable)
  • 说明row行,需要一个可迭代就可以,可迭代的每一个元素,将作为csv行的每一个元素
  • windows下回再每行末尾多出一个/r,解决办法 open(‘test.csv’, ‘w’, newline=’’)

范例:读取csv文件

from pathlib import Path
import csv

p1 = Path('d:/tmp/mydir/my.csv')
p1.parent.mkdir(parents=True, exist_ok=True)

# """\ 中\斜杠表示续行符,近接着后面字符
# ""双双引号代表一个双引号
csv_body = """\
id,name,age,comment
1,zs,20, I'm 20
2,ls,18,"test string""1223."
3,ww,30,"
python

你好
"
"""

with p1.open(encoding='utf-8') as f: #在windows下默认使用gbk编码读取
    reader = csv.reader(f)
    print(next(reader)) #字段分开,返回列表,每一个字段项
    print(next(reader))
    print('-' * 30)
    for line in reader:
        print(line)

# ['id', 'name', 'age', 'comment']
# ['1', 'zs', '20', " I'm 20"]
# ------------------------------
# ['2', 'ls', '18', 'test string"1223.']
# ['3', 'ww', '30', '\npython\n\n你好\n']

范例:写csv文件

from pathlib import Path
import csv

p1 = Path('d:/tmp/mydir/my.csv')
p1.parent.mkdir(parents=True, exist_ok=True)

# """\ 中\斜杠表示续行符,近接着后面字符
# ""双双引号代表一个双引号
csv_body = """\
id,name,age,comment
1,zs,20, I'm 20
2,ls,18,"test string""1223."
3,ww,30,"
python

你好
"
"""

#写csv文件
p2 = Path('d:/tmp/mydir/test.csv')
rows = [
    ['id', 'name', "age"],
    range(3),  #数字 -> 文本文件 -> 0 字符串
    'abcdefg',
    (1, 'tom', '20'),
    ((1,), (2,)),
    ([1], [2])
]

with p2.open('w', encoding='utf-8', newline='') as f: #写时,- '\n' 或''空串表示'\n' 不替换
    writer = csv.writer(f)
    writer.writerow(rows[0]) #写第一行
    writer.writerows(rows[1:]) #写多行

# id,name,age
# 0,1,2
# a,b,c,d,e,f,g
# 1,tom,20
# "(1,)","(2,)"
# [1],[2]

#读文件
with p2.open(encoding='utf-8', newline='') as f: #在windows下默认使用gbk编码读取.
    # newline='' 表示不会自动转换通用换行符,常见换行符都可以认为是换行
    reader = csv.reader(f)
    first = next(reader) #字段分开,返回列表,每一个字段项
    print(first)
    second = next(reader)
    print(second)
    print('-' * 30)
    for line in reader:
        print(line)
#读回来的都是字符串,类型没有
# ['id', 'name', 'age']
# ['0', '1', '2']
# ------------------------------
# ['a', 'b', 'c', 'd', 'e', 'f', 'g']
# ['1', 'tom', '20']
# ['(1,)', '(2,)']
# ['[1]', '[2]']

ini文件处理

作为配置文件,ini文件的格式很流畅

[DEFAULT]
a = test

[mysql]
default-character-set=utf8

[mysqld]
datadir =/dbserver/data
port = 33060
character-set-server=utf8
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
  • 中括号里面的部分称为section,译作节、区、段
  • 每一个section内,都是key=value形成的键值对,key称为option选项
  • 注意这里的DEFAULT是缺省section的名字,必须大写
configparser
  • configparser模块的ConfigParser类就是用来操作
  • 可以将section当做key,section存储着键值对组成的字典,可以把ini配置文件当做一个嵌套的字典。默认使用的是有序字典
read(filenames, encoding=None)
- 读取ini文件,可以是单个文件,也可以是文件列表。可以指定文件编码

sections()
- 返回section列表。缺省section不包括在内

add_section(section_name)
- 增加一个section

has_section(section, option)
- 判断seciton是否存在

options(section)
- 返回section的所有option,会追加缺省section的option

has_option(section, option)
- 判断section是否存在这个option

get(section, option, *, raw=False, vars=None[, fallback])
- 从指定的段的选项上取值,如果找到返回,如果没有找到就去找DEFAULT段有没有

getint(section, option, *, raw=False, vars=None[, fallback])
getfloat(section, option, *, raw=False,vars=None[, fallback])
getboolean(section, option, *, raw=False,vars=None[, fallback])
上面3个方法和get一样,返回指定类型数据

items(raw=False, vars=None)

items(section, raw=Fale, vars=None)
- 没有section,则返回所有section名字及其对象;如果指定section,则返回这个指定的seciton的键值对组成而元组

set(section, option, value)
- section存在的情况下,写入option=value,要求option、value必须是字符串

remove_section(section)
- 移除seciton及其所有option

remove_option(section, option)
- 移除section下的option

write(fileobject, space_aroud_delimiters=True)
- 将当前config的所有内容写入fileobject中,一般open函数使用w模式

范例:读取

import configparser #配置文件解析器 ini

cfg = configparser.ConfigParser()
# 配置文件解析器对象
print(cfg) #<configparser.ConfigParser object at 0x0000014E8B2056A0>
r = cfg.read('mysql.ini')
#r = cfg.read(('mysql.ini', 'mysql.ini'))
print(r) #['mysql.ini']

#读取section
for x in cfg.sections(): #返回的是所有section,但不包含特殊的缺省section
    print(type(x), x)
# <class 'str'> mysql
# <class 'str'> mysqld
    print(cfg.options(x)) #每个section下的options,包含缺省
# ['default-character-set', 'a']
# ['datadir', 'port', 'character-set-server', 'sql_mode', 'a']

print(cfg.items()) #ItemsView(<configparser.ConfigParser object at 0x000002D346FE56A0>)
for y in cfg.items(): #items() 迭代时,包含了DEFAULT
    print(type(y), y)
# <class 'tuple'> ('DEFAULT', <Section: DEFAULT>)
# <class 'tuple'> ('mysql', <Section: mysql>)
# <class 'tuple'> ('mysqld', <Section: mysqld>)
print('-' * 30)
for k,v in cfg.items(): #items() 迭代时,包含了DEFAULT
    print(type(k), k) #k str
    print(type(v), v) #v section对象
    print(cfg.items(k)) #二元组可迭代对象,
    print('+' * 30)
# <class 'str'> DEFAULT
# <class 'configparser.SectionProxy'> <Section: DEFAULT>
# [('a', 'test')]
# ++++++++++++++++++++++++++++++
# <class 'str'> mysql
# <class 'configparser.SectionProxy'> <Section: mysql>
# [('a', 'test'), ('default-character-set', 'utf8')]
# ++++++++++++++++++++++++++++++
# <class 'str'> mysqld
# <class 'configparser.SectionProxy'> <Section: mysqld>
# [('a', 'test'), ('datadir', '/dbserver/data'), ('port', '33060'), ('character-set-server', 'utf8'), ('sql_mode', 'NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES')]
# ++++++++++++++++++++++++++++++

print(cfg.has_option('mysqld', 'a'))  #mysql下有没有a? 有 返回True

#取值
t = cfg.get('mysqld', 'a')
print(type(t), t) #<class 'str'> test
t = cfg.get('mysqld', 'port', fallback=[]) #如果没有值,返回fallback的值
print(type(t), t) #<class 'str'> 33060
t = cfg.getint('mysqld', 'port', fallback=3306) #类型转换, int(get()) 如果没有值,返回fallback的值.
print(type(t), t) #<class 'int'> 33060

范例:写入

import configparser #配置文件解析器 ini

cfg = configparser.ConfigParser()
# 配置文件解析器对象
print(cfg) #<configparser.ConfigParser object at 0x0000014E8B2056A0>
r = cfg.read('mysql.ini')
#r = cfg.read(('mysql.ini', 'mysql.ini'))
print(r) #['mysql.ini']

#写入
cfg.set('mysqld', 'a', '100') #在内存中操作。 设置mysqld上a的值为100
with open('t.ini', 'w', encoding='utf-8') as f:
    cfg.write(f)

if cfg.has_section('test'):
    cfg.remove_section('test') #本section所有options全部删除

cfg.add_section('test')
print(cfg['mysql']) #<Section: mysql>

#字典操作更简单
cfg['test']['t1'] = '1000'
cfg['test']['t2'] = str([1,2,3])
t = cfg['test']['t2']
print(type(t), t) #<class 'str'> [1, 2, 3]
print(cfg.get('test', 't2')) #[1, 2, 3]

cfg['test'] = {'t3': 'abc'} #section存在,直接覆盖test中的内容
print(cfg.options('test')) #['t3', 'a']
cfg['test2'] = {'y': '1000'} #section不存在
print(cfg.options('test2')) #['y', 'a']

with open('t.ini', 'w', encoding='utf-8') as f:
    cfg.write(f)

#其他
print(cfg._sections)
#{'mysql': {'default-character-set': 'utf8'}, 'mysqld': {'datadir': '/dbserver/data', 'port': '33060', 'character-set-server': 'utf8', 'sql_mode': 'NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES', 'a': '100'}, 'test': {'t3': 'abc'}}
for k,v in cfg._sections.items():
    print(k,v)
# mysql {'default-character-set': 'utf8'}
# mysqld {'datadir': '/dbserver/data', 'port': '33060', 'character-set-server': 'utf8', 'sql_mode': 'NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES', 'a': '100'}
# test {'t3': 'abc'}

序列化和反序列化

主要内容:

  • 序列化、反序列化重要性
  • Python 序列化库 Pickle
  • 文本序列化方案 Json
  • 高效二进制序列化库 Msgpack
  • 常用序列化方案比较和适用场景

为什么要序列化

  • 内存中的字典、列表、集合以及各种对象,如何保存到一个文件中?
  • 如果是自己定义的类的实例,如何保存到一个文件中?
  • 如何从文件中读取数据,并让他们在内存中再次恢复成自己对应的类的实例?

要设一套 协议 ,就按照某种规则,把内存中数据保存到文件中。文件是一个字节序列,所以必须把数据转换成字节序列,输出到文件。这就是 序列化

反之,把文件的字节序列恢复到内存并且还是原来的类型,就是 反序列化

定义

serialization 序列化

  • 将内存中对象存储下来,把它变成一个个字节 ->二进制

deserialization 反序列化

  • 将文件的一个个字节恢复成内存中对象 -> 二进制

序列化保存到文件就是持久化

  • 可以将数据序列化后持久化,或者网络传输;也可以将文件中或者网络接收到的字节序列反序列化

Python提供了pickle库

pickle库

Python中的序列化、反序列化模块

对应函数

dumps  对象序列化为bytes对象
dump   对象序列化到文件对象,就是存入文件
loads  从bytes对象反序列化
load   对象反序列化,从文件读取数据

范例:序列化、反序列化

#理解二进制
import pickle
from pathlib import Path

filename = 'd:/tmp/mydir/ser.bin'

Path(filename).parent.mkdir(parents=True,exist_ok=True)

i = 99 #int
c = 'c' #str
l = list(range(1,4))
d = {'a': 1, 'b': 'abc', 'c':[1,2,3]}

#写入,序列化
with open(filename, 'wb') as f:
    pickle.dump(i, f) #序列化 99的16进制 0x63 , 二进制文件的表现,80 04 int 63 边界2e
# # hexdump.exe -C ser.bin
# 00000000  80 04 4b 63 2e                                    |..Kc.|
# 00000005
    pickle.dump(c, f) #c 0x8004 str 0x63 边界\x2e
# # hexdump.exe -C ser.bin
# 00000000  80 04 4b 63 2e 80 04 95  05 00 00 00 00 00 00 00  |..Kc............|
# 00000010  8c 01 63 94 2e                                    |..c..|
    pickle.dump(l, f) #l 0x8004 list 0x4b01 0x4b02 0x4b03 边界\x2e
# # hexdump.exe -C ser.bin
# 00000000  80 04 4b 63 2e 80 04 95  05 00 00 00 00 00 00 00  |..Kc............|
# 00000010  8c 01 63 94 2e 80 04 95  0b 00 00 00 00 00 00 00  |..c.............|
# 00000020  5d 94 28 4b 01 4b 02 4b  03 65 2e                 |].(K.K.K.e.|
    pickle.dump(d, f) #d 0x8004 dict xx 边界\x2e
# # hexdump.exe -C ser.bin
# 00000000  80 04 4b 63 2e 80 04 95  05 00 00 00 00 00 00 00  |..Kc............|
# 00000010  8c 01 63 94 2e 80 04 95  0b 00 00 00 00 00 00 00  |..c.............|
# 00000020  5d 94 28 4b 01 4b 02 4b  03 65 2e 80 04 95 23 00  |].(K.K.K.e....#.|
# 00000030  00 00 00 00 00 00 7d 94  28 8c 01 61 94 4b 01 8c  |......}.(..a.K..|
# 00000040  01 62 94 8c 03 61 62 63  94 8c 01 63 94 5d 94 28  |.b...abc...c.].(|
# 00000050  4b 01 4b 02 4b 03 65 75  2e                       |K.K.K.eu.|

#读取,反序列化
with open(filename, 'rb') as f:
    for i in range(4):
        t = pickle.load(f)
        print(i+1, type(t), t)
# 1 <class 'int'> 99
# 2 <class 'str'> c
# 3 <class 'list'> [1, 2, 3]
# 4 <class 'dict'> {'a': 1, 'b': 'abc', 'c': [1, 2, 3]}

可以看到二进制的序列化文件中,每一个数据最重要的,为了保存数据并在以后可以还原它,至少需要保存数据的 类型 数据 边界

对象序列化

import pickle

#
class AAA:
    tttt = 'ABC'  #类属性
    def __init__(self): #初始化方法
        print('init~~~~~')
        self.cccc = 'AABBCC' #实例的属性,每一个实例都不同
    def show(self): #def 方法
        print('aabbcc')

a1 = AAA()  #实例化,会调用__init__

#序列化
ser = pickle.dumps(a1)
print('ser={}'.format(ser))
# ser=b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x03AAA\x94\x93\x94)\x81\x94}\x94\x8c\x04cccc\x94\x8c\x06AABBCC\x94sb.'

with open('d:/tmp/mydir/ser', 'wb') as f:
    f.write(ser)
# # hexdump.exe -C ser
# 00000000  80 04 95 2b 00 00 00 00  00 00 00 8c 08 5f 5f 6d  |...+.........__m|
# 00000010  61 69 6e 5f 5f 94 8c 03  41 41 41 94 93 94 29 81  |ain__...AAA...).|
# 00000020  94 7d 94 8c 04 63 63 63  63 94 8c 06 41 41 42 42  |.}...cccc...AABB|
# 00000030  43 43 94 73 62 2e                                 |CC.sb.|


#反序列化
obj = pickle.loads(ser)
print(type(obj)) #返回的是类型名 AAA <class '__main__.AAA'>
print(obj) #AAA object对象实例,在内存中的位置 <__main__.AAA object at 0x0000019E2D687D90>
print(obj.tttt) #ABC
obj.show() #aabbcc
print(obj.cccc) #AABBCC

#序列化之后,反序列化的时候使用同样的定义
#list实例,反序列化时不会有这样的现象,
#因为你根本不会使用自定义的list,用的还是python的list

上面的例子中,故意使用了连续的AAA、ABC、abc等字符串,就是为了在二进制文件中能容易的发现它们 上例中,其实就保存了一个类名,因为所有的其他东西都是类定义的东西,是不变的,所以只序列化一个AAA类名。反序列化的时候找到类就可以恢复一个对象

可以看出这回除了必须保存的AAA,还序列化了aaaa和abc,因为这是每一个对象自己的属性,每一个对象不一样的,所以这些数据需要序列化

序列化、反序列化实验

序列化的类定义和反序列化的类定义可能是不一致的。

因为反序列化时会找当前代码中的类名。有则使用当前类。

因此,序列化、反序列化必须保证使用同一套类的定义,否则会带来不可预料的结果

序列化应用

一般来说,本地序列化的情况,应用较少。大多数场景都应用在网络传输中。

将数据序列化后通过网络传输到远程节点,远程服务器上的服务将接受到的数据反序列化后,就可以使用了

但是需要注意,远程接受端,反序列化时必须有对应的数据类型,否则就会报错。尤其是自定义类,必须远程得有一致的定义

现在,大多数项目,都不是单机的,也不是单服务的,需要多个程序之间配合。需要通过网络将数据传送到其他节点上去,这就需要大量的序列化、反序列化过程

但是,问题是,Python程序之间还可以都用pickle解决序列化、反序列化,如果是跨平台、跨语言、跨协议。pickle就不太适合,需要公共的协议。例如XML、Json、Protocol Buffer等

不同的协议,效率不同,适用不同的场景,根据不同的情况分析选型

Json

Json(JavaScript Object Notation,JS 对象标记)是一种轻量级的数据交换格式。它基于 ECMAScript(w3c组织制定的JS规范)的一个子集,采用完全独立于变成语言的文本格式来存储和表示数据

Json官网:http://json.org/

Json是数据类型

双引号引起来的字符串,数值,true和false,null,对象,数组。这些都是值

value.png
字符串

由双引号包围起来的任意字符的组合,可以有转义字符

数值

有正负,有整数、浮点数

对象

无序的键值对的集合

格式:{key1:value1,…,keyn:valuen}

key必须是一个字符串,需要双引号包围这个字符串

value可以是任意合法的值

object.png
数组

有序的值的集合

格式:[val1,…,valn]

array.png

范例

{
    "person":[
        {
            "name": "tom",
            "age": 18
        },
        {
            "name": "jerry",
            "age": 16
        }
    ],
    "total": 2
}      

Json 模块

Python 与 Json

Python支持少量内建数据类型到Json类型的转换

Python类型 Json类型
True true
False false
None null
str string
int integer
float float
list array
dict object
常用方法
Python类型 Json类型
dumps json编码
dump json编码并存入文件
loads json编码
load json编码,从文件读取数据

范例:

import json

d = {
    'name':'Tom',
    'age':20,
    'insterest':('movie', 'music'),  # ?
    'class':['python'] #array
} #dict => object

#序列化json
s = json.dumps(d)  # 转成字符序列
print(type(s), len(s)) #<class 'str'> 80
print(s) #{"name": "Tom", "age": 20, "insterest": ["movie", "music"], "class": ["python"]}

#反序列化json
new = json.loads(s) # str => dict 实际上是 js:object => dict
print(type(new)) #<class 'dict'>
print(new) #{'name': 'Tom', 'age': 20, 'insterest': ['movie', 'music'], 'class': ['python']}

#尽量写成js支持的类型
d = {
    'name':'Tom',
    'age':20,
    'insterest':['movie', 'music'],
    'class':['python'] #array
} #dict => object

#序列化json
s = json.dumps(d)  # 转成字符序列
print(type(s), len(s)) #<class 'str'> 80
print(s) #{"name": "Tom", "age": 20, "insterest": ["movie", "music"], "class": ["python"]}

#反序列化json
new = json.loads(s) # str => dict 实际上是 js:object => dict
print(type(new)) #<class 'dict'>
print(new) #{'name': 'Tom', 'age': 20, 'insterest': ['movie', 'music'], 'class': ['python']}


print(d == new) # ? True 等号代表内容是否相同
print(d is new) # ? False is代表内存位置是否相同
print(id(d), id(new)) #2713103083392 2713101598528

一般json编码的数据很少落地,数据都是通过网络传输。传输的时候,要考虑压缩它

本质上来说它就是个文本,就是个字符串

json很简单,几乎编程语言都支持json,所以应用范围十分广泛

MessagePack

官网:https://msgpack.org/

MessagePack是一个基于 二进制 高效的对象序列化类库,可用于跨语言通信

它可以像Json那样,在许多语言之间交换结构对象

但是它比Json更快速也更轻巧

支持Python、Ruby、Java、C/C++等众多语言。

兼容json和pickle

可以看出,大大的节约了空间

# Json:27 bytes
{"person":[{"name":"tom","age":18},{"name":"jerry",."age":16}],"total":2}

# MessagePack:18 bytes
# 82 a7 63 6f 6d 70 61 63 74 c3 a6 73 63 68 65 6d 61 00

#流量就是金钱,在流量很大时就能体现出来了
安装
pip install msgpack
常用方法
  • packb 序列化对象。提供了dumps来兼容pickle和json
  • unpackb 反序列化对象。提供了loads来兼容
  • pack 序列化对象保存到文件对象。提供了dump来兼容
  • unpack 反序列化对象保存到文件对象。提供了load来兼容

MessagePack简单易用,高效压缩,支持语言丰富 所以,用它序列化也是一种很好的选择。Python很多大名鼎鼎的库都是用了msgpack

范例:对比

import json
import pickle
import msgpack

d = {
    'Persons':[
        {'name':'tom', 'age':20}, #dict => object
        {'name':'jerry', 'age':18}
    ], #list => array
    'test':'aaaaaa',
    'total': 2
} # dict => object

#序列化
for i,module in enumerate((pickle, json, msgpack)): #元组, 三个元素
    # pickle.dumps(d) #99 bytes
    # json.dumps(d) #101 bytes 会有一些无效的空格
    # msgpack.dumps(d) #61 bytes
    data = module.dumps(d)
    print(module.__name__, "len={}".format(len(data)), type(data))
    print(i, data)
# pickle len=99 <class 'bytes'>
# 0 b'\x80\x04\x95X\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x07Persons\x94]\x94(}\x94(\x8c\x04name\x94\x8c\x03tom\x94\x8c\x03age\x94K\x14u}\x94(h\x04\x8c\x05jerry\x94h\x06K\x12ue\x8c\x04test\x94\x8c\x06aaaaaa\x94\x8c\x05total\x94K\x02u.'
# json len=101 <class 'str'>
# 1 {"Persons": [{"name": "tom", "age": 20}, {"name": "jerry", "age": 18}], "test": "aaaaaa", "total": 2}
# msgpack len=61 <class 'bytes'>
# 2 b'\x83\xa7Persons\x92\x82\xa4name\xa3tom\xa3age\x14\x82\xa4name\xa5jerry\xa3age\x12\xa4test\xa6aaaaaa\xa5total\x02'

#反序列化
u = msgpack.loads(data) #utf-8
print(type(u), u)

u2 = msgpack.loads(data, raw=True) #新版 raw=True 用原始的,不转字符串了
print(type(u2), u2)
# <class 'dict'> {'Persons': [{'name': 'tom', 'age': 20}, {'name': 'jerry', 'age': 18}], 'test': 'aaaaaa', 'total': 2}
# <class 'dict'> {b'Persons': [{b'name': b'tom', b'age': 20}, {b'name': b'jerry', b'age': 18}], b'test': b'aaaaaa', b'total': 2}