Python学习笔记-12常用内建模块(下)

该笔记记录的是学习廖雪峰Python3教程的过程,摘录了一些重点,重新编排内容,并加入了更丰富的代码示例和对学习过程中所遇到问题的理解。

本章内容安排如下:

Python之所以自称 “batteries included”,就是因为内置了许多非常有用的模块,无需额外安装和配置,即可直接使用。

本章将介绍一些常用的内建模块。

itertools

简介

Python的内建模块 itertools 提供了非常有用的用于操作迭代对象的函数。我们首先看看 itertools 提供的几个“无限”迭代器:


count

count() 返回的是一个无限的迭代器,默认初始值为0,步长为1(按Python的传参规则,只传入一个参数时,传入的参数被视作初始值)

1
2
3
4
5
6
7
8
9
>>> import itertools
>>> natuals = itertools.count(1)
>>> for n in natuals:
... print(n)
...
1
2
3
...

上述代码会打印出自然数序列,但问题是它根本停不下来,只能按 Ctrl+C 退出。


cycle

cycle() 会把传入的一个序列无限重复下去:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import itertools
>>> cs = itertools.cycle('ABC') # 注意字符串也是序列的一种,还可以是列表、元组等
>>> for c in cs:
... print(c)
...
'A'
'B'
'C'
'A'
'B'
'C'
...

同样停不下来。


repeat

repeat() 负责把一个元素无限重复下去,不过 repeat() 提供了第二个参数,用于限定重复的次数:

1
2
3
4
5
6
7
>>> ns = itertools.repeat('A', 3)
>>> for n in ns:
... print(n)
...
A
A
A

takewhile

前面介绍了几种产生“无限”迭代器的方法,有没有办法对它们进行控制呢?有的~我们可以通过 takewhile()等函数,根据条件判断来截取出有限的序列:

1
2
3
4
>>> natuals = itertools.count(1)
>>> ns = itertools.takewhile(lambda x: x <= 10, natuals)
>>> list(ns)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

这里我们只截取序列中小于等于10的数,所以迭代器产生的数不符合该条件时就会停止迭代,也就不会无限地排列下去了。

除了 takewhile() 之外,itertools 还提供了一些非常有用的迭代器操作函数,在后面几个小节中会进行介绍。


chain

chain() 可以把一组迭代对象串联起来,形成一个更大的迭代器:

1
2
3
4
5
6
7
8
9
>>> for c in itertools.chain('ABC', 'XYZ'):
... print(c)
...
A
B
C
X
Y
Z

groupby

groupby() 可以把迭代器中相邻的重复元素挑出来放在一起:

1
2
3
4
5
6
7
>>> for key, group in itertools.groupby('AAABBBCCAAA'):
... print(key, list(group))
...
A ['A', 'A', 'A']
B ['B', 'B', 'B']
C ['C', 'C']
A ['A', 'A', 'A']

实际上挑选规则是通过函数完成的,只要作用于函数的两个元素返回的值相等,这两个元素就被认为是同一组的,而函数返回值将作为该组的key。如果我们想忽略大小写来分组,可以让元素 'A''a' 都返回相同的key:

1
2
3
4
5
6
7
>>> for key, group in itertools.groupby('AaaBBbcCAAa', lambda c: c.upper()):
... print(key, list(group))
...
A ['A', 'a', 'a']
B ['B', 'B', 'b']
C ['c', 'C']
A ['A', 'A', 'a']

小结

itertools 模块提供的全部是处理迭代功能的函数,它们的返回值不是 list,而是 Iterator,也即它们的值不是立刻计算出放在内存中的,只有进行迭代时(例如使用 for 循环)才会被真正计算出来。



contextlib

引言

在Python中,读写文件要注意使用完毕后必须进行关闭(文件对象占用大量资源并且同一时间操作系统只能打开有限数量的文件)。在09IO编程中,已经介绍了利用 try...finally 机制关闭文件资源的方法:

1
2
3
4
5
6
try:
f = open('/path/to/file', 'r')
f.read()
finally:
if f:
f.close()

但是,写 try...finally 非常繁琐,所以后续又介绍了使用 with 语句的方法。with 语句允许我们非常方便地使用资源,而不必担心资源没有关闭。使用 with 语句改写后,上面的代码就可以简化为:

1
2
with open('/path/to/file', 'r') as f:
f.read()

