一、collections :内建集合模块.
1、namedtuple:创建一个自定义的tuple
对象
2、deque:为了高效实现插入和删除操作的双向列表,适合用于队列和栈
3、defaultdict:Key不存在时返回默认值
4、OrderedDict:保持Key的顺序
5、Counter:一个简单的计数器
二、base64 : 一种用64个字符来表示任意二进制数据的方法
三、struct : str
和其他二进制数据类型的转换
四、hashlib : 提供常见的摘要算法,如MD5,SHA1
五、itertools : 提供非常有用的用于操作迭代对象的函数
六、XML : XML虽然比JSON复杂,在Web中应用也不如以前多了,不过仍有很多地方在用,所以,有必要了解如何操作XML。
七、HTMLParser
------------------------------------------------------------------------------------------------------------------------------------------------
一、collections :内建集合模块.
1、namedtuple:创建一个自定义的tuple
对象,并且规定了tuple
元素的个数,并可以用属性而不是索引来引用tuple
的某个元素.
表示一个坐标:
>>> from collections import namedtuple >>> Point = namedtuple('Point',['x','y']) >>> p = Point(1,2) >>> p.x 1 >>> p.y 2
namedtuple
是一个函数,它用来创建一个自定义的tuple
对象,并且规定了tuple
元素的个数,并可以用属性而不是索引来引用tuple
的某个元素。
用namedtuple
可以很方便地定义一种数据类型,它具备tuple的不变性,又可以根据属性来引用,使用十分方便。
>>> isinstance(p,Point) True >>> isinstance(p,tuple) True
类似的,如果要用坐标和半径表示一个圆,也可以用namedtuple
定义:
# namedtuple('名称', [属性list]): Circle = namedtuple('Circle', ['x', 'y', 'r'])
2、deque:为了高效实现插入和删除操作的双向列表,适合用于队列和栈
使用list
存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list
是线性存储,数据量大的时候,插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈:
>>> from collections import deque >>> q = deque(['a','b','c']) >>> q.append('x') >>> q.appendleft('y') >>> q deque(['y', 'a', 'b', 'c', 'x'])
3、defaultdict:Key不存在时返回默认值
使用dict
时,如果引用的Key不存在,就会抛出KeyError
。如果希望key不存在时,返回一个默认值,就可以用defaultdict
:
>>> from collections import defaultdict >>> dd = defaultdict(lambda:'N/A') >>> dd['key1'] = 'abc' >>> dd['key1'] 'abc' >>> dd['key2'] 'N/A' >>> dd['key3'] 'N/A'
注意默认值是调用函数返回的,而函数在创建defaultdict
对象时传入。
除了在Key不存在时返回默认值,defaultdict
的其他行为跟dict
是完全一样的。
4、OrderedDict :保持Key的顺序
使用dict
时,Key是无序的。在对dict
做迭代时,我们无法确定Key的顺序。
如果要保持Key的顺序,可以用OrderedDict
:
>>> from collections import OrderedDict >>> d = dict([('a',1),('f',9),('b',3),('c',4)]) >>> d # dict的Key是无序的 {'a': 1, 'c': 4, 'b': 3, 'f': 9} >>> d = OrderedDict([('a',1),('f',9),('b',3),('c',4)]) >>> d # OrderedDict的Key是有序的 OrderedDict([('a', 1), ('f', 9), ('b', 3), ('c', 4)])
注意,OrderedDict
的Key会按照插入的顺序排列,不是Key本身排序:参考这里
>>> od = OrderedDict() >>> od['z'] = 1 >>> od['y'] = 2 >>> od['x'] = 3 >>> od.keys() # 按照插入的Key的顺序返回 ['z', 'y', 'x']
OrderedDict
可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key:
from collections import OrderedDict class LastUpdatedOrderedDict(OrderedDict): def __init__(self, capacity): super(LastUpdatedOrderedDict, self).__init__() self._capacity = capacity #集合大小 def __setitem__(self, key, value): containsKey = 1 if key in self else 0 if len(self) - containsKey >= self._capacity: #当前集合大小 last = self.popitem(last=False) #弹出前面的key-value,last=true 采用LIFI,last=false采用FIFO print 'remove:', last if containsKey: #有当前key del self[key] #删除存在的key print 'set:', (key, value) else: print 'add:', (key, value) OrderedDict.__setitem__(self, key, value) #设置值
5、Counter:一个简单的计数器
Counter
是一个简单的计数器,例如,统计字符出现的个数:
>>> from collections import Counter >>> c = Counter() >>> for ch in 'applications': ... c[ch] = c[ch]+1 ... >>> c Counter({'a': 2, 'i': 2, 'p': 2, 'c': 1, 'l': 1, 'o': 1, 'n': 1, 's': 1, 't': 1})
Counter
实际上也是dict
的一个子类,上面的结果可以看出,字符'a'
、'i'
、'p'
各出现了两次,其他字符各出现了一次。
二、base64 : 一种用64个字符来表示任意二进制数据的方法
用记事本打开exe
、jpg
、pdf
这些文件时,我们都会看到一大堆乱码,因为二进制文件包含很多无法显示和打印的字符,所以,如果要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法
Base64编码要求把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式。 如果剩下的字符不足3个字节,则用0填充,输出字符使用'=',因此编码后输出的文本末尾可能会出现1或2个'='。 为了保证所输出的编码位可读字符,Base64制定了一个编码表,以便进行统一转换。编码表的大小为2^6=64,这也是Base64名称的由来。
Base64编码表
码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 | |||
---|---|---|---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w | |||
1 | B | 17 | R | 33 | h | 49 | x | |||
2 | C | 18 | S | 34 | i | 50 | y | |||
3 | D | 19 | T | 35 | j | 51 | z | |||
4 | E | 20 | U | 36 | k | 52 | 0 | |||
5 | F | 21 | V | 37 | l | 53 | 1 | |||
6 | G | 22 | W | 38 | m | 54 | 2 | |||
7 | H | 23 | X | 39 | n | 55 | 3 | |||
8 | I | 24 | Y | 40 | o | 56 | 4 | |||
9 | J | 25 | Z | 41 | p | 57 | 5 | |||
10 | K | 26 | a | 42 | q | 58 | 6 | |||
11 | L | 27 | b | 43 | r | 59 | 7 | |||
12 | M | 28 | c | 44 | s | 60 | 8 | |||
13 | N | 29 | d | 45 | t | 61 | 9 | |||
14 | O | 30 | e | 46 | u | 62 | + | |||
15 | P | 31 | f | 47 | v | 63 | / |
长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。
Python内置的base64
可以直接进行base64的编解码:
>>> import base64 >>> base64.b64encode('binary\x00base64') 'YmluYXJ5AGJhc2U2NA==' >>> base64.b64decode('YmluYXJ5AGJhc2U2NA==') 'binary\x00base64'
三、struct : str
和其他二进制数据类型的转换
Python没有专门处理字节的数据类型。但由于str
既是字符串,又可以表示字节,所以,字节数组=str。而在C语言中,可以很方便地用struct、union来处理字节,以及字节和int,float的转换
在Python中,比方说要把一个32位无符号整数变成字节,也就是4个长度的str,得配合位运算符这么写:
>>> n = 10240023 >>> b1 = chr((n & 0xff000000) >> 24) >>> b2 = chr((n & 0xff0000) >> 16) >>> b3 = chr((n & 0xff00) >> 8) >>> b4 = chr(n & 0xff) >>> s = b1 + b2 + b3 + b4 >>> s '\x00\x9c@\x17'
Python提供了一个struct
模块来解决str
和其他二进制数据类型的转换:
struct.
pack()
函数把任意数据类型变成字符串:
>>> import struct >>> struct.pack('>I',10240023) '\x00\x9c@\x17'
pack
的第一个参数是处理指令,其中 '>I'
的意思是:>
表示字节顺序是big-endian,也就是网络序,I
表示4字节无符号整数。
后面的参数个数要和处理指令一致。
unpack
把str
变成相应的数据类型:
>>> struct.unpack('>IH','\xf0\xf0\xf0\xf0\x80\x80') (4042322160L, 32896)
根据>IH
的说明,后面的str
依次变为I
:4字节无符号整数和H
:2字节无符号整数。
尽管Python不适合编写底层操作字节流的代码,但在对性能要求不高的地方,利用struct
就方便多了
官方文档参考:这里,其他参考:这里
Windows的位图文件(.bmp)是一种非常简单的文件格式,可以用struct来
分析一下。
首先找一个bmp文件,没有的话用“画图”画一个。
读入前30个字节来分析:
s = '\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'
#!/usr/bin/python #coding:utf-8 import struct def main(): fb = open('./structtest.bmp','rb') s = fb.read(30) h = struct.unpack('<ccIIIIIIHH',s) print h pass if __name__ == '__main__': main()
('B', 'M', 2239542, 0, 54, 40, 1152, 648, 1, 24) [Finished in 0.1s]
BMP格式采用小端方式存储数据,文件头的结构按顺序如下:
两个字节:
'BM'
表示Windows位图,'BA'
表示OS/2位图; --BM
一个4字节整数:表示位图大小; --2239542
一个4字节整数:保留位,始终为0; --0
一个4字节整数:实际图像的偏移量; --54
一个4字节整数:Header的字节数; --40
一个4字节整数:图像宽度; --1152
一个4字节整数:图像高度; --648
一个2字节整数:始终为1; --1
一个2字节整数:颜色数。 --24
所以,组合起来用unpack
读取:
结果显示,'B'
、'M'
说明是Windows位图,位图大小为1152x648,颜色数为24。
那么我们就可以编写一个bmpinfo.py
,可以检查任意文件是否是位图文件,如果是,打印出图片大小和颜色数。
四、hashlib : 提供常见的摘要算法,如MD5,SHA1
摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。
举个例子,写了一篇文章,内容是一个字符串'how to use python hashlib - by Michael'
,并附上这篇文章的摘要是'2d73d4f15c0db7f5ecb321b6a65e5d6d'
。如果有人篡改了文章,并发表为'how to use python hashlib - by Bob'
,可以一下子看出Bob篡改了文章,因为根据'how to use python hashlib - by Bob'
计算出的摘要不同于原始文章的摘要。
摘要算法就是通过摘要函数f()
对任意长度的数据data
计算出固定长度的摘要digest
,目的是为了发现原始数据是否被人篡改过。
摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算f(data)
很容易,但通过digest
反推data
却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。
以常见的摘要算法MD5为例,计算出一个字符串的MD5值:
import hashlib md5 = hashlib.md5() md5.update('how to use md5 in python hashlib?') print md5.hexdigest()
d26a53750bc40b38b65a520292f69306 [Finished in 0.1s]
MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。
另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似:
import hashlib sha1 = hashlib.sha1() sha1.update('how to use sha1 in ') sha1.update('python hashlib?') print sha1.hexdigest()
2c76b57293ce30acef38d98f6046927161b46a44 [Finished in 0.1s]
SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。
比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法越慢,而且摘要长度更长。
有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要?完全有可能,因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为碰撞,比如Bob试图根据摘要反推出一篇文章'how to learn hashlib in python - by Bob'
,并且这篇文章的摘要恰好和之前的文章完全一致,这种情况也并非不可能出现,但是非常非常困难。
1、摘要算法应用
常用例子:
任何允许用户登录的网站都会存储用户登录的用户名和口令。如何存储用户名和口令呢?方法是存到数据库表中:
name | password --------+---------- michael | 123456 bob | abc999 alice | alice2008
如果以明文保存用户口令,如果数据库泄露,所有用户的口令就落入黑客的手里。此外,网站运维人员是可以访问数据库的,也就是能获取到所有用户的口令。
正确的保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要,比如MD5:
username | password ---------+--------------------------------- michael | e10adc3949ba59abbe56e057f20f883e bob | 878ef96e86145580c38c87f0410ad153 alice | 99b1c2188db85afee403b1536010c2c9
当用户登录时,首先计算用户输入的明文口令的MD5,然后和数据库存储的MD5对比,如果一致,说明口令输入正确,如果不一致,口令肯定错误。
练习:根据用户输入的口令,计算出存储在数据库中的MD5口令:
def calc_md5(password): pwd_md5 = hashlib.md5() pwd_md5.update(password) return pwd_md5.hexdigest()
存储MD5的好处是即使运维人员能访问数据库,也无法获知用户的明文口令。
练习:设计一个验证用户登录的函数,根据用户输入的口令是否正确,返回True或False:
db = { 'michael': 'e10adc3949ba59abbe56e057f20f883e', 'bob': '878ef96e86145580c38c87f0410ad153', 'alice': '99b1c2188db85afee403b1536010c2c9' } def login(user, password): pwd_md5 = hashlib.md5() pwd_md5.update(password) return True if db[user] == pwd_md5.hexdigest() else False
采用MD5存储口令是否就一定安全呢?也不一定。假设你是一个黑客,已经拿到了存储MD5口令的数据库,如何通过MD5反推用户的明文口令呢?暴力破解费事费力,真正的黑客不会这么干。
考虑这么个情况,很多用户喜欢用123456
,888888
,password
这些简单的口令,于是,黑客可以事先计算出这些常用口令的MD5值,得到一个反推表:
'e10adc3949ba59abbe56e057f20f883e': '123456' '21218cca77804d2ba1922c33e0151105': '888888' '5f4dcc3b5aa765d61d8327deb882cf99': 'password'
这样,无需破解,只需要对比数据库的MD5,黑客就获得了使用常用口令的用户账号。
对于用户来讲,当然不要使用过于简单的口令。但是,能否在程序设计上对简单口令加强保护呢?
由于常用口令的MD5值很容易被计算出来,所以,要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5,这一方法通过对原始口令加一个复杂字符串来实现,俗称“加盐”:
def calc_md5(password): return get_md5(password + 'the-Salt')
经过Salt处理的MD5口令,只要Salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推明文口令。
但是如果有两个用户都使用了相同的简单口令比如123456
,在数据库中,将存储两条相同的MD5值,这说明这两个用户的口令是一样的。有没有办法让使用相同口令的用户存储不同的MD5呢?
如果假定用户无法修改登录名,就可以通过把登录名作为Salt的一部分来计算MD5,从而实现相同口令的用户也存储不同的MD5。
练习:根据用户输入的登录名和口令模拟用户注册,计算更安全的MD5:
db = {} def register(username, password): db[username] = get_md5(password + username + 'the-Salt')
然后,根据修改后的MD5算法实现用户登录的验证:
def login(username, password): pass
注意:摘要算法在很多地方都有广泛的应用。要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。
五、itertools : 提供非常有用的用于操作迭代对象的函数
Python的内建模块itertools
提供了非常有用的用于操作迭代对象的函数。
首先,看看itertools
提供的几个“无限”迭代器:
>>> import itertools >>> natuals = itertools.count(1) >>> for n in natuals: ... print n ... 1 2 3 ...
因为count()
会创建一个无限的迭代器,所以上述代码会打印出自然数序列,根本停不下来,只能按Ctrl+C
退出。
cycle()
会把传入的一个序列无限重复下去:
>>> import itertools >>> cs = itertools.cycle('ABC') # 注意字符串也是序列的一种 >>> for c in cs: ... print c ... 'A' 'B' 'C' 'A' 'B' 'C' ...
同样停不下来。
repeat()
负责把一个元素无限重复下去,不过如果提供第二个参数就可以限定重复次数:
>>> ns = itertools.repeat('A', 10) >>> for n in ns: ... print n ... 打印10次'A'
无限序列只有在for
迭代时才会无限地迭代下去,如果只是创建了一个迭代对象,它不会事先把无限个元素生成出来,事实上也不可能在内存中创建无限多个元素。
无限序列虽然可以无限迭代下去,但是通常我们会通过takewhile()
等函数根据条件判断来截取出一个有限的序列:
>>> natuals = itertools.count(1) >>> ns = itertools.takewhile(lambda x: x <= 10, natuals) >>> for n in ns: ... print n ... 打印出1到10
itertools
提供的几个迭代器操作函数更加有用:
chain() groupby() imap() ifilter()
chain()
可以把一组迭代对象串联起来,形成一个更大的迭代器:
for c in chain('ABC', 'XYZ'): print c # 迭代效果:'A' 'B' 'C' 'X' 'Y' 'Z'
groupby()
把迭代器中相邻的重复元素挑出来放在一起:
>>> for key, group in itertools.groupby('AAABBBCCAAA'): ... print key, list(group) # 为什么这里要用list()函数呢? ... A ['A', 'A', 'A'] B ['B', 'B', 'B'] C ['C', 'C'] A ['A', 'A', 'A']
实际上挑选规则是通过函数完成的,只要作用于函数的两个元素返回的值相等,这两个元素就被认为是在一组的,而函数返回值作为组的key。如果我们要忽略大小写分组,就可以让元素'A'
和'a'
都返回相同的key:
>>> 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']
imap()
和map()
的区别在于,imap()
可以作用于无穷序列,并且,如果两个序列的长度不一致,以短的那个为准。
>>> for x in itertools.imap(lambda x, y: x * y, [10, 20, 30], itertools.count(1)): ... print x ... 10 40 90
注意: imap()
返回一个迭代对象,而map()
返回list。当你调用map()
时,已经计算完毕:
>>> r = map(lambda x: x*x, [1, 2, 3]) >>> r # r已经计算出来了 [1, 4, 9]
由于map()返回的是一个list,所以当用它去处理无限序列的时候,它会尝试计算完之后才返回,但是序列是无限的,所以它会一直计算下去,致使其占用的系统的内存越来越高。
当你调用imap()
时,并没有进行任何计算:
>>> r = itertools.imap(lambda x: x*x, [1, 2, 3]) >>> r <itertools.imap object at 0x103d3ff90> # r只是一个迭代对象
必须用for
循环对r
进行迭代,才会在每次循环过程中计算出下一个元素:
>>> for x in r: ... print x ... 1 4 9
这说明imap()
实现了“惰性计算”,也就是在需要获得结果的时候才计算。类似imap()
这样能够实现惰性计算的函数就可以处理无限序列:
>>> r = itertools.imap(lambda x: x*x, itertools.count(1)) >>> for n in itertools.takewhile(lambda x: x<100, r): ... print n ... 结果是什么?
>>> r = itertools.imap(lambda x: x*x , itertools.count(1)) >>> for n in itertools.takewhile(lambda x: x < 100,r): ... print n ... 1 4 9 16 25 36 49 64 81 >>>
ifilter()
就是filter()
的惰性实现。
itertools
模块提供的全部是处理迭代功能的函数,它们的返回值不是list,而是迭代对象,只有用for
循环迭代的时候才真正计算。
六、XML : XML虽然比JSON复杂,在Web中应用也不如以前多了,不过仍有很多地方在用,所以,有必要了解如何操作XML。
DOM vs SAX
操作XML有两种方法:DOM和SAX。DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。
正常情况下,优先考虑SAX,因为DOM实在太占内存。
在Python中使用SAX解析XML非常简洁,通常我们关心的事件是start_element
,end_element
和char_data
,准备好这3个函数,然后就可以解析xml了。
举个例子,当SAX解析器读到一个节点时:
<a href="/">python</a>
会产生3个事件:
start_element事件,在读取<a href="/">
时;
char_data事件,在读取python
时;
end_element事件,在读取</a>
时。
用代码实验一下:
from xml.parsers.expat import ParserCreate class DefaultSaxHandler(object): def start_element(self, name, attrs): print('sax:start_element: %s, attrs: %s' % (name, str(attrs))) def end_element(self, name): print('sax:end_element: %s' % name) def char_data(self, text): print('sax:char_data: %s' % text) xml = r'''<?xml version="1.0"?> <ol> <li><a href="/python">Python</a></li> <li><a href="/ruby">Ruby</a></li> </ol> ''' handler = DefaultSaxHandler() parser = ParserCreate() parser.returns_unicode = True parser.StartElementHandler = handler.start_element parser.EndElementHandler = handler.end_element parser.CharacterDataHandler = handler.char_data parser.Parse(xml)
当设置returns_unicode
为True时,返回的所有element名称和char_data都是unicode,处理国际化更方便。
需要注意的是读取一大段字符串时,CharacterDataHandler
可能被多次调用,所以需要自己保存起来,在EndElementHandler
里面再合并。
除了解析XML外,如何生成XML呢?99%的情况下需要生成的XML结构都是非常简单的,因此,最简单也是最有效的生成XML的方法是拼接字符串:
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呢?建议你不要用XML,改成JSON。
解析XML时,注意找出自己感兴趣的节点,响应事件时,把节点数据保存起来。解析完毕后,就可以处理数据。
练习一下解析Yahoo的XML格式的天气预报,获取当天和最近几天的天气:
http://weather.yahooapis.com/forecastrss?u=c&w=2151330
参数w
是城市代码,要查询某个城市代码,可以在weather.yahoo.com搜索城市,浏览器地址栏的URL就包含城市代码。
七、HTMLParser :解析HTML
如果要编写一个搜索引擎,第一步是用爬虫把目标网站的页面抓下来,第二步就是解析该HTML页面,看看里面的内容到底是新闻、图片还是视频。
假设第一步已经完成了,第二步应该如何解析HTML呢?
HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML。
好在Python提供了HTMLParser来非常方便地解析HTML,只需简单几行代码:
from HTMLParser import HTMLParser from htmlentitydefs import name2codepoint class MyHTMLParser(HTMLParser): def handle_starttag(self, tag, attrs): print('<%s>' % tag) def handle_endtag(self, tag): print('</%s>' % tag) def handle_startendtag(self, tag, attrs): print('<%s/>' % tag) def handle_data(self, data): print('data') def handle_comment(self, data): print('<!-- -->') def handle_entityref(self, name): print('&%s;' % name) def handle_charref(self, name): print('&#%s;' % name) parser = MyHTMLParser() parser.feed('<html><head></head><body><p>Some <a href=\"#\">html</a> tutorial...<br>END</p></body></html>')
feed()
方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。
特殊字符有两种,一种是英文表示的
,一种是数字表示的Ӓ
,这两种字符都可以通过Parser解析出来。
当然,解析HTML的还有很多其他框架,比如:PyQuery,BeautifulSoup.
asdf