第5章 对象带你飞

第5章 对象带你飞

5.1 存储

1. 文件

Python中的数据都保存在内存中,当电脑断电时,内存中的数据就会消失。另一方面,如果Python程序运行结束,那么分配给这个程序的内存空间也会清空。为了长期持续地存储,Python必须把数据存储在磁盘中。

磁盘以文件为单位来存储数据。如果以字节为单位,也就是每8位二进制数序列为单位,那么这个数据序列就称为文本。这是因为,8位二进制数序列正好对应ASCII编码中的一个字符。而Python能够借助文本对象来读写文件。

在Python中,我们可以通过内置函数open来创建文件对象。在调用open时,需要说明文件名,以及打开文件的方式

f = open(文件名、打开文件的方式)

文件名是文件存在于磁盘的名字,打开文件的常用方式有:

"r"	# 读取已经存在的文件
"w"	# 新建文件,并写入
"a"	# 如果文件存在,那么写入到文件的结尾。如果文件不存在,则新建文件并写入

例如:

f = open("test.txt", "r")

就是用只读的方式,打开了一个名为test.txt的文件。

通过上面返回的对象,我们可以读取文件:

content = f.read(10)	# 读取10个字节的数据
content = f.readline()	# 读取一行
content = f.readlines()	# 读取所有行,储存在列表中,每行是一个元素。

如果以"w"或"a"方式打开,那么我们可以写入文本:

f = open("test.txt", "w")
f.write("I like apple")	# 将"I like aplle"写入文件,如果文件中原来有内容,那么会覆盖文件的原内容。

如果想写入一行,则需要在字符串末尾加上换行符。在UNIX系统中,换行符为"\n"。在Windows系统中,换行符为"\r\n"。

f.write("I like apple\n")	# UNIX
f.write("I like apple\r\n")	# Windows,实际上,在我的win环境下,追加的内容前多了个空行

打开文件端口将占用计算机资源,因此,在读写完成后,应该及时的用文件对象的close方法关闭文件:

f.close()

2. 上下文管理器

文件操作常常和上下文管理器一起使用。上下文管理器(context manager)用于规定某个对象的使用范围。一旦进入或者离开该使用范围,则会有特殊操作被调用,比如为对象分配或释放内存。上下文管理器可用于文件操作。对于文件操作来说,我们需要在读写结束时关闭文件。程序员经常会忘记关闭文件,无谓的占用资源。上下文管理器可以在不需要文件的时候,自动关闭文件。

下面是一段常规的文件操作:

# 常规文件操作
f = open("new.txt", "w")
print(f.closed)	#检查文件是否打开
f.write("Hello World!")
f.close()

print(f.closed)	# 检查文件是否打开,打印True

如果我们加入上下文管理器的语法,就可以把程序改写为:

# 使用上下文管理器
with open("new.txt", "w") as f:
    f.write("Hello World!")
    
print(f.closed)

第二段程序就使用了with…as…结构。上下文管理器有隶属于它的程序块,当隶属的程序块执行结束时,也就是语句不再缩进时,上下文管理器就会自动关闭文件。在程序中,我们调用了f.closed属性来验证是否已经关闭。通过上下文管理器,我相当于用缩进来表达文件对象的打开范围。对于复杂的程序来说,缩进的存在能让程序员更清楚的意识到文件在哪些阶段打开,减少忘记关闭文件的可能性。

上面的上下文管理器基于f对象的__exit__()特殊方法。使用上下文管理器的语法时,Python会在进入程序块之前调用文件对象的__enter__()方法,在结束程序块的时候调用文件对象的__exit__()语句。在文件对象的__exit__()方法中,有self.close()语句。因此,在使用上下文管理器时,我们就不用明文关闭文件了。

任何定义了__enter__()方法和__exit__()方法的对象都可以用于上下文管理器。下面,我们自定义一个类Vow,并定义它的__enter__()方法和__exit__()方法。因此,由Vow类创建的对象可以用于上下文管理器:

class Vow(object):
    def __init__(self, text):
        self.text = text
    def __enter__(self):
        self.text = "I say: " + self.text	# 增加前缀
        return self	# 返回一个对象
    def __exit__(self, exc_type, exc_value, traceback):
        self.text = self.text + "!"
        
with Vow("I'm fine") as myVow:
    print(myVow.text)
    
print(myVow.text)    

运行结果如下:

I say: I'm fine

I say: I'm fine!

初始化对象时,对象的text属性是“I’m fine”。我们可以看到,在进入上下文和离开上下文时,对象调用了__enter__()方法和__exit__()方法,从而造成对象的text属性改变。

