11. 常用内建模块

常用内建模块

datetime

datetime是Python处理日期和时间的标准库。

获取当前的日期和时间:

1
2
3
4
5
6
>>> from datetime import datetime
>>> now=datetime.now() #获取当前的datetime
>>> print(now)
2016-11-01 20:25:50.305269
>>> print(type(now))
<class 'datetime.datetime'>

注意到datetime是模块,datetime模块还包含一个datetime类,通过from datetime import datetime导入的才是datetime这个类。如果仅导入import datetime,则必须引用全名datetime.datetimedatetime.now()返回当前日期和时间,其类型是datetime

获取指定日期和时间:

1
2
3
4
>>> from datetime import datetime
>>> dt=datetime(2015,11,11,11,11)
>>> print(dt)
2015-11-11 11:11:00

datetime转换为timestamp,在计算机中,时间实际上是由数字表示的,我们把1970年1月1日00:00:00UTC+00:00时区的时刻称为epoch time,记为0,当前时间就是相当于epoch time的秒数,称为timestamp。

timestamp=0=1970-1-1 00:00:00 UTC+0:00

对应的北京时间是

timestamp=0=1970-1-1 08:00:00 UTC+8:00

课件timestamp的值和时区毫无关系,因为timestamp一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,这就是为什么计算机存储的当前时间是一timestamp表示的,因为世界各地的计算机在任意时刻的timestamp都是完全相同的。

把一个datetime类型转换为timestamp只需要简单调用timestamp()方法:

1
2
>>> dt.timestamp()
1447211460.0

注意Python的timestamp是一个浮点数。如果有小数点,小数位表示毫秒数。某些编程语言的timestamp使用整数表示毫秒数,这种情况下需要把timestamp除以1000就得到Python的浮点表示方法。

timestamp转换为datetime:

1
2
3
4
>>> from datetime import datetime
>>> t=1429417200.0
>>> print(datetime.fromtimestamp(t))
2015-04-19 12:20:00

注意到timestamp是一个浮点数,它没有时区的概念,而datetime有时区的。上述转换是在timestamp和本地时间做转换。本地时间是指当前操作系统设定的时区。例如北京时区是东八区,则本地时间是2015-04-19 12:20:00,实际上就是UTC+8:00时区的时间2015-04-19 12:20:00 UTC+8:00,而此时的格林威治标准时间与北京差了八个小时,也就是UTC+0:00时区的时间:2015-04-19 04:20:00 UTC+0:00。

timestamp也可以直接转换到UTC标准时区的时间:

1
2
3
4
5
6
>>> from datetime import datetime
>>> t=1429417200.0
>>> print(datetime.fromtimestamp(t))
2015-04-19 12:20:00
>>> print(datetime.utcfromtimestamp(t))
2015-04-19 04:20:00

str转换为datetime,很多时候,用户输入的日期和时间是字符串,要处理日期和时间,首先要把str转换为datetime。转换方法是通过datetime.strptime()实现:

1
2
3
4
>>> from datetime import datetime
>>> cday=datetime.strptime('2015-6-1 18:19:59','%Y-%m-%d %H:%M:%S')
>>> print(cday)
2015-06-01 18:19:59

字符串'%Y-%m-%d %H:%M:%S'规定了日期和时间部分的格式。

datetime转换为str,如果已经有了datetime对象,要把它格式化 为字符串显示给用户,就需要转换为str,转换方法是通过strftime()实现的,同样需要一个日期和时间的格式化字符串:

1
2
3
4
5
6
>>> from datetime import datetime
>>> now=datetime.now()
>>> print(now.strftime('%a,%b %d %H:%M'))
Tue,Nov 01 21:26
>>> print(now)
2016-11-01 21:26:20.187960

datetime加减,对日期和时间进行加减实际上就是把datetime往后或往前计算,得到新的datetime。加减可以直接用+``-运算符,需要导入timedelta这个类:

1
2
3
4
5
6
7
8
9
10
>>> from datetime import datetime,timedelta
>>> now=datetime.now()
>>> now
datetime.datetime(2016, 11, 1, 21, 29, 50, 726284)
>>> now+timedelta(hours=10)
datetime.datetime(2016, 11, 2, 7, 29, 50, 726284)
>>> now-timedelta(days=2)
datetime.datetime(2016, 10, 30, 21, 29, 50, 726284)
>>> now+timedelta(days=1,hours=10)
datetime.datetime(2016, 11, 3, 7, 29, 50, 726284)

