使用python编写简单网络爬虫技巧总结

使用python写过几个小玩具,基本上都与爬虫、Web相关的,自己也积累了下经验,也看过很多文章,一直想总结下,可惜现在要忙于找工作,实验室活也比较多,也就落下了。感觉如果时间长不记录,也就懒散了,并且许多东西我写完过个两天,我自己都记不住当时怎么想的了。

0、HTTP协议

基本上常见的Web开发里,Web内容都是通过HTTP协议进行传输的(虽然咱不懂Web开发,但是基本的计算机网络知识还是了解的),通过TCP连接服务器的80端口,爬虫其实质就是通过模拟浏览器发送HTTP请求,至于HTTP请求相关知识,点击这里

1
2
3
4
HTTP通常通过创建到服务器80端口的TCP连接进行通信
HTTP协议的内容包括请求方式(method), urlheaderbody,通常以纯文本方式发送
HTTP返回内容包括状态码,headerbody,通常以纯文本方式返回
header以及body间以CRLF(\r\n)分割

1、最基础的抓取网站内容

使用python编写一个网络爬虫是非常简单的,如下例所示:

1
2
3
import urllib2
content = urllib2.urlopen('http://armsword.com').read()

但是实际工作中,这样肯定是不行的,你会遇到各种各样的问题,如下:

  • 网络出现错误,任何错误都可能。例如机器宕了,网线断了,域名出错了,网络超时了,页面没有了,网站跳转了,服务被禁了,主机负载不够了…
  • 服务器加上了限制,只让常见浏览器访问
  • 需要登录才能抓取数据
  • IP被封掉或者IP访问次数受到限制
  • 服务器加上了防盗链的限制
  • 某些网站不管你HTTP请求里有没有Accept-Encoding头部,也不管你头部具体内容是什么,反正总给你发gzip后的内容
  • URL链接千奇百怪,带汉字的也罢了,有的甚至还有回车换行
  • 某些网站HTTP头部里有一个Content-Type,网页里有好几个Content-Type,更过分的是,各个Content-Type还不一样,最过分的是,这些Content-Type可能都不是正文里使用的Content-Type,从而导致乱码
  • 需要抓取的数据很多,怎么提高抓取效率
  • 网站内容通过AJAX请求加载的
  • 需要验证码识别
    至于怎么解决上述问题,我们一一道来。

2、网络或网站出现错误等处理

之前我搞过百度音乐的爬虫,因为当时百度音乐有个bug,就是可以任何人都下载百度音乐的无损、高品质音乐,我就写了个爬虫,开了几十个进程抓取下载地址(抓包解析JSON串数据)存储到数据库里,虽然百度无ip访问限制这些麻烦事,但是程序无任何错误处理的情况下,依然出现各自异常错误,所以我们只需要捕捉异常,处理异常就可以了。我们可以根据urlopen的返回值,读取它的HTTP状态码。除此之外,我们在urlopen的时候,也需要设置timeout参数,以保证处理好超时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import urllib2
import socket

try:
f = urllib2.urlopen('http://armsword', timeout = 10)
code = f.getcode()
if code < 200 or code >= 300:
#你自己的HTTP错误处理
except Exception, e:
if isinstance(e, urllib2.HTTPError):
print 'http error: {0}'.format(e.code)
elif isinstance(e, urllib2.URLError) and isinstance(e.reason, socket.timeout):
print 'url error: socket timeout {0}'.format(e.__str__())
else:
print 'error: ' + e.__str__()

3、服务器做了限制

3.1、防盗连限制

某些站点有所谓的反盗链设置,其实说穿了很简单,就是检查你发送请求的header里面,referer站点是不是他自己,我们只要把headers的referer改成该网站即可,以V2EX的领取每日奖励地址为例:

1
2
3
4
5
header = {'Referer':'http://www.v2ex.com/signin'}
req = urllib2.Request('http://www.v2ex.com',headers = header)
response = urllib2.urlopen(url = req,timeout = 10).read
#response = urllib2.urlopen(req)).read()

3.2、限制浏览器登录,需伪装成浏览器

某些网站做了限制,进制爬虫的访问,此时我们可以更改HTTP的header,与3.1相似:

1
2
3
4
header = {&quot;User-Agent&quot;:&quot;Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36
(KHTML, like Gecko) Ubuntu Chromium/31.0.1650.63 Chrome/31.0.1650.63 Safari/537.36&quot;}
req = urllib2.Request(url,headers=header)

3.3、IP访问次数受到限制

如果IP被封掉、或者访问次数受到限制,使用代理服务器比较有用,代理IP可以自己抓取一些代理网站的数据:

1
2
3
4
5
6
import urllib2
proxy_ip = urllib2.ProxyHandler({'http':'http://XX.XX.XX.XX:XXXX'}) #XX为代理IP和端口
opener = urllib2.build_opener(proxy_ip, urllib2.HTTPHandler)
urllib2.install_opener(opener)
content = urllib2.urlopen('http://XXXX.com').read()

4、需要登录才能抓取数据

4.1、表单的处理

抓包,Chrome或者Firefox+Httpfox浏览器插件就能办到,查看POST数据,模拟表单就可以了,这里不再细说,这里主要注意,在GET/POST一些数据的时候,需要对GET/POST的数据做些编码,即使用urlencode(),我们以北邮校园网关登录为例:

1
2
3
4
5
6
7
8
9
10
11
postdata = {
'DDDDD': uname, #用户名
'upass': u_pass, #密码
'R1': 0, #其他参数
'R2': 1,
'para': 00,
'0MKKey': 123456
}

en_url = urllib.urlencode(postdata)

4.2、Cookie处理

1
2
3
4
5
6
7
8
9
10
#获取一个cookie对象
cookie = cookielib.CookieJar()
#构建cookie处理器
cookie_p = urllib2.HTTPCookieProcessor(cookie)
#装载cookie
opener = urllib2.build_opener(cookie_p)
#注意此处后面的install_opener(opener),安装不同的opener对象作为urlopen() 使用的全局URL opener
urllib2.install_opener(opener)
content = urllib2.urlopen('http://armsword.com').read()

如果同时需要代理和Cookie,这样就可以:

1
2
opener = urllib2.build_opener(proxy_ip, cookie_p, urllib2.HTTPHandler)

如果上面未使用install_opener(opener),则使用:

1
2
response = opener.open(url).read()

以V2EX每日签到为例,把上面的流程走一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#全局变量装载Cookie省略
def login():
'''
once值每次登录都不一样,在页面上可以看到
也必需有http header
'''
req = urllib2.Request(url_login)
once = get_info(req,'name','once')['value'] #省略
postdata = {
'u':username,
'p':password,
'once':once,
'next':"/"
}

header = {'Host':'www.v2ex.com','Origin':'http://www.v2ex.com',
'Referer':'http://www.v2ex.com/signin',
'User-Agent':&quot;Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36
(KHTML, like Gecko) Ubuntu Chromium/32.0.1700.107 Chrome/32.0.1700.107 Safari/537.36&quot;}

data = urllib.urlencode(postdata)
req = urllib2.Request(url_login,data,header)
response = opener.open(req)

5、gzip/deflate支持

某些网站不管你请求头带不带Accept-Encoding:gzip,它返回的都是gzip压缩的内容,urlopen不会处理gzip压缩的内容,所以得到的就是乱码,以下为解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
f = urllib2.urlopen(url)
headers = f.info()
rawdata = f.read()
if ('Content-Encoding' in headers and headers['Content-Encoding']) or \
('content-encoding' in headers and headers['content-encoding']):
import gzip
import StringIO
data = StringIO.StringIO(rawdata)
gz = gzip.GzipFile(fileobj=data)
rawdata = gz.read()
gz.close()

6、URL编码和网页编码处理
如果URL里含有中文,要对相应的中文进行编码,可以使用urllib.quote(‘要编码的字符串’),如下所示:

1
2
3
4
5
http://www.google.com/#newwindow=1&amp;q=你好
query = urllib.quote('你好')
url = 'http://www.google.com/#newwindow=1&amp;q='+query
response = urllib.urlopen(url).read()

至于处理网页编码,按照一般浏览器的处理流程,判断网页的编码首先是根据HTTP服务器发过来的HTTP响应头部中Content-Type字段,例如text/html; charset=utf-8就表示这是一个HTML网页,使用的是utf8编码。如果HTTP头部中没有,或者网页内容的head区中有charset属性或者其http-equiv属性为Content-Type的meta元素,如果有charset属性,则直接读取这个属性,如果是后者,则读取这个元素的content属性。但是实际情况是许多网页不按照规矩出牌,首先,HTTP响应里不一定有Content-Type头部,其次某些网页里可能没有Content-Type或者有多个Content-Type(例如百度搜索的缓存页面)。所以,我的经验基本是自己多猜测几次吧。其实GBK、UTF-8占了绝大部分,使用xx.decode(‘gbk’,’ignore’).encode(‘utf-8’) (或相反)试试,其中ignore是指忽略非法字符。

7、AJAX

AJAX 是一种用于创建快速动态网页的技术。
通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面。

AJAX依然是HTTP请求,只是来自页面发起的另一个HTTP请求。我之前做百度音乐的时候,在chrome下的network选项分析每一个选项,找到对应的ajax数据,分析json,在正则、查找之类的。

当然,也可以通过selenium、phantomjs、mechanize等模拟浏览器引擎构建一个真实的浏览器执行JS、渲染页面(在写这篇文章时,我还没用到过这些),这应该算是终极大招了吧。

8、效率

目前使用过多线程、多进程,多进程注意下僵尸进程。

异步:用twisted进行异步I/O抓取,tornado(如果拿到实习offer后,我可能会抽出几天时间学习这个,我感觉我应该学习下web编程,纠结了下flask还是tornado,其实无论哪个一周完全可以入门了,python你懂的。)

9、验证码

图像识别方面的知识,好吧,等我学会后再写吧。。。

根据我的经验,网络爬虫里网络带宽是主要瓶颈,上文只说了抓取,其实网页抽取依然有很多知识要做的,我现在会的都是些皮毛,要学的还很多,等拿到dream 公司的实习offer后,有空继续折腾下,现在主要精力是弥补自己的一些知识缺点和面试长考的问题。

参考链接:

http://blog.binux.me/2013/09/howto-crawl-web/

http://blog.raphaelzhang.com/2012/03/issues-in-python-crawler/

http://yxmhero1989.blog.163.com/blog/static/112157956201311821444664/