事实上,并不是只有 open() 函数返回的文件对象才能使用 with 语句。任何对象,只要正确实现了上下文管理,就可以用于 with 语句


上下文管理的实现

上下文管理是通过 __enter____exit__ 这两个方法实现的。下面的类就实现了这两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Query(object):
def __init__(self, name):
self.name = name
def __enter__(self):
print('Begin')
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')
def query(self):
print('Query info about %s...' % self.name)

这样我们就可以把自己写的资源对象用于 with 语句:

1
2
with Query('Bob') as q:
q.query()

@contextmanager装饰器

编写 __enter____exit__ 还是太繁琐了,有没有更简单的办法呢?有!Python的标准库 contextlib 提供了更简单的写法,借助它,上面的代码可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from contextlib import contextmanager
class Query(object):
def __init__(self, name):
self.name = name
def query(self):
print('Query info about %s...' % self.name)
@contextmanager
def create_query(name):
print('Begin')
q = Query(name)
yield q
print('End')

简单解析一下,我们定义一个简单的 Query 类,只有一个 query() 方法。同时我们定义了一个 create_query() 函数,由于这个函数包含 yield 关键字,所以实际上它是一个生成器。不过这个生成器只生成和抛出一个 Query 类的对象。

@contextmanager 这个装饰器接收一个生成器,并为生成器抛出的对象添加上下文管理的功能。这样 with 语句就可以正常地工作了:

1
2
with create_query('Bob') as q:
q.query()

很多时候,我们希望在某段代码执行前后自动执行特定代码,也可以用 @contextmanager 实现。例如:

1
2
3
4
5
6
7
8
9
@contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)
with tag("h1"):
print("hello")
print("world")

上述代码执行结果为:

1
2
3
4
<h1>
hello
world
</h1>

代码的执行顺序是:

  • with 语句首先执行 yield 前面的语句,因此打印出 <h1>
  • yield 之后会跳出生成器(tag() 函数),执行 with 语句内部的所有语句,因此打印出 helloworld
  • 执行完 with 语句内部的所有语句继续回到生成器;
  • 执行 yield 后面的语句,打印出 </h1>
  • 此时生成器所有语句执行完毕,不再生成,结束上下文。

借助 @contextmanager 装饰器,我们能够更加方便地实现上下文管理。


closing函数

前面一节介绍了如何为一个对象实现上下文管理功能,使得它能被作用于 with 语句。但是,得自己编写一个生成器还是很麻烦!有没有更更方便的办法呢?有!我们可以用 closing() 方法!

closing() 的本质如下:

1
2
3
4
5
6
@contextmanager
def closing(thing):
try:
yield thing
finally:
thing.close()

其实它就是一个经过 @contextmanager 装饰的生成器,它的作用就是把任意对象变为上下文对象,使其支持 with 语句。

再改写一次上面 Query 的例子:

1
2
3
4
5
6
7
8
9
10
from contextlib import closing
class Query(object):
def __init__(self, name):
self.name = name
def query(self):
print('Query info about %s...' % self.name)
with closing(Query('Bob')) as q:
q.query()

这次更加简单了~

@contextlib 还有一些其他装饰器,可以帮助我们编写更简洁的代码。



XML

简介

XML虽然比JSON复杂,在Web中应用也不如以前多了,不过仍然有很多地方会用到XML,所以我们有必要了解如何在Python中如何处理XML。


DOM vs SAX

一般来说,处理XML有两种方法,即DOM和SAX:

  • DOM会先把整个XML读入内存,然后解析为树,因此DOM占用的内存大,解析慢。优点是可以任意遍历树的节点。
  • SAX则是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。

正常情况下,优先考虑SAX,因为DOM实在太占内存


在Python中使用SAX

在Python中使用SAX解析XML非常简洁,通常我们需要关心3个事件:start_elementend_elementchar_data,准备好处理这3个事件的函数后就可以解析XML了。那么这些事件到底是什么意思呢?举个例子,当SAX解析器读到一个节点时:

1
<a href="/">python</a>

会产生3个事件:

  • start_element 事件:读取 <a href="/"> 时;
  • char_data 事件:读取 python 时;
  • end_element 事件:读取 </a> 时。

