写点什么

如何构建一个通用的垂直爬虫平台?

用户头像
Kaito
关注
发布于: 2020 年 11 月 03 日
如何构建一个通用的垂直爬虫平台?

阅读本文大约需要 15 ~ 20 分钟。


之前做爬虫时,在公司设计开发了一个通用的垂直爬虫平台,后来在公司做了内部的技术分享,这篇文章把整个爬虫平台的设计思路整理了一下,分享给大家。


写一个爬虫很简单,写一个可持续稳定运行的爬虫也不难,但如何构建一个通用化的垂直爬虫平台?


这篇文章,我就来和你分享一下,一个通用垂直爬虫平台的构建思路。


爬虫简介


首先介绍一下,什么是爬虫?


搜索引擎是这样定义的:


网络爬虫(又被称为网页蜘蛛,网络机器人),是一种按照一定的规则,自动地抓取网页信息的程序或者脚本。


很简单,爬虫就是指定规则自动采集数据的程序脚本,目的在于拿到想要的数据。


而爬虫主要分为两大类:


  • 通用爬虫(搜索引擎)

  • 垂直爬虫(特定领域)


由于第一类的开发成本较高,所以只有搜索引擎公司在做,如谷歌、百度等。


而大多数企业在做的都是第二类,成本低、数据价值高。


例如一家做电商的公司只需要电商领域有价值的数据,那开发一个只采集电商领域数据的爬虫平台,意义较大。


我要和你分享的主要是针对第二类,垂直爬虫平台的设计思路。

如何写爬虫


首先,从最简单的开始,我们先了解一下如何写一个爬虫?


简单爬虫


开发爬虫最快的语言一般是 Python,它的代码写起来非常少。我们以抓取豆瓣书籍页面为例,来写一个简单的程序。


# coding: utf8
"""简单爬虫"""
import requestsfrom lxml import etree
def main(): # 1. 定义页面URL和解析规则 crawl_urls = [ 'https://book.douban.com/subject/25862578/', 'https://book.douban.com/subject/26698660/', 'https://book.douban.com/subject/2230208/' ] parse_rule = "//div[@id='wrapper']/h1/span/text()"
for url in crawl_urls: # 2. 发起HTTP请求 response = requests.get(url)
# 3. 解析HTML result = etree.HTML(response.text).xpath(parse_rule)[0]
# 4. 保存结果 print result
if __name__ == '__main__': main()
复制代码


这个爬虫比较简单,大致流程为:


  1. 定义页面 URL 和解析规则

  2. 发起 HTTP 请求

  3. 解析 HTML,拿到数据

  4. 保存数据


任何爬虫,要想获取网页上的数据,都是经过这几步。


当然,这个简单爬虫效率比较低,是采用同步抓取的方式,只能抓完一个网页,再去抓下一个,有没有可以提高效率的方式呢?


异步爬虫


我们进行优化,由于爬虫的抓取请求都是阻塞在网络 IO 上,所以我们可以使用异步的方式来优化,例如多线程或协程并行抓取网页数据,这里用 Python 的协程来实现。


# coding: utf8
"""协程版本爬虫,提高抓取效率"""
from gevent import monkeymonkey.patch_all()
import requestsfrom lxml import etreefrom gevent.pool import Pool
def main(): # 1. 定义页面URL和解析规则 crawl_urls = [ 'https://book.douban.com/subject/25862578/', 'https://book.douban.com/subject/26698660/', 'https://book.douban.com/subject/2230208/' ] rule = "//div[@id='wrapper']/h1/span/text()"
# 2. 抓取 pool = Pool(size=10) for url in crawl_urls: pool.spawn(crawl, url, rule)
pool.join()
def crawl(url, rule): # 3. 发起HTTP请求 response = requests.get(url)
# 4. 解析HTML result = etree.HTML(response.text).xpath(rule)[0]
# 5. 保存结果 print result
if __name__ == '__main__': main()
复制代码


经过优化,我们完成了异步版本的爬虫代码。


有了这些基础知识之后,我们看一个完整的例子,如何抓取一个整站数据?


整站爬虫


