Python 3开发网络爬虫

Python 3开发网络爬虫

由于本学期好多神都选了Cisco网络课, 而我这等弱渣没选, 去蹭了一节发现讲的内容虽然我不懂但是还是无爱. 我想既然都本科就出来工作还是按照自己爱好来点技能吧, 于是我就不去了. 一个人在宿舍没有点计划好的事情做就会很容易虚度, 正好这个学期主打网络与数据库开发, 那就先学学Python开发爬虫吧. 我失散多年的好朋友Jay Loong突然说他会爬虫了, 我感到真棒, 我也要学 😀 因为一个星期有两节Cisco课, 所以本系列博文也就一周两更.

 

选择一门语言

爬虫可以用各种语言写, C++, Java都可以, 为什么要Python? 首先用C++搞网络开发的例子不多(可能是我见得太少), 然后由于Oracle收购了Sun, Java目前虽然在Android开发上很重要, 但是如果Google官司进展不顺利, 那么很有可能用Go语言替代掉Java来做Android开发. 在这计算机速度高速增长的年代里, 选语言都要看他爹的业绩, 真是稍不注意就落后于时代. 随着计算机速度的高速发展, 某种语言开发的软件运行的时间复杂度的常数系数已经不像以前那么重要, 我们可以越来越偏爱为程序员打造的而不是为计算机打造的语言. 比如Ruby这种传说中的纯种而又飘逸的的OOP语言, 或者Python这种稍严谨而流行库又非常多的语言, 都大大弱化了针对计算机运行速度而打造的特性, 强化了为程序员容易思考而打造的特性. 所以我选择Python.

 

 

选择Python版本

有2和3两个版本, 3比较新, 听说改动大. 根据我在知乎上搜集的观点来看, 我还是倾向于使用”在趋势中将会越来越火”的版本, 而非”目前已经很稳定而且很成熟”的版本. 这是个人喜好, 而且预测不一定准确. 但是如果Python3无法像Python2那么火, 那么整个Python语言就不可避免的随着时间的推移越来越落后, 因此我想其实选哪个的最坏风险都一样, 但是最好回报却是Python3的大. 其实两者区别也可以说大也可以说不大, 最终都不是什么大问题. 我选择的是Python 3.

 

选择参考资料

由于我是一边学一边写, 而不是我完全学会了之后才开始很有条理的写, 所以参考资料就很重要(本来应该是个人开发经验很重要, 但我是零基础).

写到这里的时候, 上面第二第三个链接的票数第一的回答已经看完了, 他们提到的有些部分(比如爬行的路线不能有回路)我就不写了.

 

一个简单的伪代码

以下这个简单的伪代码用到了set和queue这两种经典的数据结构, 集与队列. 集的作用是记录那些已经访问过的页面, 队列的作用是进行广度优先搜索.

1
2
3
4
5
6
7
8
9
10
11
queue Q
set S
StartPoint = "http://jecvay.com"
Q.push(StartPoint)  # 经典的BFS开头
S.insert(StartPoint)  # 访问一个页面之前先标记他为已访问
while (Q.empty() == false)  # BFS循环体
  T = Q.top()  # 并且pop
  for point in PageUrl(T)  # PageUrl(T)是指页面T中所有url的集合, point是这个集合中的一个元素.
    if (point not in S)
      Q.push(point)
      S.insert(point)

这个伪代码不能执行,  我觉得我写的有的不伦不类, 不类Python也不类C++.. 但是我相信看懂是没问题的, 这就是个最简单的BFS结构. 我是看了知乎里面的那个伪代码之后, 自己用我的风格写了一遍. 你也需要用你的风格写一遍.

这里用到的Set其内部原理是采用了Hash表, 传统的Hash对爬虫来说占用空间太大, 因此有一种叫做Bloom Filter的数据结构更适合用在这里替代Hash版本的set. 我打算以后再看这个数据结构怎么使用, 现在先跳过, 因为对于零基础的我来说, 这不是重点.

 

代码实现(一): 用Python抓取指定页面

我使用的编辑器是Idle, 安装好Python3后这个编辑器也安装好了, 小巧轻便, 按一个F5就能运行并显示结果. 代码如下:

1
2
3
4
5
6
7
#encoding:UTF-8
import urllib.request
url = "http://www.baidu.com"
data = urllib.request.urlopen(url).read()
data = data.decode('UTF-8')
print(data)

 