本地时间转换为UTC时间,一个datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个市区,除非强行给datetime设置一个时区:

1
2
3
4
5
6
7
8
>>> from datetime import datetime,timedelta,timezone
>>> tz_utc_8=timezone(timedelta(hours=8)) #创建时区UTC+8:00
>>> now=datetime.now()
>>> now
datetime.datetime(2016, 11, 1, 21, 35, 48, 271569)
>>> dt=now.replace(tzinfo=tz_utc_8) #强制设置为UTC+8:00
>>> dt
datetime.datetime(2016, 11, 1, 21, 35, 48, 271569, tzinfo=datetime.timezone(datetime.timedelta(0, 28800)))

如果系统时区恰好是UTC+8:00,那么上述代码就是正确的,否则,不能强制设置为UTC+8:00时区。

时区转换,我们可以先通过utcnow()拿到当前的UTC时间,再转换为任意时区的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> utc_dt=datetime.utcnow().replace(tzinfo=timezone.utc) #拿到UTC时间,强制设置时区为UTC+0:00
>>> print(utc_dt)
2016-11-01 13:39:21.800814+00:00 #astimezone()将转化时区为北京时间
>>> bj_dt=utc_dt.astimezone(timezone(timedelta(hours=8)))
>>> print(bj_dt)
2016-11-01 21:39:21.800814+08:00
>>> tokyo_dt=utc_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt)
2016-11-01 22:39:21.800814+09:00
>>> tokyo_dt2=bj_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt2)
2016-11-01 22:39:21.800814+09:00
>>>

时区转换的关键在于,拿到一个datetime时,要获知其正确的时区,然后强制设置时区,作为基准时间。利用带时区的datetime,通过astimezone()方法,可以转换到任意时区。

datetime表示时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。如果要存储datetime,最佳方法是将其转换为timestamp再存储,因为timestamp的值与时区完全无关。


collections

collections是Python内建的一个集合模块,提供了许多有用的集合类。

namedtuple

我们知道tuple可以表示不变集合,比如,一个点的二维坐标可以表示为:

p=(1,2)```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
但是看到(1,2),很难看出这个`tuple`是用来表示一个坐标的。定义一个class又没有必要,这时候,`namedtuple`就可以使用了:
```python
>>> from collections import namedtuple
>>> Point=namedtuple('Point',['x','y'])
>>> p=Point(1,2)
>>> p.x
1
>>> p.y
2
>>> isinstance(p,Point)
True
>>> isinstance(p,tuple)
True

namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。这样我们可以用namedtuple可以很方便的定义一种数据结构,它具备tuple的不变性,又可以根据属性来引用,十分方便。可以验证创建的Point对象是tuple的一种子类。

类似的如果要用坐标和半径定义一个圆:

1
>>> Circle=namedtuple('Circle',['x','y','r'])

deque

使用list村塾数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低。deque是为了高效实现插入和删除操作 的双向列表,适合用于队列和栈:

1
2
3
4
5
6
>>> from collections import deque
>>> q=deque(['a','b','c'])
>>> q.append('x')
>>> q.appendleft('y')
>>> q
deque(['y', 'a', 'b', 'c', 'x'])

deque除了实现list的append()pop()外,还支持appendleft()popleft()这样可以非常高效的往头部添加或删除元素。

defaultdict

使用dict时,如果引用的key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defalultdict

1
2
3
4
5
6
7
>>> from collections import defaultdict
>>> dd=defaultdict(lambda: 'N/A')
>>> dd['key1']='abc'
>>> dd['key1']
'abc'
>>> dd['key2']
'N/A'

注意默认值是返回函数调用的,而函数在创建defaultdict对象时传入。

OrderedDict

使用dict时,key是无序的。在对dict做迭代时,我们无法确定Key的顺序,如果要保持Key的顺序,可以用OrderedDict

1
2
3
4
5
6
7
>>> from collections import OrderedDict
>>> d=dict([('a',1),('b',2),('c',3)])
>>> d
{'a': 1, 'b': 2, 'c': 3}
>>> od=OrderedDict([('a',1),('b',2),('c',3)])
>>> od
OrderedDict([('a', 1), ('b', 2), ('c', 3)])

注意,OrderedDict的Key会按照插入的顺序排列,不是Key本身排序:

1
2
3
4
5
6
7
8
>>> od=OrderedDict()
>>> od['z']=1
>>> od['y']=2
>>> od['a']=3
>>> od
OrderedDict([('z', 1), ('y', 2), ('a', 3)])
>>> list(od.keys())
['z', 'y', 'a']

OrderedDict可以实现一个FIFO的dict,当容量超出限制时,先删除最早添加的Key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> 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)-constainsKey>=self._capacity:
... last=self.popoitem(last=False)
... print('remove:',last)
... if constainsKey:
... del self[key]
... print('set:',(key,value))
... else:
... print('add:',(key,value))
... OrderdeDict.__setitem__(self,key,value)
...

Counter

Counter是一个简单的计数器,例如统计字符出现的个数:

1
2
3
4
5
6
7
>>> from collections import Counter
>>> c=Counter()
>>> for ch in 'programming':
... c[ch]=c[ch]+1
...
>>> c
Counter({'m': 2, 'g': 2, 'r': 2, 'o': 1, 'p': 1, 'n': 1, 'i': 1, 'a': 1})

Counter实际上也是dict的一个子类。


base64

Base64是一种用64个字符来表示任意二进制数据的方法。用记事本打开exe,jpg,pdf这些文件会看到一大堆乱码,因为二进制文件包含了很多无法显示和打印的字符,所以,如果让笔记本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。

Base64的原理很简单,首先,准备一个包含64个字符的数组:

[‘A’,’B’,’C’……’a’,’b’,’c’…….’0’,’1’,’2’…..’+’,’/‘]

然后,对二进制数据进行处理,每三个字节一组,一共是24个bit,划为四组,每组为6bit:
Alt text

这样我们得到4个数字作为索引,然后查表,获得相应的4个字符,就是编码后的字符串。

所以,base64编码会把三个字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文,网页等直接显示。如果要编码的二进制数据不是3的倍数,最后剩下的1个或2个字节可以用\x00字节在末尾补足后,再在编码的末尾加上一个或两个=号,表示补了多少个字节,解码的时候会自动去掉。

Python内置的base64可以直接进行base64的编解码:

1
2
3
4
5
>>> import base64
>>> base64.b64encode(b'binary\x00string')
b'YmluYXJ5AHN0cmluZw=='
>>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==')
b'binary\x00string'

由于标准的Base64编码后可能出现符号+和/,在URL中就不能直接作为参数,所以又有一种URL safe的base64编码,其实就是把+、/分别变为-和_:

1
2
3
4
5
6
>>> base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')
b'abcd++//'
>>> base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff')
b'abcd--__'
>>> base64.urlsafe_b64decode('abcd--__')
b'i\xb7\x1d\xfb\xef\xff'

Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。适用于小段内容的编码,比如数字证书签名,Cookie的内容等。

由于=字符也可能出现在Base64编码中,但=用在URL,Cookie里面会造成歧义,很多Base64编码后会把=去掉。

标准Base64:
‘abcd’ -> ‘YWJjZA==’
自动去掉=:
‘abcd’ -> ‘YWJjZA’

去掉之后解码时,由于Base64是把3个字节变为4个字节的,所以Base64编码的长度永远是4的倍数,加上=后就可以正常解码了。


struct

Python没有专门处理字节的数据类型,但是由于b'str'可以表示字节,所以,字节数组=二进制str。而在C语言中,我们可以很方便的使用struct、union等来处理字节,以及字节和int、float的转换。

在Python中,比如要把一个32位无符号整数编程字节,也就是四个长度的bytes ,需要配合运算符:

1
2
3
4
5
6
7
8
>>> n=10240099
>>> b1=(n&0xff000000)>>24
>>> b2=(n&0xff0000)>>16
>>> b3=(n&0xff00)>>8
>>> b4=n&0xff
>>> bs=bytes([b1,b2,b3,b4])
>>> bs
b'\x00\x9c@c'

很麻烦,如果换成浮点数就无能为力了。Python提供了一个struct模块来解决bytes和其他二进制数据类型的转换。structpack函数把任意数据变为bytes

1
2
3
>>> import struct
>>> struct.pack('>I',10240099)
b'\x00\x9c@c'

pack的第一个参数是处理指令,‘>I’的意思是:>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数。后面的参数个数要和处理指令一致。unpackbytes变成相应的数据类型:

1
2
>>> struct.unpack('>IH',b'\xf0\xf0\xf0\xf0\x80\x80')
(4042322160, 32896)