# coding: utf8
"""整站爬虫"""
from gevent import monkeymonkey.patch_all()
from urlparse import urljoin
import requestsfrom lxml import etreefrom gevent.pool import Poolfrom gevent.queue import Queue
base_url = 'https://book.douban.com'
# 种子URLstart_url = 'https://book.douban.com/tag/?view=type&icn=index-sorttags-all'
# 解析规则rules = { # 标签页列表 'list_urls': "//table[@class='tagCol']/tbody/tr/td/a/@href", # 详情页列表 'detail_urls': "//li[@class='subject-item']/div[@class='info']/h2/a/@href", # 页码 'page_urls': "//div[@id='subject_list']/div[@class='paginator']/a/@href", # 书名 'title': "//div[@id='wrapper']/h1/span/text()",}
# 定义队列list_queue = Queue()detail_queue = Queue()
# 定义协程池pool = Pool(size=10)
def crawl(url): """首页""" response = requests.get(url) list_urls = etree.HTML(response.text).xpath(rules['list_urls']) for list_url in list_urls: list_queue.put(urljoin(base_url, list_url))
def list_loop(): """采集列表页""" while True: list_url = list_queue.get() pool.spawn(crawl_list_page, list_url)
def detail_loop(): """采集详情页""" while True: detail_url = detail_queue.get() pool.spawn(crawl_detail_page, detail_url)
def crawl_list_page(list_url): """采集列表页""" html = requests.get(list_url).text detail_urls = etree.HTML(html).xpath(rules['detail_urls']) # 详情页 for detail_url in detail_urls: detail_queue.put(urljoin(base_url, detail_url))
# 下一页 list_urls = etree.HTML(html).xpath(rules['page_urls']) for list_url in list_urls: list_queue.put(urljoin(base_url, list_url))
def crawl_detail_page(list_url): """采集详情页""" html = requests.get(list_url).text title = etree.HTML(html).xpath(rules['title'])[0] print title
def main(): # 1. 标签页 crawl(start_url) # 2. 列表页 pool.spawn(list_loop) # 3. 详情页 pool.spawn(detail_loop) # 开始采集 pool.join()
if __name__ == '__main__': main()
复制代码


我们想要抓取豆瓣图书的整站数据,执行的流程是:


  1. 找到入口,也就是从书籍标签页进入,提取所有标签 URL

  2. 进入每个标签页,提取所有列表 URL

  3. 进入每个列表页,提取每一页的详情 URL 和下一页列表 URL

  4. 进入每个详情页,拿到书籍信息

  5. 如此往复循环,直到数据抓取完毕


这就是抓取一个整站的思路,很简单,无非就是分析我们浏览网站的行为轨迹,用程序来进行自动化的请求、抓取。


理想情况下,我们应该能够拿到整站的数据,但实际情况是,对方网站往往会采取防爬虫措施,在抓取一段时间后,我们的 IP 就会被封禁。


那如何突破这些防爬措施,拿到数据呢?我们继续优化代码。


防反爬的整站爬虫


# coding: utf8
"""防反爬的整站爬虫"""
from gevent import monkeymonkey.patch_all()
import randomfrom urlparse import urljoin
import requestsfrom lxml import etreeimport geventfrom gevent.pool import Poolfrom gevent.queue import Queue
base_url = 'https://book.douban.com'
# 种子URLstart_url = 'https://book.douban.com/tag/?view=type&icn=index-sorttags-all'
# 解析规则rules = { # 标签页列表 'list_urls': "//table[@class='tagCol']/tbody/tr/td/a/@href", # 详情页列表 'detail_urls': "//li[@class='subject-item']/div[@class='info']/h2/a/@href", # 页码 'page_urls': "//div[@id='subject_list']/div[@class='paginator']/a/@href", # 书名 'title': "//div[@id='wrapper']/h1/span/text()",}
# 定义队列list_queue = Queue()detail_queue = Queue()
# 定义协程池pool = Pool(size=10)
# 定义代理池proxy_list = [ '118.190.147.92:15524', '47.92.134.176:17141', '119.23.32.38:20189',]
# 定义UserAgentuser_agent_list = [ 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A', 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko',]
def fetch(url): """发起HTTP请求""" proxies = random.choice(proxy_list) user_agent = random.choice(user_agent_list) headers = {'User-Agent': user_agent} html = requests.get(url, headers=headers, proxies=proxies).text return html
def parse(html, rule): """解析页面""" return etree.HTML(html).xpath(rule)
def crawl(url): """首页""" html = fetch(url) list_urls = parse(html, rules['list_urls']) for list_url in list_urls: list_queue.put(urljoin(base_url, list_url))
def list_loop(): """采集列表页""" while True: list_url = list_queue.get() pool.spawn(crawl_list_page, list_url)
def detail_loop(): """采集详情页""" while True: detail_url = detail_queue.get() pool.spawn(crawl_detail_page, detail_url)
def crawl_list_page(list_url): """采集列表页""" html = fetch(list_url) detail_urls = parse(html, rules['detail_urls'])
# 详情页 for detail_url in detail_urls: detail_queue.put(urljoin(base_url, detail_url))
# 下一页 list_urls = parse(html, rules['page_urls']) for list_url in list_urls: list_queue.put(urljoin(base_url, list_url))
def crawl_detail_page(list_url): """采集详情页""" html = fetch(list_url) title = parse(html, rules['title'])[0] print title
def main(): # 1. 首页 crawl(start_url) # 2. 列表页 pool.spawn(list_loop) # 3. 详情页 pool.spawn(detail_loop) # 开始采集 pool.join()
if __name__ == '__main__': main()
复制代码