urllib.request是一个库, 隶属urllib. 点此打开官方相关文档. 官方文档应该怎么使用呢? 首先点刚刚提到的这个链接进去的页面有urllib的几个子库, 我们暂时用到了request, 所以我们先看urllib.request部分. 首先看到的是一句话介绍这个库是干什么用的:

The urllib.request module defines functions and classes which help in opening URLs (mostly HTTP) in a complex world — basic and digest authentication, redirections, cookies and more.

然后把我们代码中用到的urlopen()函数部分阅读完.

urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False)

重点部分是返回值, 这个函数返回一个 http.client.HTTPResponse 对象, 这个对象又有各种方法, 比如我们用到的read()方法, 这些方法都可以根据官方文档的链接链过去. 根据官方文档所写, 我用控制台运行完毕上面这个程序后, 又继续运行如下代码, 以更熟悉这些乱七八糟的方法是干什么的.

1
2
3
4
5
6
7
8
9
10
11
12
>>> a = urllib.request.urlopen(full_url)
>>> type(a)
<class ‘http.client.HTTPResponse’>
>>> a.geturl()
‘http://www.baidu.com/s?word=Jecvay’
>>> a.info()
<http.client.HTTPMessage object at 0x03272250>
>>> a.getcode()
200

代码实现(二): 用Python简单处理URL

如果要抓取百度上面搜索关键词为Jecvay Notes的网页, 则代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
import urllib
import urllib.request
data={}
data['word']='Jecvay Notes'
url_values=urllib.parse.urlencode(data)
url="http://www.baidu.com/s?"
full_url=url+url_values
data=urllib.request.urlopen(full_url).read()
data=data.decode('UTF-8')
print(data)

data是一个字典, 然后通过urllib.parse.urlencode()来将data转换为 ‘word=Jecvay+Notes’的字符串, 最后和url合并为full_url, 其余和上面那个最简单的例子相同. 关于urlencode(), 同样通过官方文档学习一下他是干什么的. 通过查看

  1. urllib.parse.urlencode(query, doseq=False, safe=”, encoding=None, errors=None)
  2. urllib.parse.quote_plus(string, safe=”, encoding=None, errors=None)

大概知道他是把一个通俗的字符串, 转化为url格式的字符串.

上一回, 我学会了

  1. 用伪代码写出爬虫的主要框架;
  2. 用Python的urllib.request库抓取指定url的页面;
  3. 用Python的urllib.parse库对普通字符串转符合url的字符串.

这一回, 开始用Python将伪代码中的所有部分实现. 由于文章的标题就是”零基础”, 因此会先把用到的两种数据结构队列集合介绍一下. 而对于”正则表达式“部分, 限于篇幅不能介绍, 但给出我比较喜欢的几个参考资料.

 

Python的队列

在爬虫程序中, 用到了广度优先搜索(BFS)算法. 这个算法用到的数据结构就是队列.

Python的List功能已经足够完成队列的功能, 可以用 append() 来向队尾添加元素, 可以用类似数组的方式来获取队首元素, 可以用 pop(0) 来弹出队首元素. 但是List用来完成队列功能其实是低效率的, 因为List在队首使用 pop(0) 和 insert() 都是效率比较低的, Python官方建议使用collection.deque来高效的完成队列任务.

1
2
3
4
5
6
7
8
9
10
from collections import deque
queue = deque(["Eric", "John", "Michael"])
queue.append("Terry")           # Terry 入队
queue.append("Graham")          # Graham 入队
queue.popleft()                 # 队首元素出队
#输出: 'Eric'
queue.popleft()                 # 队首元素出队
#输出: 'John'
queue                           # 队列中剩下的元素
#输出: deque(['Michael', 'Terry', 'Graham'])

(以上例子引用自官方文档)

 

Python的集合

在爬虫程序中, 为了不重复爬那些已经爬过的网站, 我们需要把爬过的页面的url放进集合中, 在每一次要爬某一个url之前, 先看看集合里面是否已经存在. 如果已经存在, 我们就跳过这个url; 如果不存在, 我们先把url放入集合中, 然后再去爬这个页面.

Python提供了set这种数据结构. set是一种无序的, 不包含重复元素的结构. 一般用来测试是否已经包含了某元素, 或者用来对众多元素们去重. 与数学中的集合论同样, 他支持的运算有交, 并, 差, 对称差.

