用Python编一个抓网页的程序是非常快的,下面就是一个例子:

  1. import urllib2
  2.  
  3. html = urllib2.urlopen('http://blog.raphaelzhang.com').read()

但是在实际工作中,这种写法是远远不够的,至少会遇到下面几个问题:

  • 网络会出错,任何错误都可能。例如机器宕了,网线断了,域名出错了,网络超时了,页面没有了,网站跳转了,服务被禁了,主机负载不够了…
  • 服务器加上了限制,只让常见浏览器访问
  • 服务器加上了防盗链的限制
  • 某些2B网站不管你HTTP请求里有没有Accept-Encoding头部,也不管你头部具体内容是什么,反正总给你发gzip后的内容
  • URL链接千奇百怪,带汉字的也罢了,有的甚至还有回车换行
  • 某些网站HTTP头部里有一个Content-Type,网页里有好几个Content-Type,更过分的是,各个Content-Type还不一样,最过分的是,这些Content-Type可能都不是正文里使用的Content-Type,从而导致乱码
  • 网络链接很慢,乘分析几千个页面的时间,建议你可以好好吃顿饭去了
  • Python本身的接口有点糙

好吧,这么一大箩筐问题,我们来一个个搞定。

错误处理和服务器限制

首先是错误处理。由于urlopen本身将大部分错误,甚至包括4XX和5XX的HTTP响应,也搞成了异常,因此我们只需要捕捉异常就好了。同时,我们也可以获取urlopen返回的响应对象,读取它的HTTP状态码。除此之外,我们在urlopen的时候,也需要设置timeout参数,以保证处理好超时。下面是代码示例:

  1. import urllib2
  2. import socket
  3.  
  4. try:
  5. f = urllib2.urlopen('http://blog.raphaelzhang.com', timeout = 10)
  6. code = f.getcode()
  7. if code < 200 or code >= 300:
  8. #你自己的HTTP错误处理
  9. except Exception, e:
  10. if isinstance(e, urllib2.HTTPError):
  11. print 'http error: {0}'.format(e.code)
  12. elif isinstance(e, urllib2.URLError) and isinstance(e.reason, socket.timeout):
  13. print 'url error: socket timeout {0}'.format(e.__str__())
  14. else:
  15. print 'misc error: ' + e.__str__()

如果是服务器的限制,一般来说我们都可以通过查看真实浏览器的请求来设置对应的HTTP头部,例如针对浏览器的限制我们可以设置User-Agent头部,针对防盗链限制,我们可以设置Referer头部,下面是示例代码:

  1. import urllib2
  2.  
  3. req = urllib2.Request('http://blog.raphaelzhang.com',
  4. headers = {"Referer": "http://www.baidu.com",
  5. "User-Agent": "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24"
  6. })
  7. html = urllib2.urlopen(url = req, timeout = 10).read()

有的网站用了Cookie来限制,主要是涉及到登录和限流,这时候没有什么通用的方法,只能看能否做自动登录或者分析Cookie的问题了。

URL与内容处理

URL里奇形怪状的格式只能个别分析,个别处理,逮到一个算一个。例如针对URL里可能有汉字,相对路径,以及回车换行之类的问题,我们可以先用urlparse模块的urljoin函数处理相对路径的问题,然后去掉乱七八糟的回车换行之类的玩意,最后用urllib2的quote函数处理特殊字符编码和转义的问题,生成真正的URL。

当然,在使用的过程中你会发现Python的urljoin和urlopen都有些问题,因此具体的代码我们在后面再给。

对于那些不管三七二十一就把gzip内容丢过来的,我们直接分析它的内容格式,不要管HTTP头部就好了。代码是这样的:

  1. import urllib2
  2. import gzip, cStringIO
  3.  
  4. html = urllib2.urlopen('http://blog.raphaelzhang.com').read()
  5. if html[:6] == '\x1f\x8b\x08\x00\x00\x00':
  6. html = gzip.GzipFile(fileobj = cStringIO.StringIO(html)).read()

好了,现在又到了编码处理的痛苦时间了。由于我们是中国人,用的是汉字,因此必须要搞清楚一段文本使用的到底是什么编码(f*ck),不然就会出现传说中的乱码。

按照一般浏览器的处理流程,判断网页的编码首先是根据HTTP服务器发过来的HTTP响应头部中Content-Type字段,例如text/html; charset=utf-8就表示这是一个HTML网页,使用的是utf8编码。如果HTTP头部中没有,或者网页内容的head区中有charset属性或者其http-equiv属性为Content-Type的meta元素,如果有charset属性,则直接读取这个属性,如果是后者,则读取这个元素的content属性,格式与上述格式类似。