首先实现好处理这3个事件的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
from xml.parsers.expat import ParserCreate
def start_element(name, attrs):
print('Start element:', name, 'with attributes:', attrs)
def end_element(name):
print('End element:', name)
# 使用repr()函数可以将字符串转换为可打印的表示方式
# 这样就能更清楚地观察到空白字符了
def char_data(data):
print('Character data:', repr(data))

然后创建解析器:

1
2
3
4
p = ParserCreate()
p.StartElementHandler = start_element
p.EndElementHandler = end_element
p.CharacterDataHandler = char_data

尝试解析一个XML字符串:

1
2
3
4
5
6
7
8
xml = r'''<?xml version="1.0"?>
<ol>
<li><a href="/python">Python</a></li>
<li><a href="/ruby">Ruby</a></li>
</ol>
'''
print(p.Parse(xml))

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Start element: ol with attributes: {}
Character data: '\n'
Character data: ' '
Start element: li with attributes: {}
Start element: a with attributes: {'href': '/python'}
Character data: 'Python'
End element: a
End element: li
Character data: '\n'
Character data: ' '
Start element: li with attributes: {}
Start element: a with attributes: {'href': '/ruby'}
Character data: 'Ruby'
End element: a
End element: li
Character data: '\n'
End element: ol

注意,遇到换行符之后,即使后续还有其他内容,char_data 事件也会结束再被触发。因此,读取一大段文本时,CharacterDataHandler 可能会被多次调用,如果我们想要将文本放在一起输出而非分开输出,就要先保存下来,在 EndElementHandler 中再进行合并。

除了解析XML外,我们要如何生成XML呢?99%的情况下需要生成的XML结构都是非常简单的,因此,最简单也最有效的生成XML的方法就是拼接字符串:

1
2
3
4
5
6
L = []
L.append(r'<?xml version="1.0"?>')
L.append(r'<root>')
L.append(encode('some & data'))
L.append(r'</root>')
return ''.join(L)

注意,在使用XML字符串时,我们最好使用 r 表示该字符串不进行转义,三引号表示保留换行,从而避免一些不必要的错误和麻烦。

如果要生成复杂的XML呢?这时建议不要用XML,而是改成用JSON。


小结

解析XML时,注意找出自己感兴趣的节点,响应事件时,可以先把节点中的数据保存起来,等待解析完毕后,再进行处理。对这一章所用模块知识感兴趣的话可以查看官方文档。此外,觉得自带的XML库不够给力的话可以使用更为强大的第三方库lxml


练习

编写程序使用SAX解析Yahoo天气RSS的XML格式天气预报,获取地点、当天天气和次日天气:

由于现在Yahoo天气已经不再提供这个RSS服务了,所以链接已经失效了。这里我们直接解析一个廖老师提供好的XML字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data = r'''<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<rss version="2.0" xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">
<channel>
<title>Yahoo! Weather - Beijing, CN</title>
<lastBuildDate>Wed, 27 May 2015 11:00 am CST</lastBuildDate>
<yweather:location city="Beijing" region="" country="China"/>
<yweather:units temperature="C" distance="km" pressure="mb" speed="km/h"/>
<yweather:wind chill="28" direction="180" speed="14.48" />
<yweather:atmosphere humidity="53" visibility="2.61" pressure="1006.1" rising="0" />
<yweather:astronomy sunrise="4:51 am" sunset="7:32 pm"/>
<item>
<geo:lat>39.91</geo:lat>
<geo:long>116.39</geo:long>
<pubDate>Wed, 27 May 2015 11:00 am CST</pubDate>
<yweather:condition text="Haze" code="21" temp="28" date="Wed, 27 May 2015 11:00 am CST" />
<yweather:forecast day="Wed" date="27 May 2015" low="20" high="33" text="Partly Cloudy" code="30" />
<yweather:forecast day="Thu" date="28 May 2015" low="21" high="34" text="Sunny" code="32" />
<yweather:forecast day="Fri" date="29 May 2015" low="18" high="25" text="AM Showers" code="39" />
<yweather:forecast day="Sat" date="30 May 2015" low="18" high="32" text="Sunny" code="32" />
<yweather:forecast day="Sun" date="31 May 2015" low="20" high="37" text="Sunny" code="32" />
</item>
</channel>
</rss>
'''