创建一个set可以用 set() 函数或者花括号 {} . 但是创建一个空集是不能使用一个花括号的, 只能用 set() 函数. 因为一个空的花括号创建的是一个字典数据结构. 以下同样是Python官网提供的示例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
>>> print(basket)                      # 这里演示的是去重功能
{'orange', 'banana', 'pear', 'apple'}
>>> 'orange' in basket                 # 快速判断元素是否在集合内
True
>>> 'crabgrass' in basket
False
>>> # 下面展示两个集合间的运算.
...
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a                                 
{'a', 'r', 'b', 'c', 'd'}
>>> a - b                              # 集合a中包含元素
{'r', 'd', 'b'}
>>> a | b                              # 集合a或b中包含的所有元素
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b                              # 集合a和b中都包含了的元素
{'a', 'c'}
>>> a ^ b                              # 不同时包含于a和b的元素
{'r', 'd', 'b', 'm', 'z', 'l'}

其实我们只是用到其中的快速判断元素是否在集合内的功能, 以及集合的并运算.

 

Python的正则表达式

在爬虫程序中, 爬回来的数据是一个字符串, 字符串的内容是页面的html代码. 我们要从字符串中, 提取出页面提到过的所有url. 这就要求爬虫程序要有简单的字符串处理能力, 而正则表达式可以很轻松的完成这一任务.

参考资料

虽然正则表达式功能异常强大, 很多实际上用的规则也非常巧妙, 真正熟练正则表达式需要比较长的实践锻炼. 不过我们只需要掌握如何使用正则表达式在一个字符串中, 把所有的url都找出来, 就可以了. 如果实在想要跳过这一部分, 可以在网上找到很多现成的匹配url的表达式, 拿来用即可.

 

Python网络爬虫Ver 1.0 alpha

有了以上铺垫, 终于可以开始写真正的爬虫了. 我选择的入口地址是Fenng叔的Startup News, 我想Fenng叔刚刚拿到7000万美金融资, 不会介意大家的爬虫去光临他家的小站吧. 这个爬虫虽然可以勉强运行起来, 但是由于缺乏异常处理, 只能爬些静态页面, 也不会分辨什么是静态什么是动态, 碰到什么情况应该跳过, 所以工作一会儿就要败下阵来.

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
import re
import urllib.request
import urllib
from collections import deque
queue = deque()
visited = set()
url = 'http://news.dbanotes.net'  # 入口页面, 可以换成别的
queue.append(url)
cnt = 0
while queue:
  url = queue.popleft()  # 队首元素出队
  visited |= {url}  # 标记为已访问
  print('已经抓取: ' + str(cnt) + '   正在抓取 <---  ' + url)
  cnt += 1
  urlop = urllib.request.urlopen(url)
  if 'html' not in urlop.getheader('Content-Type'):
    continue
  # 避免程序异常中止, 用try..catch处理异常
  try:
    data = urlop.read().decode('utf-8')
  except:
    continue
  # 正则表达式提取页面中所有队列, 并判断是否已经访问过, 然后加入待爬队列
  linkre = re.compile('href=\"(.+?)\"')
  for x in linkre.findall(data):
    if 'http' in x and x not in visited:
      queue.append(x)
      print('加入队列 --->  ' + x)

这个版本的爬虫使用的正则表达式是

1
'href=\"(.+?)\"'

所以会把那些.ico或者.jpg的链接都爬下来. 这样read()了之后碰上decode(‘utf-8′)就要抛出异常. 因此我们用getheader()函数来获取抓取到的文件类型, 是html再继续分析其中的链接.

1
2
if 'html' not in urlop.getheader('Content-Type'):
    continue

但是即使是这样, 依然有些网站运行decode()会异常. 因此我们把decode()函数用try..catch语句包围住, 这样他就不会导致程序中止. 程序运行效果图如下:

a70136d7d0a8cac98b48b65566f8178b

 

下回预告

爬虫是可以工作了, 但是在碰到连不上的链接的时候, 它并不会超时跳过. 而且爬到的内容并没有进行处理, 没有获取对我们有价值的信息, 也没有保存到本地. 下次我们可以完善这个alpha版本.