这个版本的代码与之前不同的是,在发起 HTTP 请求时,加上了随机代理 IP 和请求头 UserAgent,这也是突破防爬措施的常用手段。使用这些手段,加上一些质量高的代理 IP,应对一些小网站的数据抓取,不在话下。


当然,这里只为了展示一步步写爬虫、优化爬虫的思路,来达到抓取数据的目的,现实情况的抓取与反爬比想象中的更复杂,需要具体场景具体分析。


现有问题


经过上面这几步,我们想要哪个网站的数据,分析网站网页结构,写出代码应该不成问题。


但是,抓几个网站可以这么写,但抓几十个、几百个网站,你还能写下去吗?


当我们要采集的网站越来越多,编写的爬虫脚本也会越来越多,维护起来也会变得困难。由此暴露出来的问题包括:


  • 爬虫脚本繁多,管理和维护困难

  • 爬虫规则定义零散,可能会重复开发

  • 爬虫都是后台脚本,没有监控

  • 爬虫脚本输出的数据格式不统一,可能是文件,也可能也数据库

  • 业务要想使用爬虫的数据比较困难,没有统一的对接入口


这些问题都是我们在爬虫越写越多的情况下,难免会遇到的问题。


此时,我们迫切需要一个更好的解决方案,来更好地开发爬虫,所以爬虫平台应运而生。


那么如何设计一个通用化的垂直爬虫平台呢?


平台架构


我们来分析每个爬虫的共同点,结果发现:写一个爬虫无非就是规则、抓取、解析、入库这几步,那我们可不可以把每一块分别拆开呢?


根据这个思路,我们可以把爬虫平台设计成如下图:


爬虫平台架构图


我们的爬虫平台包括的模块有:


  • 配置服务:包括抓取页面配置、解析规则配置、数据清洗配置

  • 采集服务:只专注网页的下载,并配置防爬策略

  • 代理服务:持续提供稳定、可用的代理 IP

  • 清洗服务:针对爬虫采集到的数据进行进一步清洗和规整

  • 数据服务:爬虫数据的展示,以及业务系统对接


我们把一个爬虫的每一个环节,拆开做成一个个单独的服务模块,各模块各司其职,每个模块之间通过 API 或 消息队列进行通信。


这样做的好处是,每个模块维护只维护自己领域的功能,而且每个模块可独立升级和优化,不影响其他模块。


下面我们来看一下每个模块具体是如何设计的。


详细设计


配置服务


配置服务模块,此模块主要包括采集 URL 的配置、页面解析规则的配置、数据清洗规则的配置。


我们把爬虫的规则从爬虫脚本中抽离出来,单独配置与维护,这样做的好处是便于重用和管理


由于此模块只专注配置管理,那我们可以对配置规则进一步拆开,可以支持各种方式的数据解析模式,主要包含以下几种:


  • 正则解析规则

  • CSS 解析规则

  • XPATH 解析规则


每种解析规则模式,只配置对应的表达式即可。


采集服务可以写一个配置解析器,与配置服务进行对接,这个配置解析器内部实现各种模式具体的解析逻辑。


数据清洗规则配置,主要包含每个页面采集数据后,针对这个页面字段做进一步清洗和规整化的配置规则。例如采集服务抓取到的数据包含特殊字符,在采集服务中不会做进一步处理,而是放到清洗服务中去处理,具体的清洗规则可以自定义,常见的有删除某些特殊字符、特殊字段类型转换等等。


采集服务


此服务模块比较纯粹,就是写爬虫逻辑。我们可以像之前那样开发、调试、运行爬虫脚本那样,在此模块来开发和调试爬虫逻辑。


但之前的方式只能在命令行脚本中编写爬虫程序,然后调试运行,有没有一种好的方案可以把它做成可视化的呢?


我们调研了市面上 Python 语言实现的,比较好的爬虫框架,发现 pyspider 符合我们的需求,此框架的特点:


  • 支持分布式

  • 配置可视化

  • 可周期采集

  • 支持优先级

  • 任务可监控


pyspider架构图如下:

pyspider架构图


正所谓站在巨人的肩膀上,这个框架基本可以满足我们的需求,但为了更好地实现我们的爬虫平台,我们决定对其进行二次开发,并增强一些组件,使爬虫开发成本更低,更符合我们的业务规则。


二次开发的功能主要包括:


  • 开发配置解析器,对接配置服务,可以解析配置服务的多种规则模式

  • spider handler模块定制爬虫模板,并把爬虫任务进行分类,定义成模板,降低开发成本

  • fetcher模块新增代理 IP 调度机制,对接代理服务,并增加代理 IP 调度策略

  • result_worker模块把输出结果定制化,用来对接清洗服务


基于这个开源框架,并且增强其组件的方式,我们可以做出一个分布式、可视化、任务可监控、可生成爬虫模板的采集服务模块。


这个模块的功能,只专注于网页数据的采集。


代理服务


做爬虫的都知道,代理是突破防抓的常用手段,如何获取稳定、持续的代理呢?


代理服务这个模块,就是用来实现这个功能的。


此模块内部维护代理 IP 的质量和数量,并输出给采集服务,供其采集使用。


该模块主要包括两部分:


  • 免费代理

  • 付费代理


免费代理


免费代理 IP 主要由我们自己的代理采集程序采集获得,大致思路为:


  • 收集代理源

  • 定时采集代理

  • 测试代理

  • 输出可用代理


具体的实现逻辑可以参考我之前写的这篇文章:如何构建一个爬虫代理采集服务?


付费代理


免费代理的质量和稳定性相对较差,对于采集防爬比较厉害的网站,还是不够用。


这时我们会购买一些付费代理,专门用于采集这类防爬的网站,此代理 IP 一般为高匿代理,并定时更新。


免费代理 IP + 付费代理 IP,通过 API 的方式提供给采集服务。


清洗服务


清洗服务这个模块比较简单,主要接收采集服务输出的数据,然后根据对应的规则执行清洗逻辑。


例如网页字段与数据库字段归一转换,特殊字段清洗定制化等等。


这个服务模块运行了很多 Worker,最终把输出结果输送到数据服务。


数据服务


数据服务这个模块,会接收最终清洗后的结构化数据,统一入库。且针对其他业务系统需要的数据进行统一推送输出:


主要功能包括:


  • 数据平台展示

  • 数据推送

  • 数据 API


解决的问题


好了,经过以上爬虫平台的构建,我们基本解决了最开始困扰的几个问题,现在的爬虫平台可以实现的功能包括:


  • 爬虫脚本统一管理、配置可视化

  • 爬虫模板快速生成爬虫代码,降低开发成本

  • 采集进度可监控、易跟踪

  • 采集的数据统一输出

  • 业务系统使用爬虫数据更便捷


爬虫技巧


最后,分享一下做爬虫时候的一些技巧,从整体上来说,其实核心思想就一个:尽可能地模拟人的行为


主要包括以下几方面:


  • 随机 UserAgent 模拟不同的客户端(github 有 UserAgent 库,非常全面)

  • 随机代理 IP(高匿代理 + 代理调度策略)

  • Cookie 池(针对需要登录的采集行为)

  • JavaScript 渲染页面(使用无界面浏览器加载网页获取数据)

  • 验证码识别(OCR、机器学习)


当然,做爬虫是一个相互博弈的过程,有时没必要硬碰硬,遇到问题换个思路也是一种解决办法。例如,对方的移动客户端防抓厉害,那去看一看对方的 PC 站可不可以搞一下?WAP 端是否可以尝试一下?在有限的成本拿到数据才是爬虫的目的。


爬虫做的越来越多时,你就会发现,这是一个策略和技巧同样重要的领域。


以上就是构建一个垂直爬虫平台的设计思路,从最简单的爬虫脚本,到写越来越多的爬虫,到难以维护,再到整个爬虫平台的构建,一步步都是遇到问题解决问题的产物,在我们真正发现核心问题时,解决思路也就不难了。


爬虫系列文章:



我的 Python 进阶系列文章:



关注「水滴与银弹」公众号,第一时间获取优质技术干货。7 年资深后端研发,用简单的方式把技术讲清楚。


发布于: 2020 年 11 月 03 日阅读数: 172
用户头像

Kaito

关注

公众号「水滴与银弹」,不一样的技术视角。 2017.11.17 加入

坐标北京,7年资深后端研发,从事基础架构和数据库中间件研发。擅长把晦涩难懂的技术,用简单的方式讲清楚。

评论

发布
暂无评论
如何构建一个通用的垂直爬虫平台?