本篇包括:
- 文件和异常
- 字符串和正则表达式
四、文件和异常
在实际开发中,常常需要对程序中的数据进行持久化操作,而实现数据持久化最直接简单的方式就是将数据保存到文件中。说到“文件”这个词,可能需要先科普一下关于文件系统的知识,对于这个概念,维基百科上给出了很好的诠释,这里不再浪费笔墨。
在Python中实现文件的读写操作其实非常简单,通过Python内置的open
函数,我们可以指定文件名、操作模式、编码信息等来获得操作文件的对象,接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件(字符文件还是二进制文件)以及做什么样的操作(读、写还是追加),具体的如下表所示。
操作模式 | 具体含义 |
---|---|
'r' | 读取 (默认) |
'w' | 写入(会先截断之前的内容) |
'x' | 写入,如果文件已经存在会产生异常 |
'a' | 追加,将内容写入到已有文件的末尾 |
'b' | 二进制模式 |
't' | 文本模式(默认) |
'+' | 更新(既可以读又可以写) |
下面这张图来自于菜鸟教程网站,它展示了如何根据应用程序的需要来设置操作模式。
1、读写文本文件
读
读取文本文件时,需要在使用open
函数时指定好带路径的文件名(可以使用相对路径或绝对路径)并将文件模式设置为'r'
(如果不指定,默认值也是’r’),然后通过encoding
参数指定编码(如果不指定,默认值是None,那么在读取文件时使用的是操作系统默认的编码),如果不能保证保存文件时使用的编码方式与encoding
参数指定的编码方式是一致的,那么就可能因无法解码字符而导致读取失败。下面的例子演示了如何读取一个纯文本文件。
def main():
f = open('E:/sublime_python/test.txt', 'r', encoding='utf-8')
print(f.read())
f.close()
if __name__ == '__main__':
main()
(PS. 使用F5编译运行,使用ctrl+b会出现问题…)
如果open
函数指定的文件并不存在或者无法打开,那么将引发异常状况导致程序崩溃。为了让代码有一定的健壮性和容错性,我们可以使用Python的异常机制对可能在运行时发生状况的代码进行适当的处理,如下所示。
def main():
f = None
try:
f = open('E:/sublime_python/test.txt', 'r', encoding='utf-8')
print(f.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')
finally:
if f:
f.close()
if __name__ == '__main__':
main()
在Python中,我们可以将那些在运行时可能会出现状况的代码放在try
代码块中,在try
代码块的后面可以跟上一个或多个except
来捕获可能出现的异常状况,最后使用finally
代码块来关闭打开的文件,释放掉程序中获取的外部资源,由于 finally
块的代码不论程序正常还是异常都会执行到(甚至是调用了sys
模块的exit
函数退出Python环境,finally
块都会被执行,因为exit
函数实质上是引发了SystemExit
异常),因此我们通常把finally
块称为“总是执行代码块”,它最适合用来做释放外部资源的操作。如果不愿意在finally
代码块中关闭文件对象释放资源,也可以使用上下文语法,通过with
关键字指定文件对象的上下文环境并在离开上下文环境时自动释放文件资源,代码如下所示。
def main():
try:
with open('E:/sublime_python/test.txt', 'r', encoding='utf-8') as f:
print(f.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')
if __name__ == '__main__':
main()
除了使用文件对象的read
方法读取文件之外,还可以使用for-in
循环逐行读取或者用readlines
方法将文件按行读取到一个列表容器中,代码如下所示。
import time
def main():
# 一次性读取整个文件内容
with open('E:/sublime_python/test.txt', 'r', encoding='utf-8') as f:
print(f.read())
# 通过for-in循环逐行读取
with open('E:/sublime_python/test.txt', 'r', encoding='utf-8') as f:
for line in f:
print(line, end='')
time.sleep(0.5)
print()
# 读取文件按行读取到列表中
with open('E:/sublime_python/test.txt', 'r', encoding='utf-8') as f:
lines = f.readlines()
print(lines)
if __name__ == '__main__':
main()
写
将文本信息写入文件文件时,只需在open
函数中指定好文件名并将 文件模式设置为'w'
即可。如果需要对文件内容进行追加式写入,则将模式设置为'a'
。如果要写入的文件不存在则会自动创建文件而不是引发异常。下面的例子演示了如何将1-9999之间的素数分别写入三个文件中(1-99之间的素数保存在a.txt中,100-999之间的素数保存在b.txt中,1000-9999之间的素数保存在c.txt中)。
from math import sqrt
def is_prime(n):
"""判断是否为素数"""
assert n > 0
for factor in range(2, int(sqrt(n)) + 1):
if n % factor == 0:
return False
return True if n != 1 else False
def main():
filenames = ('a.txt', 'b.txt', 'c.txt')
fs_list = [] # 用来保存打开了的三个文本文件
try:
for filename in filenames:
fs_list.append(open(filename, 'w', encoding='utf-8'))
for number in range(1, 10000):
if is_prime(number): # 如果是素数的话,就分类保存进相应的文件中
if number < 100:
fs_list[0].write(str(number) + '\n')
elif number < 1000:
fs_list[1].write(str(number) + '\n')
else:
fs_list[2].write(str(number) + '\n')
except IOError as ex:
print(ex)
print('写文件时发生错误!')
finally:
for fs in fs_list:
fs.close()
print('操作完成!')
if __name__ == '__main__':
main()
(碎碎念:这样看来,用起来比Jav简单很多呀,Java有各种各样的流…)
2、读写二进制文件
知道了如何读写文本文件要读写二进制文件也就很简单了,下面的代码实现了复制图片文件的功能。
def main():
try:
with open('E:/fdj.png', 'rb') as fs1:
data = fs1.read()
print(type(data)) # <class 'bytes'>
with open('E:/sublime_python/IMG_2630.JPG', 'wb') as fs2:
fs2.write(data)
# 把fdj.png图片复制到E:/sublime_python/IMG_2630.JPG图片了,
# 也就是说IMG_2630.JPG图片变成了fdj.png图片,但两者的文件名都没变
except FileNotFoundError as e:
print('文件无法打开!')
except IOError as e:
print('读写文件时错误!')
print('程序执行结束')
if __name__ == '__main__':
main()
读写JSON文件
如果希望把一个列表或者一个字典中的数据保存到文件中该怎么做呢?答案是将数据以JSON格式进行保存。JSON是“JavaScript Object Notation”的缩写,它本来是JavaScript语言中创建对象的一种字面量语法,现在已经被广泛的应用于跨平台跨语言的数据交换,原因很简单,因为JSON也是纯文本,任何系统任何编程语言处理纯文本都是没有问题的。目前JSON基本上已经取代了XML作为异构系统间交换数据的事实标准。关于JSON的知识,更多的可以参考JSON的官方网站,从这个网站也可以了解到每种语言处理JSON数据格式可以使用的工具或三方库,下面是一个JSON的简单例子。
{
"name": "骆昊",
"age": 38,
"qq": 957658,
"friends": ["王大锤", "白元芳"],
"cars": [
{"brand": "BYD", "max_speed": 180},
{"brand": "Audi", "max_speed": 280},
{"brand": "Benz", "max_speed": 320}
]
}
可能大家已经注意到了,上面的JSON跟Python中的字典其实是一样一样的,事实上JSON的数据类型和Python的数据类型是很容易找到对应关系的,如下面两张表所示。
我们使用Python中的json
模块就可以将字典或列表以JSON格式保存到文件中,代码如下所示。
import json
def main():
mydict = {
'name': '朱俊艳',
'age': 18,
'qq': 123456,
'friends': ['张三', '李四'],
'cars': [
{'brand': 'BYD', 'max_speed': 180},
{'brand': 'Audi', 'max_speed': 280},
{'brand': 'Benz', 'max_speed': 320}
]
}
try:
with open('data.json', 'w', encoding='utf-8') as fs:
json.dump(mydict, fs) # 关键方法
except IOError as e:
print(e)
print('保存数据完成!')
if __name__ == '__main__':
main()
json
模块主要有四个比较重要的函数,分别是:
dump
- 将Python对象按照JSON格式序列化到文件中dumps
- 将Python对象处理成JSON格式的字符串load
- 将文件中的JSON数据反序列化成对象loads
- 将字符串的内容反序列化成Python对象
这里出现了两个概念,一个叫序列化,一个叫反序列化。自由的百科全书维基百科上对这两个概念是这样解释的:“序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换为可以存储或传输的形式,这样在需要的时候能够恢复到原先的状态,而且通过序列化的数据重新获取字节时,可以利用这些字节来产生原始对象的副本(拷贝)。与这个过程相反的动作,即从一系列字节中提取数据结构的操作,就是反序列化(deserialization)”。
在Python中要实现序列化和反序列化除了使用json
模块之外,还可以使用pickle
和shelve
模块,但是这两个模块是使用特有的序列化协议来序列化数据,因此序列化后的数据只能被Python识别。关于这两个模块的相关知识可以自己看看网络上的资料。另外,如果要了解更多的关于Python异常机制的知识,可以看看segmentfault上面的文章《总结:Python中的异常处理》,这篇文章不仅介绍了Python中异常机制的使用,还总结了一系列的最佳实践,很值得一读。
五、字符串和正则表达式
1、正则表达式相关知识
在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要,正则表达式就是用于描述这些规则的工具,换句话说正则表达式是一种工具,它定义了字符串的匹配模式(如何检查一个字符串是否有跟某种模式匹配的部分或者从一个字符串中将与模式匹配的部分提取出来或者替换掉)。如果你在Windows操作系统中使用过文件查找并且在指定文件名时使用过通配符(*和?),那么正则表达式也是与之类似的用来进行文本匹配的工具,只不过比起通配符正则表达式更强大,它能更精确地描述你的需求(当然你付出的代价是书写一个正则表达式比打出一个通配符要复杂得多),比如你可以编写一个正则表达式,用来查找所有以0开头,后面跟着2-3个数字,然后是一个连字号“-”,最后是7或8位数字的字符串(像028-12345678或0813-7654321),这不就是国内的座机号码吗。最初计算机是为了做数学运算而诞生的,处理的信息基本上都是数值,而今天我们在日常工作中处理的信息基本上都是文本数据,我们希望计算机能够识别和处理符合某些模式的文本,正则表达式就显得非常重要了。 今天几乎所有的编程语言都提供了对正则表达式操作的支持,Python通过标准库中的re
模块来支持正则表达式操作。
我们可以考虑下面一个问题:我们从某个地方(可能是一个文本文件,也可能是网络上的一则新闻)获得了一个字符串,希望在字符串中找出手机号和座机号。当然我们可以设定手机号是11位的数字(注意并不是随机的11位数字,因为你没有见过“25012345678”这样的手机号吧)而座机号跟上一段中描述的模式相同,如果不使用正则表达式要完成这个任务就会很麻烦。
2、Python对正则表达式的支持
几个实例
Python提供了re
模块来支持正则表达式相关操作,下面根据实例进行学习。
- 实例0:几个简单的例子
- \bhi\b:精确查找单词“hi”
\b
: 用来匹配单词的边界;是正则表达式规定的一个特殊代码(某些人叫它元字符,metacharacter),代表着单词的开头或结尾,也就是单词的分界处。
- \bhi\b.*\bLucy\b:hi后面不远处跟着一个Lucy
.
:元字符,一个点代表一个任意字符(换行符除外),两个点代表两个,以此类推*
:元字符,匹配0次或多次其前面的那个字符
- 0\d\d-\d\d\d\d\d\d\d\d 或 0\d{2}-\d{8}:以0开头,然后是两个数字,然后是一个连字号“-”,最后是8个数字
\d
:元字符,用来匹配一个数字字符,即0 ~ 9之间的某个数字-
:不是元字符,只匹配它本身{N}
:匹配其前面的那个字符N次
- 实例1:验证输入用户名和QQ号是否有效并给出对应的提示信息
import re
def main():
username = input('请输入用户名: ')
qq = input('请输入QQ号:')
# match函数的第一个参数是正则表达式字符串或正则表达式对象
# 第二个参数是要跟正则表达式做匹配的字符串对象
m1 = re.match(r'^[0-9a-zA-Z_]{6,20}$', username)
if not m1:
print('用户名有误!')
m2 = re.match(r'^[1-9]\d{4,11}$', qq)
if not m2:
print('QQ号有误!')
if m1 and m2:
print('您输入的信息是有效的!')
if __name__ == '__main__':
main()
这里是对上面程序正则表达式部分的说明:
- 上面在书写正则表达式时使用了“原始字符串”的写法(在字符串前面加上了r),所谓“原始字符串”就是字符串中的每个字符都是它原始的意义,说得更直接一点就是字符串中没有所谓的转义字符啦。因为正则表达式中有很多元字符和需要进行转义的地方,如果不使用原始字符串就需要将反斜杠写作\,例如表示数字的\d得书写成\d,这样不仅写起来不方便,阅读的时候也会很吃力。
- ^ [0-9a-zA-Z_]{6,20}$:
^
:匹配字符串的开始$
:匹配字符串的结束
说明:^
匹配你要用来查找的字符串的开头,$
匹配结尾。这两个代码在验证输入的内容时非常有用,比如一个网站如果要求你填写的QQ号必须为5位到12位数字时,可以使用: ^\d{5,12} $(如果不使用^ 和 $ 的话,对于\d{5,12}而言,使用这样的方法就只能保证字符串里包含5到12连续位数字,而不是整个字符串就是5到12位数字)。[]
:匹配来自[]
内的字符集的任意单一字符[^]
:匹配不在[^]
内的字符集中的任意单一字符
eg:
[0-9] 所判断的单个字符在0~9的之间时, 为true;
[abc] :所判断的单个字符是a或b或c时,为true;
[^abc] :所判断的单个字符不是a或b或c时,为true;
[a-zA-Z] 所判断的单个字符在 a到 z 或 A到 Z(包括两端的字符) 之间时,为true;
[a-d[m-p]] 所判断的单个字符在a-d之间 或 m-p之间时,为true。即[a-dm-p]
[a-z&&[def]] 所判断的单个字符在a-z之间 且 为def时,为true。即[def]
[a-z&&[^def]]所判断的单个字符在a-z之间 且 不 为def时,为true。
[a-z&&[^m-p]]所判断的单个字符属于a-z、不属于m-p时,为true。即[a-lq-z]_
:就表示下划线{M,N}
: 匹配其前面的那个字符至少M次至多N次
- 实例2:从一段文字中提取出国内手机号码
import re
def main():
# 创建正则表达式对象(下面的正则使用了前瞻和回顾来保证手机号前后不应该出现数字)
pattern = re.compile(r'(?<=\D)1[34578]\d{9}(?=\D)')
sentence = '重要的事情说8130123456789遍,\
我的手机号是13512346789这个靓号,\
不是15600998765,也是110或119,\
王大锤的手机号才是15600998765。'
# 查找所有匹配并保存到一个列表中,然后输出
mylist = re.findall(pattern, sentence)
print(mylist)
print('------------------------')
# 通过迭代器取出匹配对象并获得匹配的内容
for temp in pattern.finditer(sentence):
print(temp.group())
print('------------------------')
# 通过search函数指定搜索位置找出所有匹配
m = pattern.search(sentence)
while m:
print(m.group())
m = pattern.search(sentence, m.end())
if __name__ == '__main__':
main()
这里是对上面程序的一些说明:
- 上面匹配国内手机号的正则表达式并不够好,因为像14开头的号码只有145或147,而上面的正则表达式并没有考虑这种情况,要匹配国内手机号,更好的正则表达式的写法是:(?<=\D)(1[38]\d{9}|14[57]\d{8}|15[0-35-9]\d{8}|17[678]\d{8})(?=\D),国内最近好像有19和16开头的手机号了,但是这个暂时不在我们考虑之列。
- (?<=\D)1[34578]\d{9}(?=\D):
\D
:匹配非数字的单个字符,即不是0 ~ 9中的数字(大写的转义字符匹配的内容总是与小写的相反)(?<=exp)
:匹配的字符(串)是exp后面的那些
eg:
(?<=\bdanc)\w+\b 可以匹配I love dancing and reading中的第一个ing\w
:只能匹配字母或数字或下划线三类字符,等同于==[a-z0-9A-Z_]==(?=exp)
:匹配的字符(串)是exp前面的那些
eg:
\b\w+(?=ing) 可以匹配I’m dancing中的danc
- 实例3:替换字符串中的不良内容
import re
def main():
sentence = '你丫是傻叉吗? 我操你大爷的. Fuck you.'
purified = re.sub('[操肏艹]|fuck|shit|傻[比屄逼叉缺吊屌]|煞笔',
'*', sentence, flags=re.IGNORECASE)
print(purified)
if __name__ == '__main__':
main()
这里是对上面程序的一些说明:
re模块的正则表达式相关函数中都有一个flags参数,它代表了正则表达式的匹配标记,可以通过该标记来指定匹配时是否忽略大小写、是否进行多行匹配、是否显示调试信息等。如果需要为flags参数指定多个值,可以使用按位或运算符进行叠加,如flags=re.I | re.M
(这两者的意思即上面程序中用到的re的方法请参考下面“re模块中核心函数总结”一节)。
- 实例4:拆分长字符串
import re
def main():
poem = '窗前明月光,疑是地上霜。举头望明月,低头思故乡。'
sentence_list = re.split(r'[,.,。]', poem)
# print(sentence_list) # ['窗前明月光', '疑是地上霜', '举头望明月', '低头思故乡', '']
while '' in sentence_list:
sentence_list.remove('')
print(sentence_list) # ['窗前明月光', '疑是地上霜', '举头望明月', '低头思故乡']
if __name__ == '__main__':
main()
- 实例5:再看几个简单的例子
- \ba\w*\b:匹配以字母a开头的单词——先是某个单词开始处(\b),然后是字母a,然后是任意数量的字母或数字(\w*),最后是单词结束处(\b)。
- \d+:匹配1个或更多连续的数字
+
:元字符,匹配1次或多次其前面的那个字符
- \b\w{6}\b:匹配刚好6个字符的单词
- \ (?0\d{2}[) -]?\d{8}:首先是一个转义字符
\(
,它能出现0次或1次(?),然后是一个0,后面跟着2个数字(\d{2}),然后是)或-或空格中的一个,它出现1次或不出现(?),最后是8个数字(\d{8})。如:(010)88886666,或022-22334455,或02912345678等。
?
:匹配0次或1次
- 1)0\d{2}-\d{8}|0\d{3}-\d{7} 这个表达式能匹配两种以连字号分隔的电话号码:一种是三位区号,8位本地号(如010-12345678),一种是4位区号,7位本地号(0376-2233445)。
2)(0\d{2})[- ]?\d{8}|0\d{2}[- ]?\d{8} 这个表达式匹配3位区号的电话号码,其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔,也可以没有间隔。你可以试试用分枝条件把这个表达式扩展成也支持4位区号的。
3)\d{5}-\d{4}|\d{5} 这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字,或者用连字号间隔的9位数字。之所以要给出这个例子是因为它能说明一个问题:使用分枝条件时,要注意各个条件的顺序。如果你把它改成\d{5}|\d{5}-\d{4}的话,那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。
|
:正则表达式里的分支条件
正则表达式总结
先说明一些可能遇到的问题:
如果你想查找元字符本身的话,比如你查找.,或者*,就出现了问题:你没办法指定它们,因为它们会被解释成别的意思。这时你就得使用\来取消这些字符的特殊意义。因此,你应该使用.和*。当然,要查找\本身,你也得用\。例如:deerchao. net匹配deerchao. net,C:\Windows匹配C:\Windows。
通过上面实例的学习,进行总结:
- 常用的元字符
- 常用的限定符
例子:
- Windows\d+ 匹配Windows后面跟1个或更多数字
- ^\w+ 匹配一行的第一个单词(或整个字符串的第一个单词,具体匹配哪个意思得看选项设置)
说明:
如果重复单个字符,可以使用上面的限定符;那么,如果想要重复多个字符又该怎么办?答案是使用小括号来指定子表达式(也叫做分组),然后指定这个子表达式的重复次数,另外,还可以对子表达式进行其它一些操作(后面会有介绍)。实例:
(\d{1,3}.){3}\d{1,3} 是一个简单的IP地址匹配表达式。要理解这个表达式,请按下列顺序分析它:\d{1,3} 匹配1到3位的数字,(\d{1,3}.){3} 匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,最后再加上一个一到三位的数字 (\d{1,3}) 。不幸的是,它也将匹配256.300.888.999这种不可能存在的IP地址。如果能使用算术比较的话,或许能简单地解决这个问题,但是正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个正确的IP地址:((2[0-4]\d|25[0-5]|[01]?\d\d?).){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)。
- 常用的反义代码
例子:
- \S+ 匹配不包含空白符的字符串。
- <a[^>]+> 匹配用尖括号括起来的以a开头的字符串。
- 其他更多更深入的总结请自行看这里吧,因为目前还不需要了解太多,就算学习了某日也不再记得。。。另外我也还不想学习,想学习后面的。。。所以到时候再学习吧。这个网站我学习到了《反义》这一节,再看时应该从《后向引用》开始认真的学习。
re模块中核心函数总结
后话
如果要从事爬虫类应用的开发,那么正则表达式一定是一个非常好的助手,因为它可以帮助我们迅速的从网页代码中发现某种我们指定的模式并提取出我们需要的信息,当然对于初学者来收,要编写一个正确的适当的正则表达式可能并不是一件容易的事情(当然有些常用的正则表达式可以直接在网上找找),所以实际开发爬虫应用的时候,有很多人会选择Beautiful Soup或Lxml来进行匹配和信息的提取,前者简单方便但是性能较差,后者既好用性能也好,但是安装稍嫌麻烦,这些内容我们会在后期的爬虫专题中为大家介绍。