上一次我自学爬虫的时候, 写了一个简陋的勉强能运行的爬虫alpha. alpha版有很多问题. 比如一个网站上不了, 爬虫却一直在等待连接返回response, 不知道超时跳过; 或者有的网站专门拦截爬虫程序, 我们的爬虫也不会伪装自己成为浏览器正规部队; 并且抓取的内容没有保存到本地, 没有什么作用. 这次我们一个个解决这些小问题.

此外, 在我写这系列文章的第二篇的时候, 我还是一个对http的get和post以及response这些名词一无所知的人, 但是我觉得这样是写不好爬虫的. 于是我参考了 <<计算机网络–自顶向下方法>> 这本书的第二章的大部分内容. 如果你也一样对http的机制一无所知, 我也推荐你找一找这方面的资料来看. 在看的过程中, 安装一个叫做Fiddler的软件, 边学边实践, 观察浏览器是如何访问一个网站的, 如何发出请求, 如何处理响应, 如何进行跳转, 甚至如何通过登录认证. 有句老话说得好, 越会用Fiddler, 就对理论理解更深刻; 越对理论理解深刻, Fiddler就用得越顺手. 最后我们在用爬虫去做各种各样的事情的时候, Fiddler总是最得力的助手之一.

 

 

添加超时跳过功能

首先, 我简单地将

1
urlop = urllib.request.urlopen(url)

改为

1
urlop = urllib.request.urlopen(url, timeout = 2)

运行后发现, 当发生超时, 程序因为exception中断. 于是我把这一句也放在try .. except 结构里, 问题解决.

支持自动跳转

在爬 http://baidu.com 的时候, 爬回来一个没有什么内容的东西, 这个东西告诉我们应该跳转到 http://www.baidu.com . 但是我们的爬虫并不支持自动跳转, 现在我们来加上这个功能, 让爬虫在爬 baidu.com 的时候能够抓取 www.baidu.com 的内容.

首先我们要知道爬 http://baidu.com 的时候他返回的页面是怎么样的, 这个我们既可以用 Fiddler 看, 也可以写一个小爬虫来抓取. 这里我抓到的内容如下, 你也应该尝试一下写几行 python 来抓一抓.

1
2
3
<html>
<meta http-equiv=”refresh” content=”0;url=http://www.baidu.com/”>
</html>

看代码我们知道这是一个利用 html 的 meta 来刷新与重定向的代码, 其中的0是等待0秒后跳转, 也就是立即跳转. 这样我们再像上一次说的那样用一个正则表达式把这个url提取出来就可以爬到正确的地方去了. 其实我们上一次写的爬虫已经可以具有这个功能, 这里只是单独拿出来说明一下 http 的 meta 跳转.

 

 

伪装浏览器正规军

前面几个小内容都写的比较少. 现在详细研究一下如何让网站们把我们的Python爬虫当成正规的浏览器来访. 因为如果不这么伪装自己, 有的网站就爬不回来了. 如果看过理论方面的知识, 就知道我们是要在 GET 的时候将 User-Agent 添加到header里.

如果没有看过理论知识, 按照以下关键字搜索学习吧 😀

  • HTTP 报文分两种: 请求报文响应报文
  • 请求报文的请求行首部行
  • GETPOST, HEAD, PUT, DELETE 方法

我用 IE 浏览器访问百度首页的时候, 浏览器发出去的请求报文如下:

1
2
3
4
5
6
7
8
9
GET http://www.baidu.com/ HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: www.baidu.com
DNT: 1
Connection: Keep-Alive
Cookie: BAIDUID=57F4D171573A6B88A68789EF5DDFE87:FG=1; uc_login_unique=ccba6e8d978872d57c7654130e714abd; BD_UPN=11263145; BD

