⚠注意: 可配置爬虫如今仅在Python版本(v0.2.1-v0.2.4)可用,在最新版本Golang版本(v0.3.0)还暂时不可用,后续会加上,请关注近期更新javascript
实际的大型爬虫开发项目中,爬虫工程师会被要求抓取监控几十上百个网站。通常来讲这些网站的结构大同小异,不一样的主要是被抓取项的提取规则。传统方式是让爬虫工程师写一个通用框架,而后将各网站的提取规则作成可配置的,而后将配置工做交给更初级的工程师或外包出去。这样作将爬虫开发流水线化,提升了部分生产效率。可是,配置的工做仍是一个苦力活儿,仍是很是消耗人力。所以,自动提取字段应运而生。css
自动提取字段是Crawlab在版本v0.2.2中在可配置爬虫基础上开发的新功能。它让用户不用作任何繁琐的提取规则配置,就能够自动提取出可能的要抓取的列表项,作到真正的“一键抓取”,顺利的话,开发一个网站的爬虫能够半分钟内完成。市面上有利用机器学习的方法来实现自动抓取要提取的抓取规则,有一些能够作到精准提取,但遗憾的是平台要收取高额的费用,我的开发者或小型公司通常承担不起。html
Crawlab的自动提取字段是根据人为抓取的模式来模拟的,所以不用通过任何训练就可使用。并且,Crawlab的自动提取字段功能不会向用户收取费用,由于Crawlab自己就是免费的。java
算法的核心来自于人的行为自己,经过查找网页中看起来像列表的元素来定位列表及抓取项。通常咱们查找列表项是怎样的一个过程呢?有人说:这还不容易吗,一看就知道那个是各列表呀!兄弟,拜托... 我们是在程序的角度谈这个的,它只理解HTML、CSS、JS这些代码,并不像你那样智能。node
咱们识别一个列表,首先要看它是否是有不少相似的子项;其次,这些列表一般来讲看起来比较“复杂”,含有不少看得见的元素;最后,咱们还要关注分页,分页按钮通常叫作“下一页”、“下页”、“Next”、“Next Page”等等。python
用程序能够理解的语言,咱们把以上规则总结以下:git
列表项github
列表子项算法
分页微信
这样,咱们就设计好了自动提取列表项、列表子项、分页的规则。剩下的就是写代码了。我知道这样的设计过于简单,也过于理想,没有考虑到一些特殊状况。后面咱们将经过在一些知名网站上测试看看咱们的算法表现如何。
算法实现很简单。为了更好的操做HTML标签,咱们选择了lxml
库做为HTML的操做库。lxml
是python的一个解析库,支持HTML和XML的解析,支持XPath、CSS解析方式,并且解析效率很是高。
自上而下的遍历语法是sel.iter()
。sel
是etree.Element
,而iter
会从根节点自上而下遍历各个元素,直到遍历完全部元素。它是一个generator
。
在获取到页面的HTML以后,咱们须要调用lxml
中的etree.HTML
方法构造解析树。代码很简单以下,其中r
为requests.get
的Response
# get html parse tree
sel = etree.HTML(r.content)
复制代码
这段带代码在SpiderApi._get_html
方法里。源码请见这里。
在开始构建算法以前,咱们须要实现一些辅助函数。全部函数是封装在SpiderApi
类中的,因此写法与类方法同样。
@staticmethod
def _get_children(sel):
# 获取全部不包含comments的子节点
return [tag for tag in sel.getchildren() if type(tag) != etree._Comment]
复制代码
@staticmethod
def _get_text_child_tags(sel):
# 递归获取全部文本子节点(根节点)
tags = []
for tag in sel.iter():
if type(tag) != etree._Comment and tag.text is not None and tag.text.strip() != '':
tags.append(tag)
return tags
复制代码
@staticmethod
def _get_a_child_tags(sel):
# 递归获取全部超连接子节点(根节点)
tags = []
for tag in sel.iter():
if tag.tag == 'a':
if tag.get('href') is not None and not tag.get('href').startswith('#') and not tag.get(
'href').startswith('javascript'):
tags.append(tag)
return tags
复制代码
下面是核心中的核心!同窗们请集中注意力。
咱们来编写获取列表项的代码。如下是得到列表标签候选列表list_tag_list
的代码。看起来稍稍有些复杂,但其实逻辑很简单:对于每个节点,咱们得到全部子节点(一级),过滤出高于阈值(默认10)的节点,而后过滤出节点的子标签类别惟一的节点。这样候选列表就获得了。
list_tag_list = []
threshold = spider.get('item_threshold') or 10
# iterate all child nodes in a top-down direction
for tag in sel.iter():
# get child tags
child_tags = self._get_children(tag)
if len(child_tags) < threshold:
# if number of child tags is below threshold, skip
continue
else:
# have one or more child tags
child_tags_set = set(map(lambda x: x.tag, child_tags))
# if there are more than 1 tag names, skip
if len(child_tags_set) > 1:
continue
# add as list tag
list_tag_list.append(tag)
复制代码
接下来咱们将从候选列表中筛选出包含最多文本子节点的节点。听起来有些拗口,打个比方:一个电商网站的列表子项,也就是产品项,必定是有许多例如价格、产品名、卖家等信息的,所以会包含不少文本节点。咱们就是经过这种方式过滤掉文本信息很少的列表(例如菜单列表、类别列表等等),获得最终的列表。在代码里咱们存为max_tag
。
# find the list tag with the most child text tags
max_tag = None
max_num = 0
for tag in list_tag_list:
_child_text_tags = self._get_text_child_tags(self._get_children(tag)[0])
if len(_child_text_tags) > max_num:
max_tag = tag
max_num = len(_child_text_tags)
复制代码
下面,咱们将生成列表项的CSS选择器。如下代码实现的逻辑主要就是根据上面获得的目标标签根据其id
或class
属性来生成CSS选择器。
# get list item selector
item_selector = None
if max_tag.get('id') is not None:
item_selector = f'#{max_tag.get("id")} > {self._get_children(max_tag)[0].tag}'
elif max_tag.get('class') is not None:
cls_str = '.'.join([x for x in max_tag.get("class").split(' ') if x != ''])
if len(sel.cssselect(f'.{cls_str}')) == 1:
item_selector = f'.{cls_str} > {self._get_children(max_tag)[0].tag}'
复制代码
找到目标列表项以后,咱们须要作的就是将它下面的文本标签和超连接标签提取出来。代码以下,就不细讲了。感兴趣的读者能够看源码来理解。
# get list fields
fields = []
if item_selector is not None:
first_tag = self._get_children(max_tag)[0]
for i, tag in enumerate(self._get_text_child_tags(first_tag)):
if len(first_tag.cssselect(f'{tag.tag}')) == 1:
fields.append({
'name': f'field{i + 1}',
'type': 'css',
'extract_type': 'text',
'query': f'{tag.tag}',
})
elif tag.get('class') is not None:
cls_str = '.'.join([x for x in tag.get("class").split(' ') if x != ''])
if len(tag.cssselect(f'{tag.tag}.{cls_str}')) == 1:
fields.append({
'name': f'field{i + 1}',
'type': 'css',
'extract_type': 'text',
'query': f'{tag.tag}.{cls_str}',
})
for i, tag in enumerate(self._get_a_child_tags(self._get_children(max_tag)[0])):
# if the tag is <a...></a>, extract its href
if tag.get('class') is not None:
cls_str = '.'.join([x for x in tag.get("class").split(' ') if x != ''])
fields.append({
'name': f'field{i + 1}_url',
'type': 'css',
'extract_type': 'attribute',
'attribute': 'href',
'query': f'{tag.tag}.{cls_str}',
})
复制代码
分页的代码很简单,实现也很容易,就很少说了,你们感兴趣的能够看源码
这样咱们就实现了提取列表项以及列表子项的算法。
要使用自动提取字段,首先得安装Crawlab。如何安装请查看Github。
Crawlab安装完毕运行起来后,得建立一个可配置爬虫,详细步骤请参考[爬虫手记] 我是如何在3分钟内开发完一个爬虫的 。
建立完毕后,咱们来到建立好的可配置爬虫的爬虫详情的配置标签,输入开始URL,点击提取字段按钮,Crawlab将从开始URL中提取列表字段。
接下来,点击预览看看这些字段是否为有效字段,能够适当增删改。能够的话点击运行,爬虫就开始爬数据了。
好了,你须要作的就是这几步,其他的交给Crawlab来作就能够了。
本文在对排名前10的电商网站上进行了测试,仅有3个网站不能识别(分别是由于“动态内容”、“列表没有id/class”、“lxml定位元素问题”),成功率为70%。读者们能够尝试用Crawlab自动提取字段功能对大家本身感兴趣的网站进行测试,看看是否符合预期。结果的详细列表以下。
网站 | 成功提取 | 缘由 |
---|---|---|
淘宝 | N | 动态内容 |
京东 | Y | |
阿里巴巴1688 | Y | |
搜了网 | Y | |
苏宁易购 | Y | |
糯米网 | Y | |
买购网 | N | 列表没有id/class |
天猫 | Y | |
当当网 | N | lxml定位元素问题 |
Crawlab的算法固然还须要改进,例如考虑动态内容和列表没有id/class等定位点的时候。也欢迎各位前来试用,甚至贡献该项目。
Github: tikazyq/crawlab
若是您以为Crawlab对您的平常开发或公司有帮助,请加做者微信拉入开发交流群,你们一块儿交流关于Crawlab的使用和开发。