爬虫在平时也经常用,但一直没有系统的总结过,其实它涉及了许多的知识点。这一系列会理一遍这些知识点,不求详尽,只希望以点带面构建一个爬虫的知识框架。这一篇是概念性解释以及入门级爬虫介绍(以爬取网易新闻为例)。
爬虫基础
什么是爬虫
爬虫说白了其实就是获取资源的程序。制作爬虫的总体分三步:爬-取-存。首先要获取整个网页的所有内容,然后再取出其中对你有用的部分,最后再保存有用的部分。
爬虫类型
- 网络爬虫
网络爬虫,是一种按照一定的规则,自动的 抓取万维网信息的程序或者脚本。网络爬虫是搜索引擎系统中十分重要的组成部分,爬取的网页信息用于建立索引从而为搜索引擎提供支持,它决定着整个引擎系统的内容是否丰富,信息是否即时,其性能的优劣直接影响着搜索引擎的效果。 - 传统爬虫
从一个或若干初始网页的URL开始,获得初始网页的URL,在抓取网页过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。
工作原理
- 根据一定的网页分析算法过滤与主题无关的链接,保留有用链接并将其放入等待抓取的URL队列
- 根据一定的搜索策略从队列中选择下一步要抓取的网页URL,重复上述过程,直到达到指定条件才结束爬取
- 对所有抓取的网页进行一定的分析、过滤,并建立索引,以便之后的查询和检索。
爬取策略
广度优先
完成当前层次的搜索后才进行下一层次的搜索。一般的使用策略,一般通过队列来实现。
最佳优先
会有评估算法,凡是被算法评估为有用的网页,先来爬取。
深度优先
实际应用很少。可能会导致trapped问题。通过栈来实现。
URL( Uniform Resource Locator: 统一资源定位符)
互联网上资源均有其唯一的地址,由三部分组成。
- 模式/协议
- 文件所在IP地址及端口号
- 主机上的资源位置
- 例子:http://www.example.com/index.html
Web Server/Socket如何建立连接和传输数据的
web server 的工作过程其实和打电话的过程差不多(买电话–>注册号码–>监听–>排队接听–>读写–>关闭),经典的三步握手(有人在吗?我在呢,你呢?我也在)在排队接听时进行。下面一张图足以解释一切。
Crawler端需要一个socket接口,向服务器端发起connect请求,完成连接后就可以和服务器交流了,操作完毕会关闭socket接口。服务器端更复杂一点,也需要一个socket接口,并且这个socket接口需要绑定一个地址(bind()),这就相当于有一个固定的电话号码,这样其他人拨打这个号码就可以找到这个服务器。绑定之后服务器的socket就开始监听(listen())有没有用户请求,如果有,就接收请求(accept()),和用户建立连接,然后就可以交流。
HTML DOM
- DOM 将 HTML 文档表达为树结构
- 定义了访问和操作 HTML 文档的标准
Cookie
- 由服务器端生成,发送给 User-Agent(一般是浏览器),浏览器会将 Cookie 的 key/value 保存到某个目录下的文本文件哪,下次访问同一网站时就发送该 Cookie 给服务器。
HTTP
- GET 直接以链接形式访问,链接中包含了所有的参数
- PUT 把提交的数据放到 HTTP 包的包体中
eg. import urllib import urllib2 url='http://www.zhihu.com/#signin' user_agent='MOZILLA/5.0' values={'username':'252618408@qq.com','password':'xxx'} headers={'User-Agent':user_agent} data=urllib.urlencode(values) # urlencode 是 urllib 独有的方法 request=urllib2.Request(url,data,headers) # write a letter response=urllib2.urlopen(request) # send the letter and get the reply page=response.read() # read the reply
urllib 仅可以接受 URL,这意味着你不可以伪装你的 User Agent 字符串等,但 urllib 提供了 urlencode 方法用来GET查询字符串等产生,而 urllib2 没有。
因此 urllib, urllib2经常一起使用。
Headers 设置
- User-Agent: 部分服务器或 Proxy 会通过该值来判断是否是浏览器发出的请求
- Content-Type: 使用 REST 接口时,服务器会检查该值,用来确定 HTTP Body 中的内容该怎样解析
- application/xml: 在 XMl RPC, 如 RESTful/SOAP 调用时使用
- application/json: 在 JSON RPC 调用时使用
- application/x-www-form-urlencoded: 浏览器提交 Web 表单时使用
爬虫难点
爬虫的两部分,一是下载 Web 页面,有许多问题需要考虑。如何最大程度地利用本地带宽,如何调度针对不同站点的 Web 请求以减轻对方服务器的负担等。一个高性能的 Web Crawler 系统里,DNS 查询也会成为急需优化的瓶颈,另外,还有一些“行规”需要遵循(例如robots.txt)。
而获取了网页之后的分析过程也是非常复杂的,Internet 上的东西千奇百怪,各种错误百出的 HTML 页面都有,要想全部分析清楚几乎是不可能的事;另外,随着 AJAX 的流行,如何获取由 Javascript 动态生成的内容成了一大难题;除此之外,Internet 上还有有各种有意或无意出现的 Spider Trap ,如果盲目的跟踪超链接的话,就会陷入 Trap 中万劫不复了,例如这个网站,据说是之前 Google 宣称 Internet 上的 Unique URL 数目已经达到了 1 trillion 个,因此这个人 is proud to announce the second trillion 。
最简单的爬虫
requests 库
import requests url = "http://shuang0420.github.io/" r = requests.get(url)
urllib2 库
|
|
这是一个简单的爬虫,打开 webPage.html 是这样的显示,没有css.
实例:爬取网易新闻
爬取网易新闻 [代码示例]
– 使用 urllib2 的 requests包来爬取页面
– 使用正则表达式和 bs4 分析一级页面,使用 Xpath 来分析二级页面
– 将得到的标题和链接,保存为本地文件
分析初始页面
我们的初始页面是 http://news.163.com/rank
查看源代码
我们想要的是分类标题和URL,需要解析 DOM 文档树,这里使用了 BeautifulSoup 里的方法。
然而,Beautiful Soup对文档的解析速度不会比它所依赖的解析器更快,如果对计算时间要求很高或者计算机的时间比程序员的时间更值钱,那么就应该直接使用 lxml。换句话说,还有提高Beautiful Soup效率的办法,使用lxml作为解析器。Beautiful Soup用lxml做解析器比用html5lib或Python内置解析器速度快很多。bs4 的默认解析器是 html.parser,使用lxml的代码如下:
BeautifulSoup(markup, "lxml")
分析二级页面
查看源代码
我们要爬取的是
|
|
潜在问题
我们的任务是爬取1万个网页,按上面这个程序,耗费时间长,我们可以考虑开启多个线程(池)去一起爬取,或者用分布式架构去并发的爬取网页。
种子URL和后续解析到的URL都放在一个列表里,我们应该设计一个更合理的数据结构来存放这些待爬取的URL才是,比如队列或者优先队列。
对各个网站的url,我们一视同仁,事实上,我们应当区别对待。大站好站优先原则应当予以考虑。
每次发起请求,我们都是根据url发起请求,而这个过程中会牵涉到DNS解析,将url转换成ip地址。一个网站通常由成千上万的URL,因此,我们可以考虑将这些网站域名的IP地址进行缓存,避免每次都发起DNS请求,费时费力。
解析到网页中的urls后,我们没有做任何去重处理,全部放入待爬取的列表中。事实上,可能有很多链接是重复的,我们做了很多重复劳动。
爬虫被封禁问题
优化方案
并行爬取问题
关于并行爬取,首先我们想到的是多线程或者线程池方式,一个爬虫程序内部开启多个线程。同一台机器开启多个爬虫程序,这样,我们就有N多爬取线程在同时工作,大大提高了效率。
当然,如果我们要爬取的任务特别多,一台机器、一个网点肯定是不够的,我们必须考虑分布式爬虫。分布式架构,考虑的问题有很多,我们需要一个scheduler来分配任务并排序,各个爬虫之间还需要通信合作,共同完成任务,不要重复爬取相同的网页。分配任务时我们还需要考虑负载均衡以做到公平。(可以通过Hash,比如根据网站域名进行hash)
负载均衡分派完任务之后,千万不要以为万事大吉了,万一哪台机器挂了呢?原先指派给挂掉的哪台机器的任务指派给谁?又或者哪天要增加几台机器,任务有该如何进行重新分配呢?所以我们还要 task table 来纪录状态。
待爬取网页队列
如何对待待抓取队列,跟操作系统如何调度进程是类似的场景。
不同网站,重要程度不同,因此,可以设计一个优先级队列来存放待爬起的网页链接。如此一来,每次抓取时,我们都优先爬取重要的网页。
当然,你也可以效仿操作系统的进程调度策略之多级反馈队列调度算法。DNS缓存
为了避免每次都发起DNS查询,我们可以将DNS进行缓存。DNS缓存当然是设计一个hash表来存储已有的域名及其IP。网页去重
说到网页去重,第一个想到的是垃圾邮件过滤。垃圾邮件过滤一个经典的解决方案是Bloom Filter(布隆过滤器)。布隆过滤器原理简单来说就是:建立一个大的位数组,然后用多个Hash函数对同一个url进行hash得到多个数字,然后将位数组中这些数字对应的位置为1。下次再来一个url时,同样是用多个Hash函数进行hash,得到多个数字,我们只需要判断位数组中这些数字对应的为是全为1,如果全为1,那么说明这个url已经出现过。如此,便完成了url去重的问题。当然,这种方法会有误差,只要误差在我们的容忍范围之类,比如1万个网页,我只爬取到了9999个,并不会有太大的实际影响。
一种很不错的方法来自url相似度计算,简单介绍下。
考虑到url本身的结构,对其相似度的计算就可以抽象为对其关键特征相似度的计算。比如可以把站点抽象为一维特征,目录深度抽象为一维特征,一级目录、二级目录、尾部页面的名字也都可以抽象为一维特征。比如下面两个url:
url1: http://www.spongeliu.com/go/happy/1234.html
url2: http://www.spongeliu.com/snoopy/tree/abcd.html特征:
- 站点特征:如果两个url站点一样,则特征取值1,否则取值0;
- 目录深度特征:特征取值分别是两个url的目录深度是否一致;
- 一级目录特征:在这维特征的取值上,可以采用多种方法,比如如果一级目录名字相同则特征取1,否则取0;或者根据目录名字的编辑距离算出一个特征值;或者根据目录名字的pattern,如是否数字、是否字母、是否字母数字穿插等。这取决于具体需求,这里示例仅仅根据目录名是否相同取1和0;
- 尾页面特征:这维特征的取值同一级目录,可以判断后缀是否相同、是否数字页、是否机器生成的随机字符串或者根据编辑长度来取值,具体也依赖于需求。这里示例仅仅判断最后一级目录的特征是否一致(比如是否都由数字组成、是否都有字母组成等)。
这样,对于这两个url就获得了4个维度的特征,分别是:1 1 0 0 。有了这两个特征组合,就可以根据具体需求判断是否相似了。我们定义一下每个特征的重要程度,给出一个公式:
similarity = feather1 * x1 + feather2*x2 + feather3*x3 + feather4*x4
其中x表示对应特征的重要程度,比如我认为站点和目录都不重要,最后尾页面的特征才是最重要的,那么x1,x2,x3都可以取值为0,x4取值为1,这样根据similarity就能得出是否相似了。或者认为站点的重要性占10%,目录深度占50%,尾页面的特征占40%,那么系数分别取值为0.1\0.5\0\0.4即可。
其实这样找出需要的特征,可以把这个问题简化成一个机器学习的问题,只需要人为判断出一批url是否相似,用svm训练一下就可以达到机器判断的目的。
除了上面这种两个url相似度的判断,也可以将每一条url都抽象成一组特征,然后计算出一个url的得分,设置一个分数差的阈值,就可以达到从一大堆url中找出相似的url的目的。数据存储的问题
数据存储同样是个很有技术含量的问题。用关系数据库存取还是用NoSQL,抑或是自己设计特定的文件格式进行存储,都大有文章可做。进程间通信
分布式爬虫,就必然离不开进程间的通信。我们可以以规定的数据格式进行数据交互,完成进程间通信。反爬虫机制问题
针对反爬虫机制,我们可以通过轮换IP地址、轮换Cookie、修改用户代理(User Agent)、限制速度、避免重复性爬行模式等方法解决。
参考链接:
网络爬虫基本原理(一)
http://www.chinahadoop.cn/course/596/learn#lesson/11986
https://www.bittiger.io/blog/post/5pDTFcDwkmCvvmKys
https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html