爬虫平台 Crawlab 核心原理--自动提取字段算法 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
tikazyq
V2EX    Python

爬虫平台 Crawlab 核心原理--自动提取字段算法

  •  
  •   tikazyq
    tikazyq 2019 年 6 月 4 日 3416 次点击
    这是一个创建于 2450 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    实际的大型爬虫开发项目中,爬虫工程师会被要求抓取监控几十上百个网站。一般来说这些网站的结构大同小异,不同的主要是被抓取项的提取规则。传统方式是让爬虫工程师写一个通用框架,然后将各网站的提取规则做成可配置的,然后将配置工作交给更初级的工程师或外包出去。这样做将爬虫开发流水线化,提高了部分生产效率。但是,配置的工作还是一个苦力活儿,还是非常消耗人力。因此,自动提取字段应运而生。

    自动提取字段是 Crawlab 在版本 v0.2.2中在可配置爬虫基础上开发的新功能。它让用户不用做任何繁琐的提取规则配置,就可以自动提取出可能的要抓取的列表项,做到真正的“一键抓取”,顺利的话,开发一个网站的爬虫可以半分钟内完成。市面上有利用机器学习的方法来实现自动抓取要提取的抓取规则,有一些可以做到精准提取,但遗憾的是平台要收取高额的费用,个人开发者或小型公司一般承担不起。

    Crawlab 的自动提取字段是根据人为抓取的模式来模拟的,因此不用经过任何训练就可以使用。而且,Crawlab 的自动提取字段功能不会向用户收取费用,因为 Crawlab 本身就是免费的。

    算法介绍

    算法的核心来自于人的行为本身,通过查找网页中看起来像列表的元素来定位列表及抓取项。一般我们查找列表项是怎样的一个过程呢?有人说:这还不容易吗,一看就知道那个是各列表呀!兄弟,拜托... 咱们是在程序的角度谈这个的,它只理解 HTML、CSS、JS 这些代码,并不像你那样智能。

    我们识别一个列表,首先要看它是不是有很多类似的子项;其次,这些列表通常来说看起来比较“复杂”,含有很多看得见的元素;最后,我们还要关注分页,分页按钮一般叫做“下一页”、“下页”、“ Next ”、“ Next Page ”等等。

    用程序可以理解的语言,我们把以上规则总结如下:

    列表项

    1. 从根节点自上而下遍历标签;
    2. 对于每一个标签,如果包含多个同样的子标签,判断为列表标签候选;
    3. 取子标签(递归)个数最多的列表标签候选为列表标签;

    列表子项

    1. 对以上规则提取的列表标签,对每个子标签(递归)进行遍历
    2. 将有 href 的 a 标签为加入目标字段;
    3. 将有 text 的标签为加入目标字段。

    分页

    1. 对于每一个标签,如果标签文本为特定文本(“下一页”、“下页”、“ next page ”、“ next ”),选取该标签为目标标签。

    这样,我们就设计好了自动提取列表项、列表子项、分页的规则。剩下的就是写代码了。我知道这样的设计过于简单,也过于理想,没有考虑到一些特殊情况。后面我们将通过在一些知名网站上测试看看我们的算法表现如何。

    算法实现

    算法实现很简单。为了更好的操作 HTML 标签,我们选择了lxml库作为 HTML 的操作库。lxml是 python 的一个解析库,支持 HTML 和 XML 的解析,支持 XPath、CSS 解析方式,而且解析效率非常高。

    自上而下的遍历语法是sel.iter()seletree.Element,而iter会从根节点自上而下遍历各个元素,直到遍历完所有元素。它是一个generator

    构造解析树

    在获取到页面的 HTML 之后,我们需要调用lxml中的etree.HTML方法构造解析树。代码很简单如下,其中rrequests.getResponse

    # 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 选择器。以下代码实现的逻辑主要就是根据上面得到的目标标签根据其idclass属性来生成 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 的使用和开发。

    9 条回复    2019-07-12 13:53:48 +08:00
    1508281800
        1
    1508281800  
       2019 年 6 月 5 日
    来学习一个
    liwl
        2
    liwl  
       2019 年 6 月 5 日   1
    可惜没有 docker 版本...
    tikazyq
        3
    tikazyq  
    OP
       2019 年 6 月 5 日
    @liwl docker 版本正在开发,敬请期待和关注
    KyX
        4
    KyX  
       2019 年 6 月 5 日
    好工具,先码一个。
    有点像 readability 算法,最近也在研究这些
    tikazyq
        5
    tikazyq  
    OP
       2019 年 6 月 5 日 via iPhone
    @KyX 啥叫 readability 算法?看起来好高级
    GoTop
        6
    GoTop  
       2019 年 7 月 2 日
    readability 是一个有名的自动提取网页正文内容的库

    而楼主的这个是自动提取分页的链接
    tikazyq
        7
    tikazyq  
    OP
       2019 年 7 月 3 日
    @GoTop 有空了去研究研究,多
    GoTop
        8
    GoTop  
       2019 年 7 月 9 日
    列表项 和 列表专项 有什么区别?能解释一下吗?
    tikazyq
        9
    tikazyq  
    OP
       2019 年 7 月 12 日
    @GoTop 列表项是列表子元素中的最大元素,列表子项就是列表项下的子元素
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1888 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 14:59 PVG 22:59 LAX 06:59 JFK 09:59
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86