爬虫总结(一)-- 爬虫基础 & python实现

爬虫在平时也经常用,但一直没有系统的总结过,其实它涉及了许多的知识点。这一系列会理一遍这些知识点,不求详尽,只希望以点带面构建一个爬虫的知识框架。这一篇是概念性解释以及入门级爬虫介绍(以爬取网易新闻为例)。

爬虫基础

什么是爬虫

爬虫说白了其实就是获取资源的程序。制作爬虫的总体分三步:爬-取-存。首先要获取整个网页的所有内容,然后再取出其中对你有用的部分,最后再保存有用的部分。

爬虫类型

  • 网络爬虫
    网络爬虫,是一种按照一定的规则,自动的 抓取万维网信息的程序或者脚本。网络爬虫是搜索引擎系统中十分重要的组成部分,爬取的网页信息用于建立索引从而为搜索引擎提供支持,它决定着整个引擎系统的内容是否丰富,信息是否即时,其性能的优劣直接影响着搜索引擎的效果。
  • 传统爬虫
    从一个或若干初始网页的URL开始,获得初始网页的URL,在抓取网页过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。

工作原理

  • 根据一定的网页分析算法过滤与主题无关的链接,保留有用链接并将其放入等待抓取的URL队列
  • 根据一定的搜索策略从队列中选择下一步要抓取的网页URL,重复上述过程,直到达到指定条件才结束爬取
  • 对所有抓取的网页进行一定的分析、过滤,并建立索引,以便之后的查询和检索。

爬取策略

广度优先

完成当前层次的搜索后才进行下一层次的搜索。一般的使用策略,一般通过队列来实现。

最佳优先

会有评估算法,凡是被算法评估为有用的网页,先来爬取。

深度优先

实际应用很少。可能会导致trapped问题。通过栈来实现。

URL( Uniform Resource Locator: 统一资源定位符)

互联网上资源均有其唯一的地址,由三部分组成。

Web Server/Socket如何建立连接和传输数据的

web server 的工作过程其实和打电话的过程差不多(买电话–>注册号码–>监听–>排队接听–>读写–>关闭),经典的三步握手(有人在吗?我在呢,你呢?我也在)在排队接听时进行。下面一张图足以解释一切。

Crawler端需要一个socket接口,向服务器端发起connect请求,完成连接后就可以和服务器交流了,操作完毕会关闭socket接口。服务器端更复杂一点,也需要一个socket接口,并且这个socket接口需要绑定一个地址(bind()),这就相当于有一个固定的电话号码,这样其他人拨打这个号码就可以找到这个服务器。绑定之后服务器的socket就开始监听(listen())有没有用户请求,如果有,就接收请求(accept()),和用户建立连接,然后就可以交流。

HTML DOM

  • DOM 将 HTML 文档表达为树结构
  • 定义了访问和操作 HTML 文档的标准

  • 由服务器端生成,发送给 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 库

      
      
1
2
3
4
5
6
7
8
9
10
11
      
      
import urllib2
# request source file
url = "http://shuang0420.github.io/"
request = urllib2.Request(url) # write a letter
response = urllib2.urlopen(request) # send the letter and get the reply
page = response.read() # read the reply
# save source file
webFile = open('webPage.html', 'wb')
webFile.write(page)
webFile.close()

这是一个简单的爬虫,打开 webPage.html 是这样的显示,没有css.

实例:爬取网易新闻

爬取网易新闻 [代码示例]
– 使用 urllib2 的 requests包来爬取页面
– 使用正则表达式和 bs4 分析一级页面,使用 Xpath 来分析二级页面
– 将得到的标题和链接,保存为本地文件

分析初始页面

我们的初始页面是 http://news.163.com/rank

查看源代码

我们想要的是分类标题和URL,需要解析 DOM 文档树,这里使用了 BeautifulSoup 里的方法。

      
      
1
2
3
4
5
6
7
8
9
10
11
12
      
      
def Nav_Info(myPage):
# 二级导航的标题和页面
pageInfo = re.findall(r'<div class="subNav">.*?<div class="area areabg1">', myPage, re.S)[
0].replace('<div class="subNav">', '').replace('<div class="area areabg1">', '')
soup = BeautifulSoup(pageInfo, "lxml")
tags = soup('a')
topics = []
for tag in tags:
# 只要 科技、财经、体育 的新闻
# if (tag.string=='科技' or tag.string=='财经' or tag.string=='体育'):
topics.append((tag.string, tag.get('href', None)))
return topics