__enter__()返回一个对象。上下文管理器会使用这一对象作为as所指的变量。我们自定义的__enter__()返回的是self,也就是新建的Vow类对象本身。在__enter__()中,我们为text属性增加了前缀“I say: ”。在__exit__()中,我们为text属性增加了后缀"!"。

值得注意的是,__exit__()有四个参数。当程序块中出现异常时,__exit__()参数中的exc_typeexc_valuetraceback用于描述异常。我们可以根据这三个参数进行相应的处理。如果正常运行结束,则这三个参数都是None。

3. pickle包

我们能把文本存于文件。但Python中最常见的是对象,当程序结束或计算机关闭时,这些存在于奶茶的对象会消失。那么,我们能否把对象保存到磁盘上呢?

利用Python的pickle包就可以做到这一点。通过pickle包,我们可以把某个对象保存下来,再存成磁盘里的文件

**实际上,对象的存储分为两步。第一步,我们将对象在内存中的数据直接抓取出来,转换成一个有序地文本,即所谓的序列化(Serialization)。第二步,将文本存入文件。等到需要时,我们从文件中读出文本,再放入内存,就可以获得原有的对象。**下面是一个具体的例子,首先第一步是序列化,将内存中的对象转换为文本流:

import pickle

class Bird(object):
    have_father = True
    reproduction_method = "egg"
    
summer = Bird()		# 创建对象
pickle_string = pickle.dumps(summer)	# 序列化对象

**使用pickle包的dumps()方法可以将对象转换成字符串的形式。**随后我们用字节文本的存储方法,将该字符串存储在文件。继续第二步:

with open("summer.pkl", "wb") as f:
    f.write(pickle_string)

上面程序故意分成了两步,以便更好地展示整个过程。其实,我们可以使用dump()方法,一次完成两步:

import pickle

class Bird(object):
    have_feather = True
    reproduction_method = "egg"
    
summer = Bird()
with open("summer.pkl", "w") as f:
    pickle.dump(summer, f)	# 序列化并保存对象

对象summer将存储在文件summer.pkl中。有了这个文件,我们就可以在必要的时候读取对象了。读取对象与存储对象的过程正好相反。首先,我们从文件中读出文本。然后使用pickle的loads()方法,将字符串形式的文本转换为对象。我们也可以使用pickle的load()方法,将上面两步合并。

有时候,仅仅是反向恢复还不够。**对象依赖于它的类,所以Python在创建对象时,需要找到相应的类。因此当我们从文本中读取对象时,程序中必须已经定义过类。对于Python总是存在的内置类,例如列表、词典、字符串等,不需要在程序中定义。但对于用户自定义的类,就必须要先定义类,然后才能从文件中载入该类的对象。**下面是一个读取对象的例子:

import pickle

class Bird(object):
    have_feather = True
    reproduction_method = "egg"
    
with open("summer.pkl", "rb") as f:
    summer = pickle.load(f)
    
print(summer.have_feather)	# 打印True    

5.2 一寸光阴

1. time包

挂钟时间(Wall Clock Time)

计算机还可以测量CPU实际运行的时间,也就是处理器时间(Processor Clock Time),以测量实际性能。当CPU处于闲置状态时,处理器时间会暂停。

我们能通过Python编程来管理时间和日期。标准库time包提供了基本的时间功能。下面使用time包:

import time
print(time.time())	# 挂钟时间,单位是秒

还能借助模块time测量程序的运行时间,比如:

import time
start = time.clock()
for i in range(100000):
    print(i**2)
    
end = time.clock()
print(end - start)

上面的程序调用了两次clock()方法,从而测量出镶嵌其间的程序所用的时间。在不同的计算机系统上,clock的返回值会有所不同。在UNIX系统上,返回的是处理器时间。当CPU处于闲置状态时,处理器时间会暂停。因此,我们获得的是CPU运行时间。在Windows系统上,返回的则是挂钟时间。

time.clock()在3.8被移除了

可以用

time.perf_counter()返回系统运行时间

time.process_time()返回进程运行时间

import time

start = time.perf_counter()

for i in range(100000):
    print(i**2)
    
end = time.perf_counter()

print(end - start)

方法sleep()可以让程序休眠。根据sleep()接受到的参数,程序会在某时间间隔之后醒来继续运行:

import time

print("start")
time.sleep(10)	# 休眠10秒
print("wake up")

time包还定义了struct_time对象。该对象将挂钟时间转换成年、月、日、时、分、秒等,存储在该对象的各个属性中,比如tm_yeartime_montime_mday下面几种方法可以将挂钟时间转换为struct_time对象:

st = time.gmtime	# 返回struct_time格式的UTC时间
st = time.localtime()	# 返回struct_time格式的当地时间,当地失去根据系统环境决定

我们也可以反过来,把一个struct_time对象转换为time对象:

s = time.mktime(st)将struct_time格式转换成挂钟时间

2. datetime包

datetime包是基于time包的一个高级包。datetime可以理解为由date和time两个部分组成。date是指年、月、日构成的日期,相当于日历。time是指时、分、秒、毫秒构成的一天24小时中的具体时间,提供了与手表类似的功能。因此,datetime模块下有两个类: datetime.date类和datetime.time类。你也可以把日历和手表合在一起使用,即直接调用datetime.datetime类。

一个时间点,比如2012年9月3日21时30分,我们可以用如下方式表达:

import datetime
   
t = datetime.datetime(2012, 9, 3, 21, 30)
print(t)

对象t有如下属性:

hour, minute, second, millisecond, microsecond:小时、分、秒、毫秒、微秒

year, month, day, weekday:年、月、日、星期几

借助datetime包,我们还可以进行时间间隔的运算。它包含一个专门代表时间间隔对象的类,即timedelta。一个datetime.datetime的时间点加上一个时间间隔,就可以得到一个新的时间点。比如今天的上午3点加上5个小时,就可以得到今天的上午8点。同理,两个时间点相减可以得到一个时间间隔。

import time

t		= datetime.dateime(2012, 9, 3, 21, 30)
t_next	= datetime.datetime(2012, 9, 5, 23, 30)
delta1	= datetime.timedelta(seconds = 600)
delta2	= datetime.timedelta(weeks = 3)

print(t + delta1)	# 打印2012-9-3 21:40:00
print(t + delta2)	# 打印2012-9-24 21:30:00
print(t_next - t)	# 打印2 days, 2:00:00

在给datetime.timedelta传递参数时,除了上面的秒(seconds)和星期(weeks)外,还可以是天(days),小时(hours),毫秒(milliseconds)、微秒(microseconds)等。

两个datetime对象能进行比较运算,以确定那个时间间隔更长。比如使用上面的t和t_next:

print(t > t_next) # 打印False

3. 日期格式

对于包含有时间信息的字符串来说,我们可以借助datetime包,把它转换成datetime类的对象,比如:

from datetime import datetime

str = "output-1997-12-23-030000.txt"

format = "output-%Y-%m-%d-%H%M%S.txt"

t	= datetime.strptime(str, format)
print(t)

包含有时间信息的字符串是"output-1997-12-23-030000.txt",是一个文件名。字符串format定义了一个格式。这个格式中包含了几个由%引领的特殊字符,用来代表不同的时间信息。%Y表示年份,%m表示月,%d表示日、%H表示24小时制的小时,%M表示分,%S表示秒。通过strptime方法,Python会把需要解析的字符串王格式上凑。比如说,在%Y的位置,正好看到1997,就认为1997是对象t的年。以此类推,就从字符串中获得了t对象的时间信息。

反过来,我们也可以调用datetime对象的strftime方法,将一个datetime对象转换为特定格式的字符串,比如:

from datetime import datetime
format = '%Y-%m-%d %H:%M'
t = datetime(2012, 9, 5, 23, 30)
print(t.strftime(format))	# 打印2012-09-05 23:30

可以看到,格式化转换的关键是%号引领的特殊符号。这些特殊符号有很多种,分别代表不同的时间信息。常用的特殊符号还有:

%A: 表示英文的星期几,如;Sunday、Monday
%a: 简写的英文星期几,如Sun、Mon...
%I: 表示小时,12小时制
%p: 上午或下午,即AM或PM
%f: 表示毫秒,如22014000001

如果想在格式中表达%这个字符本身,而不是特殊符号,那么可以使用%%。

5.3 看起来像那样的东西

1. 正则表达式

Python中可以使用包re来处理正则表达式。

找到字符串中的数字:

import re
m = re.search("[0-9],"abcd4ef")
print(m.group(0))          

re.search()接收两个参数,第一个参数"[0-9]"正则表达式,表示从字符串中找从0-9的任意一个数字字符。

re.search()如果从第二个参数中找到符合要求的子字符串,就返回一个对象m,你可以通过m.group()的方法来查看搜索到的结果。如果没有找到符合要求的字符串,则re.search()会返回None。

除了search()方法外,re包还提供了其他搜索方法,它们的功能有所差别。

m = re.search(pattern, string)	# 搜索整个字符串,直到发现符合的子字符串
m = re.match(pattern, string)	# 从头开始检查字符串是否符合正则表达式,必须从字符串的第一个字符开始就相符

我们可以从这两个函数中选择一个进行搜索。在上面的例子中,如果使用re.match()的话,则会得到None,因为字符串的起始为"a",不符合"[0-9]"的要求。再一次,我们可以使用m.group()来查看找到的字符串。

我们还可以在搜索之后将搜索到的子字符串进行替换。下面的sub()利用正则表达式pattern在字符串string中进行搜索。对于搜索到的字符串,用另一个字符串replacement进行替换。函数将返回替换后的字符串:

str = re.sub(pattern, replacement, string)

此外,常用的方法还有

re.split()	# 根据正则表达式分割字符串,将分割后的所有子字符串放在一个表(list)中返回
re.findall()	# 根据正则表达式搜索字符串,将所有符合条件的子字符串放在一个表(list)中返回

2. 写一个正则表达式

正则表达式可视化及调试

正则表达式的常用语法。正则表达式用某些符号代表单个字符:

.		# 任意的一个字符
a|b		# 字符a或字符b
[afg]	# a或者f或者g的一个字符
[0-4]	# 0-4范围内的一个字符
[a-f]	# a-f范围内的一个字符
[^m]	# 不是m的一个字符
\s		# 一个空格
\S		# 一个非空格
\d		# 一个数字,相当于[0-9]
\D		# 一个非数字,相当于[^0-9]
\w		# 数字或字母,相当于[0-9a-zA-Z]
\W		# 非数字非字符,相当于[^0-9a-zA-Z]

正则表达式还可以用某些符号来表示某种形式的重复,这些符号紧跟在单个字符之后,就表示多个这样类似的字符:

*	# 重复超过0次或更多次
+	# 重复1次或超过1次
?	# 重复0次或1次
{m}	# 重复m此。比如,a{4}相当于aaaa,再比如,[1-3]{2}相当于[1-3][1-3]
{m, n}	# 重复m到n次。比如a{2, 5}表示a重复2到5次。小于m次的重复,或者大于n此的重复都不符合条件

下面是重复符号的例子:

正则表达式相符的字符串举例不相符的字符串举例
[0-9]{3, 5}“9678”“12”,“1234567”
a?b“b”,“ab”“cb”
a+b“ab”,“aaaaaab”“b”

最后,还有位置相关的符号:

^	# 字符串的起始位置
$	# 字符串的结尾位置

下面是位置符号的一些例子:

正则表达式相符的字符串举例不相符的字符串举例
^ab.*c$abeeccabeecc

3. 进一步提取

有的时候,我们想在搜索的同时,对结果进一步提炼。比如说,我们从下面的一个字符串中提取信息:

content = "abcd_output_1994_abcd_1912_abcd"

如果我们把正则表达式写成:

"output_\d{4}"

那么用search()方法可以找到"output_1994"。但如果我们想进一步提取出1994本身,则可以在正则表达式上给目标加上括号:

output_(\d{4})

括号()包围了一个小的正则表达式\d{4}。这个小的正则表达式能从结果中进一步筛选信息,即4位阿拉伯数字。用括号()圈起来的正则表达式的一部分,称为群(group)。一个正则表达式中可以有多个群。

我们可以group(number)的方法来查询群。需要注意的是,group(0)是整个正则表达式的搜索结果。group(1)是第一个群,以此类推:

import re

m = re.search("output_(\d{4})", "output_1986.txt")
print(m.group(1))	# 将找到的4个数字组成的1986

我们还可以将群命名,以便更好地使用group查询:

import re

m = re.search("output_(?P<year>\d{4})", "output_1986.txt")	# (?P<name>...) 为group命名
print(m.group("year"))	# 打印1986

上面的(?P<year>...)括住了一个群,并把它命名为year。用这种方式来产生群,就可以通过"year"这个键来提取结果。

5.4 Python有网瘾

1. HTTP协议

mozilla开发者手册

HTTP教程

2. http.client包

Python标准库中的http.client包可用于发出HTTP请求。在上一节中,我们已将看到,**HTTP请求最重要的一些信息是主机地址、请求方法和资源路径。**只要明确这些信息,再加上http.client包的帮助,就可以发出HTTP请求了。

import http.client

conn = http.client.HTTPConnection("www.baidu.com")   # 主机地址
conn.request("GET", "/")    # 请求方法和资源路径/
response = conn.getresponse()   # 获得回复

print(response.status, response.reason) # 回复的状态码和状态描述
content = response.read()   # 回复的主体内容
print(content)

如果网络正常,那么上面的程序将访问网址,并获得对应位置的超文本文件。

https://infoinsecu.wordpress.com/

5.5 写一个爬虫

网络爬虫。这里,我们让爬虫访问笔者的博客首页,提取出最近文章的发表日期和阅读量。

第一步当然是访问博客首页,获得首页的内容。根据5.4节的内容。笔者的博客地址是www.cnblogs.com/vamei,主机地址是www.cnblogs.com,资源位置是/vamei。这个页面是一个超文本文件,所以我们用HTTP协议访问:

import http.client

conn = http.client.HTTPConnection("www.cnblogs.com")	# 主机地址
conn.request("GET", "/vamei")	# 请求方法和资源路径
response = conn.getresponse()	# 获得回复

content = response.read()	# 回复的主体内容
content = content.split("\r\n")	# 分割成行

这里的content是列表,列表的每一个元素都是超文本的一行。对于我们所关心的信息来说,它们存在的行看起来是下面的样子:

我们想要的信息,如2014-08-12 20:55,以及阅读量6221镶嵌在一串文字中。要想提取出类似这样的信息,可以用正则表达式:

import re

pattern = "posted @ (\d{4}-[0-1]\d-{0-3}\d [0-2]\d:[0-6]\d) Vamei 阅读\((\d+)\) 评论"

for line in content:
    m = re.search(pattern, line)
    if m != None:
        print(m.group(1), m.group(2))

把两端程序合在一起,将打印出结果如下:

根据本章的内容,你还可以把日期转换成日期对象,进行更复杂的操作,如查询文章是星期几发表的。你还可以把上面的内容写入文件,长久的保存起来。

因为笔者的博客已经使用https协议了,所以这里我们选择另一位大佬的博客:http://www.harmj0y.net/blog/

主机名是:www.harmj0y.net

资源路径是:/blog/

我们想获得发布日期和作者

Debug版

import http.client

conn = http.client.HTTPConnection("www.harmj0y.net")     # 主机地址
conn.request("GET", "/blog/")   # 请求方法和资源路径
response = conn.getresponse()   # 获得回复

content = response.read()   # 回复的主体内容
print(type(content))    # <class 'bytes'>
content = content.decode()  # 解码
print(type(content))    # <class 'str'>
print(content)  # 标准格式的HTML页面
content = content.split("\n")     # 分割成行,每行是列表中的一个元素
print(content)
print(type(content))    # <class 'list'>

字节流bytes解码之后,得到的是str,形式上就是标准的HTML页面格式。用split("\n"),以换行符作为分隔符,将一个HTML页面的一行作为一个元素,存入列表中。

正式版

import http.client

conn = http.client.HTTPConnection("www.harmj0y.net")     # 主机地址
conn.request("GET", "/blog/")   # 请求方法和资源路径
response = conn.getresponse()   # 获得回复

content = response.read()   # 回复的主体内容
content = content.decode()  # 解码
content = content.split("\n")     # 分割成行,每行是列表中的一个元素

这里的content是列表,列表的每个元素是超文本的一行。对于我们关心的信息,它们长这样:

Published February 28, 2019 by harmj0y

匹配上述文本的正则表达式是

Published ([A-Za-z]{3,10} [0-3]\d, \d{4}) by (harmj0y)

正则表达式可视化及调试

我们想要的信息,如February 28, 2019,以及作者harmj0y镶嵌在一段文字中。要提取出这样的信息,使用正则表达式:

import http.client
import re

conn = http.client.HTTPConnection("www.harmj0y.net")
conn.request('GET', '/blog/')
response = conn.getresponse()
content = response.read()
content = content.decode()
# content是HTML页面的字符串形式
pattern = '([A-Za-z]{3,9} [0-3][0-9], [0-9]{4}).+(harmj0y)'
# 因为这里用\r\n作为分隔符并不能正确的获得HTML页面的每一行,所以直接re.findall找出所有匹配的元素,并把它们放入列表中。

list = re.findall(pattern, content)

# [('February 28, 2019', 'harmj0y'), ('February 20, 2019', 'harmj0y'), ('November 28, 2018', 'harmj0y'), ('October 25, 2018', 'harmj0y'), ('September 24, 2018', 'harmj0y'), ('August 22, 2018', 'harmj0y'), ('July 24, 2018', 'harmj0y'), ('July 17, 2018', 'harmj0y'), ('April 10, 2018', 'harmj0y')]

for m in list:
    print(m[0], m[1])

输出的结果为:

February 28, 2019 harmj0y
February 20, 2019 harmj0y
November 28, 2018 harmj0y
October 25, 2018 harmj0y
September 24, 2018 harmj0y
August 22, 2018 harmj0y
July 24, 2018 harmj0y
July 17, 2018 harmj0y
April 10, 2018 harmj0y
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值