根据>IH的说明,后面的bytes一次变为I:4字节无符号数和H:2字节无符号整数。所以,尽管Python不适合编写底层操作字节流的代码,但在对性能要求不高的地方,利用struct 很方便。

hashlib

Python的hashlib提供了常见的摘要算法,如MD5、SHA1等等。

摘要算法

又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串。摘要算法就是通过摘要函数f()对任意的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。摘要算法之所以可以确定数据是否被篡改过是因为摘要函数是一个单向函数,计算f(data)很容易,但是通过digest 反推data很困难。对原始数据做一个bit的修改都会导致计算出的摘要完全不同。

以MD5为例,计算一个字符串的MD5值:

1
2
3
4
5
6
>>> import hashlib
>>>
>>> md5=hashlib.md5()
>>> md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
>>> print(md5.hexdigest())
d26a53750bc40b38b65a520292f69306

如果数据量很大,可以分块多次调用update(),最后计算的结果是一样的:

1
2
3
4
5
6
7
>>> import hashlib
>>>
>>> md5=hashlib.md5()
>>> md5.update('how to use md5 in'.encode('utf-8'))
>>> md5.update('python hashlib?'.encode('utf-8'))
>>> print(md5.hexdigest())
047b2e59ed7530a32db954459f82266b

MD5是最常见的摘要算法,速度很快,上观南城结果是固定的128bit字节,通常用一个32位的16进制字符串表示。

另一种常见的摘要算法是SHA1:

1
2
3
4
5
6
7
>>> import hashlib
>>>
>>> sha1=hashlib.sha1()
>>> sha1.update('how to use sha1 in '.encode('utf-8'))
>>> sha1.update('python hashlib?'.encode('utf-8'))
>>> print(sha1.hexdigest())
2c76b57293ce30acef38d98f6046927161b46a44

SHA1的结果是160bit字节,通常用一个40位的16进制字符来表示。比SHA1更安全的算法是SHA256和SHA512,越安全的算法越慢,摘要长度也会更长。

摘要算法应用

任何允许用户登录的网站都会存储用户登录的用户名和口令,存储的方法是存储到数据库表中:

name passwd
Michael 123456
bob asd121
alice alice123456

如果以明文保存用户口令,如果数据库泄露,所有用户的口令都泄露。而且网站运维人员是可以访问数据库的,也就是能获取到所有用户的口令。正确保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要,比如MD5:
| name | passwd |
| :——– | ——–:|
| Michael | e10adc3949ba59abbe56e057f20f883e |
|bob | 878ef96e86145580c38c87f0410ad153 |
|alice | 99b1c2188db85afee403b1536010c2c9|

当用户登录时,首先计算机用户输入的明文口令的MD5,然后和数据库的MD5对比,结果一致说明口令输入正确,如果不一致则口令错误。


itertools

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

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()负责把一个元素无限重复下去,不过提供第二个参数就可以限定重复次数:

1
2
3
4
5
6
7
8
9
10
11
>>> import itertools
>>>
>>> ns=itertools.repeat('a',4)
>>> for n in ns:
print(n)
a
a
a
a

无限序列只有在for迭代时才会无限地迭代下去,如果只是创建了一个迭代对象,它不会事先把无限个元素生成出来,事实上也不可能在内存中创建无限多个元素。无限序列虽然可以无限迭代下去,但是通常我们会通过takewhile()等函数根据条件判断来截取出一个有限的序列:

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

itertools提供的几个迭代器操作函数更加有用:

chain()

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

1
2
3
4
5
6
7
8
9
10
>>> for c in itertools.chain('abc','1234'):
print(c)
a
b
c
1
2
3

groupby()

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

1
2
3
4
5
6
7
8
9
>>> for key,group in itertools.groupby('aaabbbccaaadd'):
print(key,list(group))
a ['a', 'a', 'a']
b ['b', 'b', 'b']
c ['c', 'c']
a ['a', 'a', 'a']
d ['d', 'd']

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

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


XML

DOM vs SAX

操作XMl有两种方法:DOM和SAX,DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,缺点是我们需要自己处理事件。正常情况下优先考虑SAX。在Python中我们关心的事件是start_element,end_element,char_data准备好这三个函数就可以解析xml了。如当SAX解析器读到一个节点时:

python

会产生三个事件:

1.start_element事件,在读取时;
2.char_data事件,在读取python时
3.end_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
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.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml)

需要注意的是读取一大段字符串时,CharacterDataHandler可能被多次调用,所以需要自己保存起来,在EndElementHandler里面合并。