我们需要的信息有三样,分别是地点、当天天气和次日天气。在这段XML中,地点可以从 yweather:location 标签的 city 属性和 country 属性中获得。当天天气从第一个 yweather:forecast 标签的 textlowhigh 这三个属性获得。次日天气则在第二个 yweather:forecast 标签中。并且注意到,我们只需要编写处理 start_element 事件的函数就可以取出所有这些信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# -*- coding:utf-8 -*-
from xml.parsers.expat import ParserCreate
class WeatherSaxHandler(object):
def __init__(self):
self.result = dict()
self.count = 0
self.result['forecast'] = dict()
def start_element(self, name, attrs):
if name == 'yweather:location':
self.result['city'] = attrs['city']
self.result['country'] = attrs['country']
elif name == 'yweather:forecast':
if self.count == 0:
self.result['forecast']['today'] = attrs
self.count += 1
elif self.count == 1:
self.result['forecast']['tomorrow'] = attrs
self.count += 1
def parse_weather(data):
handler = WeatherSaxHandler()
p = ParserCreate()
p.StartElementHandler = handler.start_element
p.Parse(data)
return {
'city': handler.result['city'],
'country': handler.result['country'],
'today': {
'text': handler.result['forecast']['today']['text'],
'low': int(handler.result['forecast']['today']['low']),
'high': int(handler.result['forecast']['today']['high'])
},
'tomorrow': {
'text': handler.result['forecast']['tomorrow']['text'],
'low': int(handler.result['forecast']['tomorrow']['low']),
'high': int(handler.result['forecast']['tomorrow']['high'])
}
}
1
2
3
4
5
6
7
8
9
10
11
12
# 测试:
weather = parse_weather(data)
assert weather['city'] == 'Beijing', weather['city']
assert weather['country'] == 'China', weather['country']
assert weather['today']['text'] == 'Partly Cloudy', weather['today']['text']
assert weather['today']['low'] == 20, weather['today']['low']
assert weather['today']['high'] == 33, weather['today']['high']
assert weather['tomorrow']['text'] == 'Sunny', weather['tomorrow']['text']
assert weather['tomorrow']['low'] == 21, weather['tomorrow']['low']
assert weather['tomorrow']['high'] == 34, weather['tomorrow']['high']
print('Weather:', str(weather))


HTMLParser

简介

如果我们要编写一个搜索引擎,第一步是用爬虫把目标网站的页面抓下来,第二步就是解析该HTML页面,看看里面的内容到底是新闻、图片还是视频。

假设第一步已经完成了,第二步我们应该如何解析HTML呢?

HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML。

好在Python提供了 HTMLParser 模块帮助我们解析HTML,非常方便,只需简单几行代码即可完成。


HTML字符实体

在学习 HTMLParser 之前,我们需要首先了解一下HTML需要注意的地方。在HTML中,某些字符是预留的
在HTML中不能使用小于号(<)和大于号(>),这是因为浏览器会误认为它们是标签符号。
如果希望正确地显示预留字符,我们必须在HTML的源代码中使用字符实体(character entities)

具体来说,常用的字符实体如下:

显示结果 描述 实体名称 实体编号
  空格 &nbsp; &#160;
< 小于号 &lt; &#60;
> 大于号 &gt; &#62;
& 和号 &amp; &#38;
" 引号 &quot; &#34;
' 撇号  &apos; (IE不支持) &#39;
分(cent) &cent; &#162;
£ 镑(pound) &pound; &#163;
¥ 元(yen) &yen; &#165;
欧元(euro) &euro; &#8364;
§ 小节 &sect; &#167;
© 版权(copyright) &copy; &#169;
® 注册商标 &reg; &#174;
商标 &trade; &#8482;
× 乘号 &times; &#215;
÷ 除号 &divide; &#247;

完整的字符实体表可以查看W3School的HTML 实体符号参考手册

注意到表格中有实体名称实体编号两种形式,在编写HTML代码时这两种形式都是可以使用的。即字符实体既可以写作 &entity_name; 的形式,也可以写作 &#entity_number; 的形式。比如需要显示小于号时可以写作 &lt; 也可以写作 &#60;。使用实体名而不是编号的好处是,名称更易于记忆。不过坏处是,浏览器也许并不支持所有实体名称(但对实体编号的支持却很好)。特别地,实体编号可以写作十进制形式,也可以写作十六进制形式&#60 等价于 &#x3C


使用HTMLParser解析HTML