按理来说,如果大家都按规矩出牌,编码的问题是很好搞定的。但是,问题就在于大家可能不按规矩办事,走自己的捷径…(OMG)。首先,HTTP响应里不一定有Content-Type头部,其次某些网页里可能没有Content-Type或者有多个Content-Type(例如百度搜索的缓存页面)。这个时候,我们就必须冒险猜测了,因此处理编码的一般流程是:

  1. 读取urlopen返回的HTTP相应对象的headers.dict['content-type']属性,将charset读取出来
  2. 利用正则表达式解析HTML里head区对应的meta元素中Content-Type/charset属性的值,并读取出来
  3. 如果上面两步中获得的编码只有gb2312或者gbk,则可以认为网页的编码是gbk(反正gbk兼容gb2312)
  4. 如果发现有多个编码,而且这些编码互不兼容,例如又有utf8又有ios8859-1的,那我们只好使用chardet模块了,调用chardet的detect函数,读取encoding属性即可
  5. 获得了编码后,可以将网页用decode方法转码为Python中的unicode串以方便后续的统一处理

几个需要注意的地方,我的做法是只要网页的编码是gb2312,我就将其假定为gbk。因为不少网站虽然写的是gb2312,实际上是gbk编码的,毕竟gb2312覆盖的字符数太少,很多字,例如朱棣、陶喆,朱镕基这些词中都有字不在gb2312的覆盖范围内。

其次,chardet不是万能的,如果有现成的Content-Type/charset,就仍然使用Content-Type/charset。因为chardet使用的是一种猜测方式来做的,主要是参考的老版本的火狐代码中的猜测算法,就是根据某些编码的特征来猜测的。例如gb2312编码中“的”这个字的对应的编码的出现频率可能比较高等。在我的实际使用中,它出错的概率也有10%左右吧。另外,因为它要分析和猜测,所以时间也比较长。

检测编码的代码有点长,具体可以参看这个代码里的getencoding函数的实现。

提高性能

网页抓取程序的主要性能瓶颈都在网络处理上。关于这一点,可以使用cProfile.run和pstats.Stats来测试一下每个函数的调用时间就可以得到验证了。一般来说,可以通过下面几种方法来解决:

  • 使用threading或者multiprocess来并行处理,同时抓取多个页面,使用都非常简单的,文档在这里
  • 在HTTP请求里面加上Accept-Encoding头部,将其设为gzip即可,表示可以接受gzip压缩的数据,绝大多数网站都支持gzip压缩,可以节省70 ~ 80%的流量
  • 如果不需要读取所有内容,可以在HTTP请求里面加上Range头部(HTTP断点续传也需要这个头部的)。例如把Range头部的值设为bytes=0-1023就是相当于请求开头1024个字节的内容,这样可以大大节省带宽。不过少数网站不支持这个头部,这个需要注意下
  • 调用urlopen的时候一定要设置timeout参数,不然可能程序会永远等待在那里的

并行处理具体使用多线程还是多进程,就我现在测试来看区别不大,毕竟主要瓶颈是在网络链接上。

其实除了上述方法外,在Python里还有一些可能更好的提高性能的方法。例如使用greenletstacklessPyPy等支持更好多线程多进程的工具或python,还可以使用异步IO,例如twisted或者PycURL。不过个人对greenlet不熟,觉得twisted实在太twisted,PycURL不够pythonic,而stackless和pypy怕破坏了其他python程序,因此仍然使用urllib2 + threading方案。当然,因为GIL的问题,python多线程仍然不够快,可是对单线程情况来说,已经有数倍时间的节省了。

Python的小问题

在抓网页的时候,Python暴露出一些小问题。主要是urlopen和urljoin函数的问题。

urlparse模块里的urljoin函数,其功能是将一个相对url,例如../img/mypic.png,加上当前页面的绝对url,例如http://blog.raphaelzhang.com/apk/index.html,转化为一个绝对URL,例如在这个例子里就是http://blog.raphaelzhang.com/img/mypic.png。但是urljoin处理的结果还需要进一步处理,就是去掉多余的..路径,去掉回车换行等特殊符号。代码是这样的:

  1. from urlparse import urljoin
  2.  
  3. relurl = '../../img/\nmypic.png'
  4. absurl = 'http://blog.raphaelzhang.com/2012/'
  5. url = urljoin(absurl, relurl)
  6. #url为http://blog.raphaelzhang.com/../img/\nmypic.png
  7. url = reduce(lambda r,x: r.replace(x[0], x[1]), [('/../', '/'), ('\n', ''), ('\r', '')], url)
  8. #url为正常的http://blog.raphaelzhang.com/img/mypic.png

urlopen函数也有一些问题,它其实对url字符串有自己的要求。

首先,你交给urlopen的函数需要自己做汉字等特殊字符的编码工作,使用urllib2的quote函数即可,就像这样:

  1. import urllib2
  2.  
  3. #此处的url当然是经过了上述urljoin和reduce处理过后的良好的绝对URL了
  4. url = urllib2.quote(url.split('#')[0].encode('utf8'), safe = "%/:=&?~#+!$,;'@()*[]")

其次,url里面不能有#。因为从理论上来说,#后面的是fragment,是在获取整个文档以后定位用的,而不是用来获取文档用的,但是实际上urlopen应该可以自己做这个事的,却把这个任务留给了开发人员。上面我已经用url.split(‘#’)[0]的方式来处理这个问题了。

最后,Python 2.6和之前的版本不能处理类似http://blog.raphaelzhang.com?id=123这样的url,会导致运行时错误,我们必须手工处理一下,将这种url转化为正常的http://blog.raphaelzhang.com/?id=123这样的url才行,不过这个bug在Python 2.7里面已经解决了。