HTMLParser

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

HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或者SAX来解析HTML。可以用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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from html.parser import HTMLParser
from html.entities 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('<!--',data ,'-->')
def handle_entityref(self,name):
print('&%s:' %name)
def handle_charref(self,name):
print('&#%s:' %name)
parser=MyHTMLParser()
parser.feed('''<html>
<head></head>
<body>
<!-- test html parser-->
<p>Some<a href=\"#\">html</a> HTML&nbsp;tutorial...<br>END</p>
</body></html>''')

feed()方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。特殊字符有两种,一种是用英文表示的&nbsp,一种是用数字表示的&#1234;,这两种字符都可以通过Parser解析出来。


urllib

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

Get

urllib的request模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的相应:

例如,对豆瓣的一个URLhttps://api.douban.com/v2/book/2129650进行抓取并返回相应:

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
>>> from urllib import request
>>>
>>> with request.urlopen('https://api.douban.com/v2/book/2129650') as f:
... dada=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'))
...
Status: 200 OK
Date:Sun, 06 Nov 2016 08:44:01 GMT
Content-Type:application/json; charset=utf-8
Content-Length:2055
Connection:close
Vary:Accept-Encoding
X-Ratelimit-Remaining2:96
X-Ratelimit-Limit2:100
Expires:Sun, 1 Jan 2006 01:00:00 GMT
Pragma:no-cache
Cache-Control:must-revalidate, no-cache, private
Set-Cookie:bid=L9LbgN0KHkE; Expires=Mon, 06-Nov-17 08:44:01 GMT; Domain=.douban.com; Path=/
X-DOUBAN-NEWBID:L9LbgN0KHkE
X-DAE-Node:dis16
X-DAE-App:book
Server:dae
Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰编著"],"pubdate":"2007-6","tags":[{"count":20,"name":"spring","title":"spring"},{"count":11,"name":"Java","title":"Java"},{"count":6,"name":"javaee","title":"javaee"},{"count":4,"name":"j2ee","title":"j2ee"},{"count":3,"name":"POJO","title":"POJO"},{"count":3,"name":"计算机","title":"计算机"},{"count":3,"name":"藏书","title":"藏书"},{"count":3,"name":"编程","title":"编程"}],"origin_title":"","image":"https://img3.doubanio.com\/mpic\/s2648230.jpg","binding":"","translator":[],"catalog":"","pages":"509","images":{"small":"https://img3.doubanio.com\/spic\/s2648230.jpg","large":"https://img3.doubanio.com\/lpic\/s2648230.jpg","medium":"https://img3.doubanio.com\/mpic\/s2648230.jpg"},"alt":"https:\/\/book.douban.com\/subject\/2129650\/","id":"2129650","publisher":"电子工业","isbn10":"7121042622","isbn13":"9787121042621","title":"Spring 2.0核心技术与最佳实践","url":"https:\/\/api.douban.com\/v2\/book\/2129650","alt_title":"","author_intro":"","summary":"本书注重实践而又深入 理论,由浅入深且详细介绍了Spring 2.0框架的几乎全部的内容,并重点突出2.0版本的新特性。本书将为读者展示如何应用Spring 2.0框架创建灵活高效的JavaEE应用,并提供了一个真正 可直接部署的完整的Web应用程序——Live在线书店(http:\/\/www.livebookstore.net)。\n在介绍Spring框架的同时,本书还介绍了与Spring相关的大量第三方框架,涉及领域全面,实用 性强。本书另一大特色是实用性强,易于上手,以实际项目为出发点,介绍项目开发中应遵循 的最佳开发模式。\n本书还介绍了大量实践性极强的例子,并给出了完整的配置步骤,几乎覆 盖了Spring 2.0版本的新特性。\n本书适合有一定Java基础的读者,对JavaEE开发人员特别有 帮助。本书既可以作为Spring 2.0的学习指南,也可以作为实际项目开发的参考手册。","price":"59.80元"}

可以看到HTTP相应的头和JSON数据,如果我们要模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone6区请求豆瓣首页:

1
2
3
4
5
6
7
8
9
10
>>> 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的移动版网页。

Post

如果要以POST发送一个请求,只需要把参数data以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'))

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请求。如果要模拟浏览器完成特定功能,需要把请求伪装成浏览器。伪装的方法是先监控浏览器发出的请求,再根据浏览器的请求头来伪装,User-Agent头就是用来标示浏览器的。

Donate comment here