类似于XML的SAX解析方法,使用 HTMLParser 解析HTML时,我们只需要为不同的事件编写相应的处理函数就可以了。以下面的代码为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from html.parser import HTMLParser
from html.entities import name2codepoint
class MyHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
print('This is a start tag: %s' % tag)
def handle_endtag(self, tag):
print('This is an end tag: %s' % tag)
def handle_startendtag(self, tag, attrs):
print('This is a start-end tag: %s' % tag)
def handle_data(self, data):
print('This is data:', repr(data))
def handle_comment(self, data):
print('This is a comment:', data)
def handle_entityref(self, name):
print('This is a named character reference: %s' % chr(name2codepoint[name]))
def handle_charref(self, name):
if name.startswith('x'):
print('This is a numeric character reference: %s' % chr(int(name[1:], 16)))
else:
print('This is a numeric character reference: %s' % chr(int(name)))
parser = MyHTMLParser(convert_charrefs=False)
parser.feed(r'''
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<!-- test html parser -->
<p>My personal website is <a href="http://www.2wildkids.com/">www.2wildkids.com</a>. Welcome to visit it.</p>
<img src="http://oe0e8k1nf.bkt.clouddn.com/avator-lion.jpg" />
<p>&times; is a named character reference and &#215; is a numeric character reference.
</body>
</html>''')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
This is data: '\n'
This is data: '\n'
This is a start tag: html
This is data: '\n '
This is a start tag: head
This is data: '\n '
This is a start tag: title
This is data: 'Test'
This is an end tag: title
This is data: '\n '
This is an end tag: head
This is data: '\n '
This is a start tag: body
This is data: '\n '
This is a comment: test html parser
This is data: '\n '
This is a start tag: p
This is data: 'My personal website is '
This is a start tag: a
This is data: 'www.2wildkids.com'
This is an end tag: a
This is data: '. Welcome to visit it.'
This is an end tag: p
This is data: '\n '
This is a start-end tag: img
This is data: '\n '
This is a start tag: p
This is a named character reference: ×
This is data: ' is a named character reference and '
This is a numeric character reference: ×;
This is data: ' is a numeric character reference.\n '
This is an end tag: body
This is data: '\n'
This is an end tag: html

feed() 方法可以多次调用,所以HTML字符串可以一部分一部分地塞进去,而无需一次传入完整的HTML文档。

代码比较简单,不需要过多地讲解。有几个小知识点需要注意一下:

  • HTML中每个标签可能会有一些属性,比如 <img> 标签的 src 属性还有大小属性等等,这些属性传入事件处理函数时,会被整合到一个元组(attrs 参数)中,每个属性会以键值对的形式被存放在这个元组里。
  • 处理实体名称需要 name2codepoint 这个字典,注意导入的方式。它可以将实体名称映射为十进制code point,然后再使用 chr() 函数就能得到对应的Unicode字符了。
  • 处理实体编号需要先判断使用了十进制表示形式还是十六进制表示形式。

还有一个问题,因为廖老师教程中使用的是Python3的早期版本,在早期版本中,HTMLParser类初始化时 convert_charrefs 参数默认是 False,不会把HTML字符串中的字符实体转换为Unicode字符。而在Python3.5版本中,这一参数被修改为默认是 True,所以传入HTML字符串时会自动进行转换,转换后自然就无法触发 handle_entityref() 事件和 handle_charref() 事件了。这个小问题刚开始也让我小卡了一下,在查看官方文档后终于解决了问题。


小结

借助 HTMLParser 模块,我们可以非常方便地把网页中的文本、图像等解析出来。此外,我们也可以使用更为强大的第三方库 BeautifulSoup


练习

查看Python官网的新闻页,用浏览器查看该网页的源码,尝试解析出Python官网发布的会议名称、时间和地点。

由于源码较长,所以就不在笔记中展示出来了,源码文件 Our Events _ Python.org.html 放在Res目录下。

首先分析一下源码,会议名称、时间和地点在源码中是这样表示的:

会议名称

1
<h3 class="event-title"><a href="https://www.python.org/events/python-events/491/">PyCon Belarus 2017</a></h3>

会议时间

1
<time datetime="2017-02-04T00:00:00+00:00">04 Feb. – 05 Feb. <span class="say-no-more"> 2017</span></time>

会议地点

1
<span class="event-location">Minsk, Belarus</span>

