原文:https://mofanpy.com/tutorials/python-basic/interactive-python/regex/
读写文件
创建文件
这个文件会被保存在哪呢?正常来说,他会保存在你脚本的当前目录下,比如所你运行的是 me.py 脚本, 那么 new_file.txt 就会被创建在这个脚本的同级目录中。
如果你觉得要写一个 f.close() 或者有时候你怕自己忘记要 close(), Python 人性化地提供了另外一种打开文件的方式。 这个方式把打开和关闭嵌入到了一个 with 架构中。所以再也不用担心忘记关闭文件了,这种 with 的模式也是我个人比较喜欢的模式。 下面这个案例,除了使用了 with 模式,我还换了一种写入数据的方式 writelines(), 当你传入的时候像列表样的数据时, 列表中的每个元素就是一行记录,数据会分行来写。
注意,在列表里,每个元素最后都最好写一个\n来表示要另起一行。不然读出来的时候就黏在一起了。不信你试试。
读文件
上面有演示如果你的记录是一个列表,你想在文件中每行记录列表当中的一个值,可以用 writelines(),那么在读文件的时候, 也可以 readlines() 直接读出来一个列表。
在机器学习和大数据的时代,我们的数据往往是非常大的,很多都是以GB为单位。而如果一次性读取一个这么大的文件,很有可能你的内存都放不下。 怎么办呢?我们能不能一行一行读取,取代一次性读取,不让内存被一次性占满?当然可以,readline() 就来拯救你了。 注意哦,这个功能没有 s,不是上一个功能哦。
文件编码,中文乱码
在做中文项目的时候,有一个很头疼的事情,很有可能你的文件是从不同平台拿过来的,比如 Windows 的文件要在 MacOS 里打开。 英文的还好,一般不会有什么问题,但是中文的就很容易出现编码不一的情况,出现中文乱码。这时候我们怎么办呢? 首先你要弄清楚原文件的编码,通常来说是 utf-8、gbk、gb2312其中的某一种。
下面我模拟一下,有些文件在 Windows 存储的时候,是以 gbk 的格式存储的,下面的 chinese.txt 我就模拟用 gbk 编码保存。 注意这里我选用的写模式 w 还多了一个 b,合起来是 wb,意思是 write binary 形式,取代默认的 text 形式。所以我读的时候, 也加上了一个 b,变成了 rb,read binary。你点运行试试,他会给你出一段乱码,因为Python不识别这段编码后的文本。
那如果我直接用原始的 r 来读文本呢?它甚至都会给我报一个错。读不了。
怎么办?方法还是有的,我们先确认是哪一种文件编码,然后在读的时候,需要传入一个 encoding 的参数,表示用这一种编码来读。 这样中文乱码的问题就顺利解决了。
更多读写模式
os库
os库是Python自带的一个非常有用的模块。如果你做一个系统性的东西,要处理文件输出,读入等问题, 那么你有80%概率会使用到os库中的一些功能。 os模块有非常多功能,我们今天就来侧重将一些你日常会经常使用的。
文件目录操作
首先你得知道你在哪?此话怎讲?我当然知道我在那,就在电脑面前做着不可描述的事。 NONONO,我指的是你的当前目录是在哪。当你运行一个程序的时候,通常而言当前目录就是你运行这个程序的目录。 那我们来找找在这个交互式学Python的环境中,你的当前目录是哪?
os.makedirs(“project”,exist_ok=True)----创建名为“project”的文件夹,后面这个 exist_ok 的意思是当我检测到这个目录存在, 我就睁一只眼闭一只眼,当没事发生,不然这句指令会报错的。
通过 os.path.exists(“project”) 来检测是否真的存在这个东西。
文件管理系统
下面我们来以文件管理系统里面会遇到的情况举例。比如用户注册了我这个系统,我得为这个用户创建一个他的文件夹。
以上只能删除空文件夹,如果文件夹里面存在文件,就删不掉。
这怎么办呢?把文件一个个删了?太费事了吧,其实这种事情,需要用到另一个库,名字叫做 shutil,但是你要注意的是, 这个库太强大了,如果你不小心删掉了其他东西,那就真没了,一定要小心!它可以清空整个目录。
随着你的文件系统越来越厉害,你得应付不同用户的不同需求,比如改名字。
文件目录多种检验
有了用户目录,还有很多事情是需要判断的,比如有没有某个文件呀,文件路径的一些便捷操作呀。 下面我先建一个文件,然后再对其进行一些鉴别。Python里的 os.path 真的也是非常容易接触到的一个内容。 光是判断这是否是一个文件或者是否是一个文件夹,都有很多中不同的判断方式。
在很多时候,我们的文件是通过传参传进来的,比如我随便告诉你一个文件目录,我想为这个文件创建一个副本,我就得用到三个功能,
或者,还有个更方便的功能代替拿文件名和文件夹名的工作:
正则表达式
不用正则的判断
正则给格外信息
先不管我写的那些正则 pattern 规则,你可以看到在返回的信息中,有一个 span=(13,30),有一个 match='mofan@mofanpy.com 这类的信息。 他们分别代表着在原始字符串中,我们找到的 pattern 是从哪一位到哪一位,pattern 找到的具体字符串又是什么。下面我们再来看看一个简单的例子。
我们看到了很多次,我都是用 r"xxx" 来写一个 pattern,这是为什么呢?因为正则表达式很多时候都要包含\,r 代表原生字符串, 使用 r 开头的字符串是为了不让你混淆 pattern 字符串中到底要写几个 \,你只要当成一个规则来记住在写 pattern 的时候,都写上一个 r 在前面就好了。
你想必会思考,我拿着这个 match 变量,该怎么使用呢?怎么才能拿出来里面匹配好的字符串呢?目前这个阶段,你就记着用 .group() 能取出来里面找到的字符。
同时满足多种条件
在上面匹配 run 字符中,我们看到了可以在字符串中找固定的词,如果我想让这种匹配模式多样化,一种模式包含多种字符的可能呢? 你首先可能尝试的是下面这种方法。比如判断中我们不区分它是不是过去式。
正则更厉害的一个地方就是它能把这两种情况写在一个判断里。只需要使用一个 | 就可以搞定啦。这个 | 就代表或者的意思。
我们观察,在 run 与 ran 之间,其实它们的差别就只是中间这个字母,我们还能使用 [au] 来简化一下。 让它同时接受中间字母是 a 或者 u 的情况。
还有一种情况,就是我的前后都是固定的,但是我要同时满足多个字符的不同匹配,比如我想同时找到 find 和 found。我该怎么办呢?
按类型匹配
再回到 email 匹配的例子,我们就不难发现,我用了 \w 这个标识符,来表示任意的字母和数字还有下划线。因为大多数 email 都是只包含这些的。 而且我还是用了 +? 用来表示让 \w 至少匹配 1 次,并且当我识别到 @ 的时候做非贪婪模式匹配,也就是遇到 @ 就跳过当前的重复匹配模式, 进入下一个匹配阶段。
除了邮箱在我们的文件管理系统中常见,我们再举一个常见的电话号码的识别例子。比如手机号的识别,假设我们只识别 138 开头的手机号码。 下面的 \d{8} 就是用来表示任意的数字,重复 8 遍。
中文
我怎么才能把汉字全部匹配出来呢?就像 r"[a-zA-Z]" 识别出所有英文那样? 当然也可以,汉字通常是用 Unicode 来表示的,如果把汉字变成 Unicode,我们可以看到汉字在计算机中裸体的样子。
你看到 中 字用 Unicode 表示的样子后,会不会突然发现,这不就是一串英文吗?只要我把汉字的 Unicode 全写出来就好了, 好在 Unicode 是可以连续的,我们可以用英文那样类似的办法来处理。
这挺符合我们的预期,它将里面的中文都识别出来了,剔除掉了英文和标点。那有时候我们还是想留下对标点的识别的,怎么办? 我们只需要将中文标点的识别范围,比如 [!?。,¥【】「」] 补进去就好了。
查找替换等更多功能
在模式中获取特定信息
上面这种做法虽然可行,但是还不够简单利索,因为同时用到了两个功能 finditer 和 sub,正则还可以更简单, 我们能用 group 获取到特定位置的信息。上面的例子中已经使用了 group,但是我们还没有发挥 group 的全部力量。 在介绍 group 之前我在介绍一下和 group 一起用的 ()。只要我们在正则表达中,加入一个 () 选定要截取返回的位置, 他就直接返回括号里的内容。
那如果我想获取更详细的信息呢?比如年月日分开获取?答案是多做几个括号就好了,然后用 group 功能获取到不同括号中匹配到的字符串。
string = "I have 2021-02-01.jpg, 2021-02-02.jpg, 2021-02-03.jpg"
match = re.finditer(r"(\d+?)-(\d+?)-(\d+?)\.jpg", string)
for file in match:
print("matched string:", file.group(0), ",year:", file.group(1), ", month:", file.group(2), ", day:", file.group(3))
我还提供你另一种途径实现上面的功能,你看看,下面这个 findall 也可以达到同样的目的。只是它没有提供 file.group(0) 这种全匹配的信息。
string = "I have 2021-02-01.jpg, 2021-02-02.jpg, 2021-02-03.jpg"
match = re.findall(r"(\d+?)-(\d+?)-(\d+?)\.jpg", string)
for file in match:
print("year:", file[0], ", month:", file[1], ", day:", file[2])
有时候我们 group 的信息太多了,括号写得太多,让人眼花缭乱怎么办?我们还能用一个名字来索引匹配好的字段, 然后用 group(“索引”) 的方式获取到对应的片段。注意,上面方案中的 findall 不提供名字索引的方法, 我们可以用 search 或者 finditer 来调用 group 方法。为了索引,我们需要在括号中写上 ?P<索引名> 这种模式。
string = "I have 2021-02-01.jpg, 2021-02-02.jpg, 2021-02-03.jpg"
match = re.finditer(r"(?P<y>\d+?)-(?P<m>\d+?)-(?P<d>\d+?)\.jpg", string)
for file in match:
print("matched string:", file.group(0),
", year:", file.group("y"),
", month:", file.group("m"),
", day:", file.group("d"))
多模式匹配
在正则中还有一些特别的 flags,可以在 re.match(),re.findall() 等功能中使用。 主要目的也是方便我们编写正则,和用更简单的方法处理更复杂的表达式。
ptn, string = r"r[ua]n", "I Ran to you"
print("without re.I:", re.search(ptn, string))
print("with re.I:", re.search(ptn, string, flags=re.I))
第二个我们想在每行文字的开头匹配特定字符,如果用 ^ran 固定样式开头,我是匹配不到第二行的 ran to you 的,所以我们得加上一个 re.M flag。 注意我们提到过的 re.search() 和 re.match() 有一丢丢不一样,re.match() 是不管你有没有 re.M flag,我的匹配都是按照最头头上开始匹配的。 所以在下面的实验中,re.match() 匹配不到任何东西。
ptn = r"^ran"
string = """I
ran to you"""
print("without re.M:", re.search(ptn, string))
print("with re.M:", re.search(ptn, string, flags=re.M))
print("with re.M and match:", re.match(ptn, string, flags=re.M))
如果你想用多种 flags,也是可以的,比如我想同时用 re.M, re.I,你只需要这样书写re.M|re.I
ptn = r"^ran"
string = """I
Ran to you"""
print("with re.M and re.I:", re.search(ptn, string, flags=re.M|re.I))
其实还有一种写法可以直接在 ptn 里面定义这些 flags,有的人会比较喜欢下面这样的写法,在模式 ptn 的开头,注明我要采用哪几个 flags: (?im) 这就是说要用 re.I, re.M。
更快地执行
如果你要重复判断一个正则表达式,我们通常不会直接在 re.search(ptn) 这里里面写 ptn,而是在外面先定义好,解析好一个正则 pattern,然后直接用这个 pattern 循环执行查找。 这样可以更有效率,比如你要重复查找 100 万次,我们先 compile 正则再查找能节省可观的时间。
pickle/json序列化
文件系统,机器学习,大数据等,都少不了数据文件。Python 也提供了一些比较方便序列化的存储的组件。 什么是序列化(Serialization)呢,说简单也简单,就是把像字典,列表这类的数据,打包保存在电脑硬盘中。 如果粗略的理解对比,就有点像 zip 和 unzip 的过程。
pickle
我在什么时候会想到要用 pickle 呢?就是我想要把一整个 class 都打包的时候,我会想起要用 pickle。 但是用 pickle 打包 class 是一件有风险的事情,有时候还会失败。我们在后面一点来看看哪些时候会失败,我们又怎么避免打包失败。
下面,我们先来看看一个最简单的例子,怎么把一个字典打包到 pickle 里,打包以后又是什么样子?
import pickle
data = {"filename": "f1.txt", "create_time": "today", "size": 111}
pickle.dumps(data)
如果你点击了运行,你可以看到这个字典被 pickle 以后,已经面目全非,你不能直接读出来里面的信息,因为这些信息已经被编码了。 所以当你在用 pickle 的时候另外一点重要的是,你需不需要能看懂被打包的数据,如果没有这个需求,那你就可以用 pickle, 不然我们后面还会介绍一个叫 json 的打包库,它打出来的包,就是你能看懂的东西了。
好,前面我们只是让 data 变成一串编码字节,下面我们可以用 pickle.dump() 将字典直接转换成一个文件。
光存下来肯定没意思,我们还的在另外一个时刻,将文件读出来。pickle.load() 就能帮上忙。 我们用 open 的方式把文件都读出来,然后再用 pickle 对其解析,还原成最开始的那一个字典。
除了常见的字典,列表,元组这类的数据,pickle 甚至都可以打包 Python 的功能以及类。打包功能的意义我觉得并不是很大, 但是能打包类还是比较有意思的。比如我有一个文件系统,我定义了一个 File 类,而且基于它生成了很多 file 实例。 这些实例都是可以被 pickle 的。
注意,在反序列化 unpickle 的时候,这个 File 的 class 一定要有,不然反序列化会因为找不到 File 类而失败的。 这比较好理解,就是按模具重组原件,你没有这个 File class 模具,pickle 的原件也没有一个模具能够组装起来。
class File:
def __init__(self, name, create_time, size):
self.name = name
self.create_time = create_time
self.size = size
def change_name(self, new_name):
self.name = new_name
data = File("f2.txt", "now", 222)
# 存
with open("data.pkl", "wb") as f:
pickle.dump(data, f)
# 读
with open("data.pkl", "rb") as f:
read_data = pickle.load(f)
print(read_data.name)
print(read_data.size)
#################
# 注意,可能是浏览器的原因,点击运行后会报错,但是在正常的 Python 执行环境中,
# 运行是没有问题的,运行结果如下:
#################
"""
f2.txt
222
"""
你看最后,unpickle 出来的东西还是一个 class 的实例。你可以按正常的 class 方式使用这个 unpickle 的东西。
有些类型的对象是不能被序列化的。这些通常是那些依赖外部系统状态的对象, 比如打开的文件,网络连接,线程,进程,栈帧等等。 如果你在 class 中把上述东西赋值到了 class 的属性上,比如下面的 self.file = open(),这样的 class 在 pickle 的时候会报错的。
如果你硬要用 pickle 保存,我们也还是有解决方案的。 用户自定义类可以通过提供 getstate() 和 setstate() 方法来绕过 pickle 的这些限制。 pickle.dump() 会调用 getstate() 获取序列化的对象。 setstate() 在反序列化时被调用。
class File:
def __init__(self, name, create_time, size):
self.name = name
self.create_time = create_time
self.size = size
self.file = open(name, "w")
def __getstate__(self):
# pickle 出去需要且能被 pickle 的信息
pickled = {"name": self.name, "create_time": self.create_time, "size": self.size}
return pickled
def __setstate__(self, pickled_dict):
# unpickle 加载回来,重组 class
self.__init__(
pickled_dict["name"], pickled_dict["create_time"], pickled_dict["size"])
data = File("f3.txt", "now", 222)
# 存
with open("data.pkl", "wb") as f:
pickle.dump(data, f)
# 读
with open("data.pkl", "rb") as f:
read_data = pickle.load(f)
print(read_data.name)
print(read_data.size)
#################
# 注意,可能是浏览器的原因,点击运行后会报错,
# 在正常的 Python 环境中,结果如下:
#################
"""
f3.txt
222
"""
不知道有什么用…先放着吧
Json
什么是 json 数据?一般来说,Python 里的字典,列表都可以是 json 数据格式。如果我们用 Python 里的 json 库来读一个 Python 字典, 会处理成什么样呢?
Pickle 和 Json 的不同
这里给老师提了个问:)