然而,Beautiful Soup对文档的解析速度不会比它所依赖的解析器更快,如果对计算时间要求很高或者计算机的时间比程序员的时间更值钱,那么就应该直接使用 lxml。换句话说,还有提高Beautiful Soup效率的办法,使用lxml作为解析器。Beautiful Soup用lxml做解析器比用html5lib或Python内置解析器速度快很多。bs4 的默认解析器是 html.parser,使用lxml的代码如下:

BeautifulSoup(markup, "lxml")

分析二级页面

查看源代码

我们要爬取的是之间的新闻标题和链接,同样需要解析文档树,可以通过以下代码实现,这里用了 lxml 解析器,效率更高。

      
      
1
2
3
4
5
6
      
      
def News_Info(newPage):
# xpath 使用路径表达式来选取文档中的节点或节点集
dom = etree.HTML(newPage)
news_titles = dom.xpath('//tr/td/a/text()')
news_urls = dom.xpath('//tr/td/a/@href')
return zip(news_titles, news_urls)

完整代码

潜在问题

  1. 我们的任务是爬取1万个网页,按上面这个程序,耗费时间长,我们可以考虑开启多个线程(池)去一起爬取,或者用分布式架构去并发的爬取网页。

  2. 种子URL和后续解析到的URL都放在一个列表里,我们应该设计一个更合理的数据结构来存放这些待爬取的URL才是,比如队列或者优先队列。

  3. 对各个网站的url,我们一视同仁,事实上,我们应当区别对待。大站好站优先原则应当予以考虑。

  4. 每次发起请求,我们都是根据url发起请求,而这个过程中会牵涉到DNS解析,将url转换成ip地址。一个网站通常由成千上万的URL,因此,我们可以考虑将这些网站域名的IP地址进行缓存,避免每次都发起DNS请求,费时费力。

  5. 解析到网页中的urls后,我们没有做任何去重处理,全部放入待爬取的列表中。事实上,可能有很多链接是重复的,我们做了很多重复劳动。

  6. 爬虫被封禁问题

优化方案

  1. 并行爬取问题

    关于并行爬取,首先我们想到的是多线程或者线程池方式,一个爬虫程序内部开启多个线程。同一台机器开启多个爬虫程序,这样,我们就有N多爬取线程在同时工作,大大提高了效率。

    当然,如果我们要爬取的任务特别多,一台机器、一个网点肯定是不够的,我们必须考虑分布式爬虫。分布式架构,考虑的问题有很多,我们需要一个scheduler来分配任务并排序,各个爬虫之间还需要通信合作,共同完成任务,不要重复爬取相同的网页。分配任务时我们还需要考虑负载均衡以做到公平。(可以通过Hash,比如根据网站域名进行hash)

    负载均衡分派完任务之后,千万不要以为万事大吉了,万一哪台机器挂了呢?原先指派给挂掉的哪台机器的任务指派给谁?又或者哪天要增加几台机器,任务有该如何进行重新分配呢?所以我们还要 task table 来纪录状态。

  2. 待爬取网页队列
    如何对待待抓取队列,跟操作系统如何调度进程是类似的场景。
    不同网站,重要程度不同,因此,可以设计一个优先级队列来存放待爬起的网页链接。如此一来,每次抓取时,我们都优先爬取重要的网页。
    当然,你也可以效仿操作系统的进程调度策略之多级反馈队列调度算法。

  3. DNS缓存
    为了避免每次都发起DNS查询,我们可以将DNS进行缓存。DNS缓存当然是设计一个hash表来存储已有的域名及其IP。

  4. 网页去重
    说到网页去重,第一个想到的是垃圾邮件过滤。垃圾邮件过滤一个经典的解决方案是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的目的。

  5. 数据存储的问题
    数据存储同样是个很有技术含量的问题。用关系数据库存取还是用NoSQL,抑或是自己设计特定的文件格式进行存储,都大有文章可做。

  6. 进程间通信
    分布式爬虫,就必然离不开进程间的通信。我们可以以规定的数据格式进行数据交互,完成进程间通信。

  7. 反爬虫机制问题
    针对反爬虫机制,我们可以通过轮换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


原文地址: http://www.shuang0420.com/2016/06/11/%E7%88%AC%E8%99%AB%E6%80%BB%E7%BB%93%EF%BC%88%E4%B8%80%EF%BC%89/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值