并且,出现会议名称后一定会紧接着出现相应的会议时间和会议地点。因此,提取策略可以分为以下步骤:

  1. 提取会议名称:在 h3 标签触发 handle_starttag 事件时,判断其 class 属性是否为 event-title ,是则紧接着触发的一次 handle_data 事件所得的就是会议名称。
  2. 提取会议时间:在 time 标签触发 handle_starttag 事件时,则紧接着触发的两次 handle_data 事件所得的分别是会议日期和年份。
  3. 提取会议地点:在 span 标签触发 handle_starttag 事件时,判断其 class 属性是否为 event-location ,是则紧接着触发的一次 handle_data 事件所得的就是会议地点。

我们可以设置一个 flag 变量来标记触发事件的标签,方便在 handle_data 时判断处理。又因为会议日期和年份是分开的,我们可以使用一个中间变量 date 来暂存日期,待和年份合并后再进行输出。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def __init__(self, **kw):
super().__init__(**kw)
self.flag = ''
self.date = ''
def handle_starttag(self, tag, attrs):
if tag == 'h3':
for attr in attrs:
if attr[0] == 'class' and attr[1] == 'event-title':
self.flag = 'title'
elif tag == 'time':
self.flag = 'time'
elif tag == 'span':
for attr in attrs:
if attr[0] == 'class' and attr[1] == 'event-location':
self.flag = 'location'
def handle_data(self, data):
if self.flag == 'title':
print('会议名称:', data)
self.flag = ''
elif self.flag == 'time':
if self.date == '':
self.date = self.date + data
else:
self.date = self.date + data
print('会议时间:', self.date)
self.date = ''
self.flag = ''
elif self.flag == 'location':
print('会议地点:', data, '\n')
self.flag = ''
with open(r'E:\wheels\learnpython\My_Python_Notebook\Res\Our Events _ Python.org.html', 'r', encoding='utf-8') as f:
html = f.read()
parser = MyHTMLParser(convert_charrefs=False)
parser.feed(html)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
会议名称: PyCon Belarus 2017
会议时间: 04 Feb. – 05 Feb. 2017
会议地点: Minsk, Belarus
会议名称: PyTennessee 2017
会议时间: 04 Feb. – 06 Feb. 2017
会议地点: Nashville, Tennessee, USA
会议名称: PythonFOSDEM 2017
会议时间: 04 Feb. – 06 Feb. 2017
会议地点: Université Libre de Bruxelles, Franklin Rooseveltlaan 50, 1050 Brussel, Belgium
会议名称: FOSDEM 2017
会议时间: 04 Feb. – 06 Feb. 2017
会议地点: Université Libre de Bruxelles, Franklin Rooseveltlaan 50, 1050 Brussel, Belgium
会议名称: PyCon Colombia 2017
会议时间: 10 Feb. – 12 Feb. 2017
会议地点: Bogota, Colombia
会议名称: PyCon Pune 2017
会议时间: 16 Feb. – 20 Feb. 2017
会议地点: COEP, Pune, India
会议名称: PyCon Cameroon
会议时间: 20 Jan. – 23 Jan. 2017
会议地点: Molyko Buea,Cameroon


urllib

简介

urllib 库提供了一系列用于操作URL的功能。


Get

urllibrequest 模块可以非常方便地抓取URL内容,urlopen() 函数首先发送一个GET请求到指定的页面,然后返回HTTP的响应。比方说,对豆瓣的一个URL(https://api.douban.com/v2/book/2129650)进行抓取,并返回响应:

1
2
3
4
5
6
7
8
from urllib import request
with request.urlopen('https://api.douban.com/v2/book/2129650') as f:
data = f.read()
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', data.decode('utf-8'))

可以看到HTTP响应状态,header,以及返回的JSON数据:

1
2
3
4
5
6
7
8
9
10
11
Status: 200 OK
Server: nginx
Date: Tue, 26 May 2015 10:02:27 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2049
Connection: close
Expires: Sun, 1 Jan 2006 01:00:00 GMT
Pragma: no-cache
Cache-Control: must-revalidate, no-cache, private
X-DAE-Node: pidl1
Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰编著"],"pubdate":"2007-6","tags":[{"count":20,"name":"spring","title":"spring"}...}

如果我们要想模拟浏览器发送GET请求,就需要使用 Request 对象,通过往 Request 对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone 6去请求豆瓣首页:

1
2
3
4
5
6
7
8
9
from urllib import request
req = request.Request('http://www.douban.com/')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with request.urlopen(req) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))