好了,Python网页抓取的上述问题告一段落了,下面我们还要处理分析网页的问题,留待下文分解。

用Python抓网页的注意事项的更多相关文章

  1. python抓网页数据【ref:http://www.1point3acres.com/bbs/thread-83337-1-1.html】

    前言:数据科学越来越火了,网页是数据很大的一个来源.最近很多人问怎么抓网页数据,据我所知,常见的编程语言(C++,java,python)都可以实现抓网页数据,甚至很多统计\计算的语言(R,Matla ...

  2. 手把手教你用python抓网页数据

    http://www.1point3acres.com/bbs/thread-83337-1-1.html

  3. Python 抓取网页并提取信息(程序详解)

    最近因项目需要用到python处理网页,因此学习相关知识.下面程序使用python抓取网页并提取信息,具体内容如下: #---------------------------------------- ...

  4. python爬虫抓网页的总结

    python爬虫抓网页的总结 更多 python 爬虫   学用python也有3个多月了,用得最多的还是各类爬虫脚本:写过抓代理本机验证的脚本,写过在discuz论坛中自动登录自动发贴的脚本,写过自 ...

  5. python抓取网页例子

    python抓取网页例子 最近在学习python,刚刚完成了一个网页抓取的例子,通过python抓取全世界所有的学校以及学院的数据,并存为xml文件.数据源是人人网. 因为刚学习python,写的代码 ...

  6. 爬虫学习笔记(1)-- 利用Python从网页抓取数据

    最近想从一个网站上下载资源,懒得一个个的点击下载了,想写一个爬虫把程序全部下载下来,在这里做一个简单的记录 Python的基础语法在这里就不多做叙述了,黑马程序员上有一个基础的视频教学,可以跟着学习一 ...

  7. 使用 Python 抓取欧洲足球联赛数据

    Web Scraping在大数据时代,一切都要用数据来说话,大数据处理的过程一般需要经过以下的几个步骤    数据的采集和获取    数据的清洗,抽取,变形和装载    数据的分析,探索和预测    ...

  8. [python]乱码:python抓取脚本

    参考: http://www.zhxl.me/1409.html 使用 python urllib2 抓取网页时出现乱码的解决方案 发表回复 这里记录的是一个门外汉解决使用 urllib2 抓取网页时 ...

  9. 如何用python抓取js生成的数据 - SegmentFault

    如何用python抓取js生成的数据 - SegmentFault 如何用python抓取js生成的数据 1赞 踩 收藏 想写一个爬虫,但是需要抓去的的数据是js生成的,在源代码里看不到,要怎么才能抓 ...

随机推荐

  1. 妙味课堂——HTML+CSS(第四课)(一)

    这一课学的东西真是太多了,还不赶快记下来,留待以后慢慢回味! 首先我们回顾一下inline-block的特性: 使块元素在一行显示 使内嵌支持宽高 换行被解析了(问题) 不设置宽度的时候,宽度由内容撑 ...

  2. 有用的一些web网站

    1.http://www.aseoe.com/api-download/download.html 爱思资源网

  3. 关于在linux下清屏的几种技巧

    在windows的DOS操作界面里面,清屏的命令是cls,那么在linux 里面的清屏命令是什么呢?下面笔者分享几种在linux下用过的清屏方法. 1.clear命令.这个命令将会刷新屏幕,本质上只是 ...

  4. ​浅谈Asp.net的sessionState

    见:http://my.oschina.net/kavensu/blog/330436

  5. Qt中的键盘事件,以及焦点的设置(比较详细)

    Qt键盘事件属于Qt事件系统,所以事件系统中所有规则对按键事件都有效.下面关注点在按键特有的部分: focus 一个拥有焦点(focus)的QWidget才可以接受键盘事件.有输入焦点的窗口是活动窗口 ...

  6. Golang哲学思想

    Golang是一门新语言,经过几年发展,慢慢地也已经被许多大公司认可.最大的特点是速度快,并发性好,与网络的功能结合好,是一门服务端语言,号称“网络时代的新语言”:另外还是一个编译型的Python.不 ...

  7. UML系列02之 UML类图(2)

    UML类图的几种关系 在UML类图中,关系可以分为4种: 泛化, 实现, 关联 和 依赖.1. 泛化 -- 表示"类与类之间的继承关系".2. 实现 -- 表示"类与接口 ...

  8. android-HttpClient上传信息(包括图片)到服务端

    需要下载apache公司下的HttpComponents项目下的HTTPCLIENT ----------地址为http://hc.apache.org/downloads.cgi 主要是用到了htt ...

  9. Codeforces Round #362 (Div. 2) A.B.C

    A. Pineapple Incident time limit per test 1 second memory limit per test 256 megabytes input standar ...

  10. STL笔记(2) STL之父访谈录

    年3月,dr.dobb's journal特约记者, 著名技术书籍作家al stevens采访了stl创始人alexander stepanov. 这份访谈纪录是迄今为止对于stl发展历史的最完备介绍 ...