然后百度收到这个消息后, 返回给我的的响应报文如下(有删节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HTTP/1.1 200 OK
Date: Mon, 29 Sep 2014 13:07:01 GMT
Content-Type: text/html; charset=utf-8
Connection: Keep-Alive
Vary: Accept-Encoding
Cache-Control: private
Cxy_all: baidu+8b13ba5a7289a37fb380e0324ad688e7
Expires: Mon, 29 Sep 2014 13:06:21 GMT
X-Powered-By: HPHP
Server: BWS/1.1
BDPAGETYPE: 1
BDQID: 0x8d15bb610001fe79
BDUSERID: 0
Set-Cookie: BDSVRTM=0; path=/
Set-Cookie: BD_HOME=0; path=/
Content-Length: 80137
<!DOCTYPE html><!–STATUS OK–><html><head><meta http-equiv=”content-type” content=”text/html;charset=utf-8″><meta http-equiv=”X-UA-Compatible” content=”IE=Edge”><link rel=”dns-prefetch” href=”//s1.bdstatic.com”/><link rel=”dns-prefetch” href=”//t1.baidu.com”/><link rel=”dns-prefetch” href=”//t2.baidu.com”/><link rel=”dns-prefetch” href=”//t3.baidu.com”/><link rel=”dns-prefetch” href=”//t10.baidu.com”/><link rel=”dns-prefetch” href=”//t11.baidu.com”/><link rel=”dns-prefetch” href=”//t12.baidu.com”/><link rel=”dns-prefetch” href=”//b1.bdstatic.com”/><title>百度一下,你就知道</title><style index=”index” > ……….这里省略两万字……………. </script></body></html>

如果能够看懂这段话的第一句就OK了, 别的可以以后再配合 Fiddler 慢慢研究. 所以我们要做的就是在 Python 爬虫向百度发起请求的时候, 顺便在请求里面写上 User-Agent, 表明自己是浏览器君.

在 GET 的时候添加 header 有很多方法, 下面介绍两种方法.

第一种方法比较简便直接, 但是不好扩展功能, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
import urllib.request
 
url = 'http://www.baidu.com/'
req = urllib.request.Request(url, headers = {
    'Connection': 'Keep-Alive',
    'Accept': 'text/html, application/xhtml+xml, */*',
    'Accept-Language': 'en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3',
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko'
})
oper = urllib.request.urlopen(req)
data = oper.read()
print(data.decode())

第二种方法使用了 build_opener 这个方法, 用来自定义 opener, 这种方法的好处是可以方便的拓展功能, 例如下面的代码就拓展了自动处理 Cookies 的功能.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import urllib.request
import http.cookiejar
 
# head: dict of header
def makeMyOpener(head = {
    'Connection': 'Keep-Alive',
    'Accept': 'text/html, application/xhtml+xml, */*',
    'Accept-Language': 'en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3',
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko'
}):
    cj = http.cookiejar.CookieJar()
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
    header = []
    for key, value in head.items():
        elem = (key, value)
        header.append(elem)
    opener.addheaders = header
    return opener
 
oper = makeMyOpener()
uop = oper.open('http://www.baidu.com/', timeout = 1000)
data = uop.read()
print(data.decode())

上述代码运行后通过 Fiddler 抓到的 GET 报文如下所示:

1
2
3
4
5
6
7
GET http://www.baidu.com/ HTTP/1.1
Accept-Encoding: identity
Connection: close
Host: www.baidu.com
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept: text/html, application/xhtml+xml, */*
Accept-Language: en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3

可见我们在代码里写的东西都添加到请求报文里面了.

 

保存抓回来的报文

顺便说说文件操作. Python 的文件操作还是相当方便的. 我们可以讲抓回来的数据 data 以二进制形式保存, 也可以经过 decode() 处理成为字符串后以文本形式保存. 改动一下打开文件的方式就能用不同的姿势保存文件了. 下面是参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
def saveFile(data):
    save_path = 'D:\\temp.out'
    f_obj = open(save_path, 'wb') # wb 表示打开方式
    f_obj.write(data)
    f_obj.close()
 
# 这里省略爬虫代码
# ...
 
# 爬到的数据放到 dat 变量里
# 将 dat 变量保存到 D 盘下
saveFile(dat)

下回我们会用 Python 来爬那些需要登录之后才能看到的信息. 在那之前, 我已经对 Fiddler 稍微熟悉了. 希望一起学习的也提前安装个 Fiddler 玩一下.

今天的工作很有意思, 我们用 Python 来登录网站, 用Cookies记录登录信息, 然后就可以抓取登录之后才能看到的信息. 今天我们拿知乎网来做示范. 为什么是知乎? 这个很难解释, 但是肯定的是知乎这么大这么成功的网站完全不用我来帮他打广告. 知乎网的登录比较简单, 传输的时候没有对用户名和密码加密, 却又不失代表性, 有一个必须从主页跳转登录的过程.

不得不说一下, Fiddler 这个软件是 Tpircsboy 告诉我的. 感谢他给我带来这么好玩的东西.

 

第一步: 使用 Fiddler 观察浏览器行为

在开着 Fiddler 的条件下运行浏览器, 输入知乎网的网址 http://www.zhihu.com 回车后到 Fiddler 中就能看到捕捉到的连接信息. 在左边选中一条 200 连接, 在右边打开 Inspactors 透视图, 上方是该条连接的请求报文信息, 下方是响应报文信息.
其中 Raw 标签是显示报文的原文. 下方的响应报文很有可能是没有经过解压或者解码的, 这种情况他会在中间部位有一个小提示, 点击一下就能解码显示出原文了.

 

以上这个截图是在未登录的时候进入 http://www.zhihu.com 得到的. 现在我们来输入用户名和密码登陆知乎网, 再看看浏览器和知乎服务器之间发生了什么.

 

点击登陆后, 回到 Fiddler 里查看新出现的一个 200 链接. 我们浏览器携带者我的帐号密码给知乎服务器发送了一个 POST, 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST http://www.zhihu.com/login HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
X-Requested-With: XMLHttpRequest
Referer: http://www.zhihu.com/#signin
Accept-Language: en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 6.4; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Length: 97
DNT: 1
Host: www.zhihu.com
Connection: Keep-Alive
Pragma: no-cache
Cookie: __utma=51854390.1539896551.1412320246.1412320246.1412320246.1; __utmb=51854390.6.10.1412320246; __utmc=51854390; __utmz=51854390.1412320246.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utmv=51854390.000–|3=entry_date=20141003=1
_xsrf=4b41f6c7a9668187ccd8a610065b9718&email=此处涂黑%40gmail.com&password=此处不可见&rememberme=y

截图如下:

我的浏览器给 http://www.zhihu.com/login 这个网址(多了一个/login) 发送了一个POST, 内容都已经在上面列出来了, 有用户名, 有密码, 有一个”记住我”的 yes, 其中这个 WebForms 标签下 Fiddler 能够比较井井有条的列出来 POST 的内容. 所以我们用 Python 也发送相同的内容就能登录了. 但是这里出现了一个 Name 为 _xsrf 的项, 他的值是 4b41f6c7a9668187ccd8a610065b9718. 我们要先获取这个值, 然后才能给他发.

浏览器是如何获取的呢, 我们刚刚是先访问了 http://www.zhihu.com/ 这个网址, 就是首页, 然后登录的时候他却给 http://www.zhihu.com/login 这个网址发信息. 所以用侦探一般的思维去思考这个问题, 就会发现肯定是首页把 _xsrf 生成发送给我们, 然后我们再把这个 _xsrf 发送给 /login 这个 url. 这样一会儿过后我们就要从第一个 GET 得到的响应报文里面去寻找 _xsrf

截图下方的方框说明, 我们不仅登录成功了, 而且服务器还告诉我们的浏览器如何保存它给出的 Cookies 信息. 所以我们也要用 Python 把这些 Cookies 信息记录下来.

这样 Fiddler 的工作就基本结束了!

 

第二步: 解压缩

简单的写一个 GET 程序, 把知乎首页 GET 下来, 然后 decode() 一下解码, 结果报错. 仔细一看, 发现知乎网传给我们的是经过 gzip 压缩之后的数据. 这样我们就需要先对数据解压. Python 进行 gzip 解压很方便, 因为内置有库可以用. 代码片段如下:

1
2
3
4
5
6
7
8
9
import gzip
def ungzip(data):
    try:        # 尝试解压
        print('正在解压.....')
        data = gzip.decompress(data)
        print('解压完毕!')
    except:
        print('未经压缩, 无需解压')
    return data

通过 opener.read() 读取回来的数据, 经过 ungzip 自动处理后, 再来一遍 decode() 就可以得到解码后的 str 了

 

第二步: 使用正则表达式获取沙漠之舟

_xsrf 这个键的值在茫茫无际的互联网沙漠之中指引我们用正确的姿势来登录知乎, 所以 _xsrf 可谓沙漠之舟. 如果没有 _xsrf, 我们或许有用户名和密码也无法登录知乎(我没试过, 不过我们学校的教务系统确实如此) 如上文所说, 我们在第一遍 GET 的时候可以从响应报文中的 HTML 代码里面得到这个沙漠之舟. 如下函数实现了这个功能, 返回的 str 就是 _xsrf 的值.

1
2
3
4
5
import re
def getXSRF(data):
    cer = re.compile('name=\"_xsrf\" value=\"(.*)\"', flags = 0)
    strlist = cer.findall(data)
    return strlist[0]

第三步: 发射 POST !!

集齐 _xsrf, id, password 三大法宝, 我们可以发射 POST 了. 这个 POST 一旦发射过去, 我们就登陆上了服务器, 服务器就会发给我们 Cookies. 本来处理 Cookies 是个麻烦的事情, 不过 Python 的 http.cookiejar 库给了我们很方便的解决方案, 只要在创建 opener 的时候将一个 HTTPCookieProcessor 放进去, Cookies 的事情就不用我们管了. 下面的代码体现了这一点.

1
2
3
4
5
6
7
8
9
10
11
12
13
import http.cookiejar
import urllib.request
def getOpener(head):
    # deal with the Cookies
    cj = http.cookiejar.CookieJar()
    pro = urllib.request.HTTPCookieProcessor(cj)
    opener = urllib.request.build_opener(pro)
    header = []
    for key, value in head.items():
        elem = (key, value)
        header.append(elem)
    opener.addheaders = header
    return opener

getOpener 函数接收一个 head 参数, 这个参数是一个字典. 函数把字典转换成元组集合, 放进 opener. 这样我们建立的这个 opener 就有两大功能:

自动处理使用 opener 过程中遇到的 Cookies
自动在发出的 GET 或者 POST 请求中加上自定义的 Header

第四部: 正式运行

正式运行还差一点点, 我们要把要 POST 的数据弄成 opener.open() 支持的格式. 所以还要 urllib.parse 库里的 urlencode() 函数. 这个函数可以把 字典 或者 元组集合 类型的数据转换成 & 连接的 str.

str 还不行, 还要通过 encode() 来编码, 才能当作 opener.open() 或者 urlopen() 的 POST 数据参数来使用. 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
url = 'http://www.zhihu.com/'
opener = getOpener(header)
op = opener.open(url)
data = op.read()
data = ungzip(data)     # 解压
_xsrf = getXSRF(data.decode())
 
url += 'login'
id = '这里填你的知乎帐号'
password = '这里填你的知乎密码'
postDict = {
        '_xsrf':_xsrf,
        'email': id,
        'password': password,
        'rememberme': 'y'
}
postData = urllib.parse.urlencode(postDict).encode()
op = opener.open(url, postData)
data = op.read()
data = ungzip(data)
 
print(data.decode())  # 你可以根据你的喜欢来处理抓取回来的数据了!

代码运行后, 我们发现自己关注的人的动态(显示在登陆后的知乎首页的那些), 都被抓取回来了. 下一步做一个统计分析器, 或者自动推送器, 或者内容分级自动分类器, 都可以.

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import gzip
import re
import http.cookiejar
import urllib.request
import urllib.parse
 
def ungzip(data):
    try:        # 尝试解压
        print('正在解压.....')
        data = gzip.decompress(data)
        print('解压完毕!')
    except:
        print('未经压缩, 无需解压')
    return data
 
def getXSRF(data):
    cer = re.compile('name=\"_xsrf\" value=\"(.*)\"', flags = 0)
    strlist = cer.findall(data)
    return strlist[0]
 
def getOpener(head):
    # deal with the Cookies
    cj = http.cookiejar.CookieJar()
    pro = urllib.request.HTTPCookieProcessor(cj)
    opener = urllib.request.build_opener(pro)
    header = []
    for key, value in head.items():
        elem = (key, value)
        header.append(elem)
    opener.addheaders = header
    return opener
 
header = {
    'Connection': 'Keep-Alive',
    'Accept': 'text/html, application/xhtml+xml, */*',
    'Accept-Language': 'en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3',
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko',
    'Accept-Encoding': 'gzip, deflate',
    'Host': 'www.zhihu.com',
    'DNT': '1'
}
 
url = 'http://www.zhihu.com/'
opener = getOpener(header)
op = opener.open(url)
data = op.read()
data = ungzip(data)     # 解压
_xsrf = getXSRF(data.decode())
 
url += 'login'
id = '这里填你的知乎帐号'
password = '这里填你的知乎密码'
postDict = {
        '_xsrf':_xsrf,
        'email': id,
        'password': password,
        'rememberme': 'y'
}
postData = urllib.parse.urlencode(postDict).encode()
op = opener.open(url, postData)
data = op.read()
data = ungzip(data)
 
print(data.decode())
Comments are closed.