这样豆瓣会返回适合iPhone的移动版网页:

1
2
3
4
5
...
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">
<meta name="format-detection" content="telephone=no">
<link rel="apple-touch-icon" sizes="57x57" href="http://img4.douban.com/pics/cardkit/launcher/57.png" />
...

Post

如果要以POST方式发送一个请求,就需要把参数数据以bytes的形式传入

比方说,我们模拟微博登录,需要先读取用于登录的邮箱和口令,然后按照 weibo.cn 登录页的格式以 username=xxx&password=xxx 的方式来传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from urllib import request, parse
print('Login to weibo.cn...')
email = input('Email: ')
passwd = input('Password: ')
login_data = parse.urlencode([
('username', email),
('password', passwd),
('entry', 'mweibo'),
('client_id', ''),
('savestate', '1'),
('ec', ''),
('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
])
req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Origin', 'https://passport.weibo.cn')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
req.add_header('Referer', 'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')
with request.urlopen(req, data=login_data.encode('utf-8')) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))

如果登录成功,我们获得的响应如下:

1
2
3
4
5
6
Status: 200 OK
Server: nginx/1.2.0
...
Set-Cookie: SSOLoginState=1432620126; path=/; domain=weibo.cn
...
Data: {"retcode":20000000,"msg":"","data":{...,"uid":"1658384301"}}

如果登录失败,我们获得的响应如下:

1
2
3
...
Data: {"retcode":50011015,"msg":"\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef","data":{"username":"example@python.org","errline":536}}
Handler

如果还需要更复杂的控制,比如通过一个代理服务器(Proxy) 去访问网站,我们需要利用 ProxyHandler 来处理,示例代码如下:

1
2
3
4
5
6
proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'})
proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()
proxy_auth_handler.add_password('realm', 'host', 'username', 'password')
opener = urllib.request.build_opener(proxy_handler, proxy_auth_handler)
with opener.open('http://www.example.com/login.html') as f:
pass

小结

urllib 提供的功能就是利用程序去执行各种HTTP请求。如果要模拟浏览器完成特定功能,需要把请求伪装成浏览器。伪装的方法是先监控浏览器发出的请求,再根据浏览器的发出的请求中的header来伪装,User-Agent就是用来标识浏览器的。


练习

利用 urllib 读取XML,将XML一节的天气预报数据由硬编码改为由 urllib 获取

由于雅虎天气API已经跪了,这里改用百度天气API来尝试。

接口例子:http://api.map.baidu.com/telematics/v3/weather?location=广州&ak=8IoIaU655sQrs95uMWRWPDIa

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from urllib import request
from xml.parsers.expat import ParserCreate
class WeatherSaxHandler(object):
def __init__(self):
self.result = dict()
self.currentTag = '' #
self.flag = True
def start_element(self, name, attrs):
self.currentTag = name
def char_data(self, data):
if self.flag == True:
if self.currentTag == 'currentCity':
self.result['城市'] = data
elif self.currentTag == 'date':
self.result['当前'] = data
elif self.currentTag == 'weather':
self.result['天气'] = data
elif self.currentTag == 'wind':
self.result['风速'] = data
elif self.currentTag == 'temperature':
self.result['气温'] = data
self.flag = False
self.currentTag = '' # 记得每次解析完信息要重置
def parse_weather(data):
handler = WeatherSaxHandler()
p = ParserCreate()
p.StartElementHandler = handler.start_element
p.CharacterDataHandler = handler.char_data
p.Parse(data)
return handler.result
def fetch_xml(url):
with request.urlopen(url) as f:
data = f.read().decode('utf-8')
print('Status:', f.status, f.reason)
return parse_weather(data)
# 测试
print(fetch_xml('http://api.map.baidu.com/telematics/v3/weather?location=guangzhou&ak=8IoIaU655sQrs95uMWRWPDIa'))

运行结果:

1
2
Status: 200 OK
{'气温': '21 ~ 14℃', '城市': 'guangzhou', '风速': '微风', '当前': '周六 02月04日 (实时:19℃)', '天气': '小雨'}


坚持技术分享,您的支持将鼓励我继续创作!