python 用递归实现通用爬虫解析器


Posted in Python onApril 16, 2021

我们在写爬虫的过程中,除了研究反爬之外,几乎全部的时间都在写解析逻辑。那么,生命苦短,为什么我们不写一个通用解析器呢?对啊!为什么不呢?开整!

需求分析

爬虫要解析的网页类型无外乎 html、json 以及一些二进制文件(video、excel 文件等)。既然要做成通用解析器,我们有两种实现方式,一种是将网页内容转换成统一的形式,然后用对应的解析规则去解析,比如全部将网页内容转换成 html 形式,然后用 xpath 去提取。

python 用递归实现通用爬虫解析器

另外一种是配置文件预先告知的方式,你配置成什么类型,解析器就通过对应的解析规则去解析。

统一网页形式,需要做大量的网页内容形式转换,而配置文件预先告知则需要在配置时指定更多解析字段。相比较而言,通过第二种方式,未来改变较多的是配置规则,不需要动核心代码,引入 bug 的可能性较低。因此这里我们采用第二种方式实现解析器

进一步分析

解析器对于网页内容的提取,本质上和我们在本地电脑上查找和整理文件,没有什么差别。比如像下面这样

python 用递归实现通用爬虫解析器

解析内容就是从中提取我们想要的信息,然后整理成我们希望的格式。比如上面的内容,我们提取出来的形式应该是这样

{
  "design": "设计图.psd",
  "software": "sketch.dmg"
}

而在实际的爬虫开发过程中,网页形式远比以上的复杂。其实遇到最多的问题是在一组列表中嵌套一个列表,我们需要把这种形式提取出来。比如像下面这种形式

{
    "a": "a",
    "b": [
        {"c": "c1", "d": "d1"},
        {"c": "c2", "d": "d2"}]
}

他提取出信息后应该是这样

[
  {
    "a": "a",
    "c": "c1",
    "d": "d1"
  },
  {
    "a": "a",
    "c": "c2",
    "d": "d2"
  }
]

如果小伙伴对于算法熟悉的话,应该能察觉出这种遍历用递归来写是非常方便的。但要注意的是 python 会限定递归的层数,小伙伴可以通过下面这个方法查看递归限定的层数

import sys
print(sys.getrecursionlimit())

>>>1000

我这边限定的层数是 1k。对于解析网页来说完全够用了,如果哪个人把网页解析逻辑嵌套了 1000 层,我建议你直接跟老板提放弃这个网页吧!

再进一步分析

我们已经知道对于通用解析来说,就是通过配置解析规则提取页面的对应信息。而针对有列表层级的网页可能还涉及递归遍历问题。那如何去配置这种解析规则呢?其实很简单,只需要在进入每一个层级之前先指定该层的数据形式,比如下面这个原数据

{
  "a": "a",
  "b": [
          {"c": "c1", "d": "d1"},
          {"c": "c2", "d" : "d2"}
       ]
}

想提取嵌套信息,我们的解析规则就应该是这样的

[
 {
  "$name": "a",
  "$value_type": "raw",
  "$parse_method": "json",
  "$parse_rule": "a",
  "$each": []
 },
 {
  "$name": "__datas__",
  "$value_type": "recursion",
  "$parse_method": "json",
  "$parse_rule": "b",
  "$each": [
        {  
         "$name": "c",
          "$value_type": "raw",
         "$parse_method": "json",
         "$parse_rule": "c",
         "$each": []
        },
        {  
         "$name": "d",
          "$value_type": "raw",
         "$parse_method": "json",
         "$parse_rule": "d",
         "$each": []
        }
      ]
 }
]

其中 $name 字段表示我们最终希望最外层数据所拥有的字段名,当然如果是需要递归到内层的字段,则将列表保存为 __datas__ ,然后根据这个 __datas__ 进行内层结构的解析。最终我们得到的数据结构应该是这样的

[
  {"a": "a", "c": "c1", "d": "d1"}, 
  {"a": "a", "c": "c2", "d": "d2"}
]

以上我们只演示了 json 的解析规则,如果要拿来解析 html 对象呢?很简单,将解析方式改为 xpath 对象,然后传入 xpath 解析语法即可。

代码实现

总共分成两部分,一部分根据原最终结果和规则进行打包,将所有涉及 recursion 逻辑的字段进行转换,代码如下

def _pack_json(result, rules):
        item = {}

        for p_rule in rules:

            if p_rule.get("$value_type") == "raw":
                if p_rule.get("$parse_method") == "json":
                    item[p_rule.get("$name")] = glom(result, p_rule.get("$parse_rule"))

            elif p_rule.get("$value_type") == "recursion":
                if p_rule.get("$parse_method") == "json":
                    tmp_result = glom(result, p_rule.get("$parse_rule"))
                    total_result = []
                    for per_r in tmp_result:
                        total_result.append(_pack_json(per_r, p_rule.get("$each")))
                    item[p_rule.get("$name")] = total_result
        return item

另外一部分将上一步得到的进行解析,将打包得到的结果进行解包,即将所有内嵌的数据提到最外层,代码如下

def _unpack_datas(result: dict) -> list:
        if "__datas__" not in result:
            return [result]

        item_results = []
        all_item = result.pop("__datas__")

        for per_item in all_item:
            if "__datas__" in per_item:
                tmp_datas = per_item.pop("__datas__")
                for per_tmp_data in tmp_datas:
                    tmp_item = _unpack_datas(per_tmp_data)
                    for per_tmp_item in tmp_item:
                        item_results.append({**per_tmp_item, **per_item})
            else:
                item_results.append({**result, **per_item})

        return item_results

后再包一层执行入口就可以了,完整代码如下

from loguru import logger

from glom import glom


def parse(result, rules):

    def _pack_json(result, rules):
        item = {}

        for p_rule in rules:

            if p_rule.get("$value_type") == "raw":
                if p_rule.get("$parse_method") == "json":
                    item[p_rule.get("$name")] = glom(result, p_rule.get("$parse_rule"))

            elif p_rule.get("$value_type") == "recursion":
                if p_rule.get("$parse_method") == "json":
                    tmp_result = glom(result, p_rule.get("$parse_rule"))
                    total_result = []
                    for per_r in tmp_result:
                        total_result.append(_pack_json(per_r, p_rule.get("$each")))
                    item[p_rule.get("$name")] = total_result
        return item

    def _unpack_datas(result: dict) -> list:
        if "__datas__" not in result:
            return [result]

        item_results = []
        all_item = result.pop("__datas__")

        for per_item in all_item:
            if "__datas__" in per_item:
                tmp_datas = per_item.pop("__datas__")
                for per_tmp_data in tmp_datas:
                    tmp_item = _unpack_datas(per_tmp_data)
                    for per_tmp_item in tmp_item:
                        item_results.append({**per_tmp_item, **per_item})
            else:
                item_results.append({**result, **per_item})

        return item_results

    pack_result = _pack_json(result, rules)
    logger.info(pack_result)
    return _unpack_datas(pack_result)

以上,就是通用解析器的完整案例。案例中仅实现了对于 json 的支持,小伙伴可以基于自己的项目,改造成其他的解析形式。通用解析其实是鸡仔为了偷懒写的,因为鸡仔发现,在爬虫开发中,大部分工作都耗在解析这部分。而有了通用解析的前端页面,运营和数据分析师就可以根据自己的需要配置自己想爬取的站点了。人生苦短,你懂得。我去摸鱼了~

实现方式请移步至 github 查看:https://github.com/hacksman/learn_lab/blob/master/small_bug_lab/general_parser.py

以上就是python 用递归实现通用爬虫解析器的详细内容,更多关于python 递归实现爬虫解析器的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
Python脚本实现下载合并SAE日志
Feb 10 Python
Python argv用法详解
Jan 08 Python
Python 装饰器深入理解
Mar 16 Python
Python向日志输出中添加上下文信息
May 24 Python
python jieba分词并统计词频后输出结果到Excel和txt文档方法
Feb 11 Python
Python设计模式之原型模式实例详解
Jan 18 Python
Python3实现从排序数组中删除重复项算法分析
Apr 03 Python
python实现自动化上线脚本的示例
Jul 01 Python
python 设置输出图像的像素大小方法
Jul 04 Python
python爬虫 urllib模块反爬虫机制UA详解
Aug 20 Python
Tensorflow矩阵运算实例(矩阵相乘,点乘,行/列累加)
Feb 05 Python
python标准库sys和OS的函数使用方法与实例详解
Feb 12 Python
MATLAB 如何求取离散点的曲率最大值
用Python远程登陆服务器的步骤
Matlab求解数组中的最大值及它所在的具体位置
Apr 16 #Python
python 机器学习的标准化、归一化、正则化、离散化和白化
Apr 16 #Python
python中print格式化输出的问题
Apr 16 #Python
CocosCreator ScrollView优化系列之分帧加载
深度学习tensorflow基础mnist
You might like
php数组合并的二种方法
2014/03/21 PHP
PHP程序员常见的40个陋习,你中了几个?
2014/11/20 PHP
php导出中文内容excel文件类实例
2015/07/06 PHP
PHP用mb_string函数库处理与windows相关中文字符及Win环境下开启PHP Mb_String方法
2015/11/11 PHP
Zend Framework教程之Zend_Db_Table表关联实例详解
2016/03/23 PHP
PHP开发之归档格式phar文件概念与用法详解【创建,使用,解包还原提取】
2017/11/17 PHP
thinkPHP5.0框架验证码调用及点击图片刷新简单实现方法
2018/09/07 PHP
showModelessDialog()使用详解
2006/09/21 Javascript
javascript之解决IE下不渲染的bug
2007/06/29 Javascript
使用百度地图api实现根据地址查询经纬度
2014/12/11 Javascript
解析Node.js异常处理中domain模块的使用方法
2016/02/16 Javascript
javascript 将共享属性迁移到原型中去的实现方法
2016/08/31 Javascript
Jil,高效的json序列化和反序列化库
2017/02/15 Javascript
jquery实现左右滑动式轮播图
2017/03/02 Javascript
React学习笔记之条件渲染(一)
2017/07/02 Javascript
React 使用Hooks简化受控组件的状态绑定
2019/03/18 Javascript
node.js使用http模块创建服务器和客户端完整示例
2020/02/10 Javascript
浅谈python字符串方法的简单使用
2016/07/18 Python
python实现石头剪刀布小游戏
2021/01/20 Python
PyCharm License Activation激活码失效问题的解决方法(图文详解)
2020/03/12 Python
Python 通过监听端口实现唯一脚本运行方式
2020/05/05 Python
Html5实现如何在两个div元素之间拖放图像
2013/03/29 HTML / CSS
html5本地存储 localStorage操作使用详解
2016/09/20 HTML / CSS
HTML5触摸事件实现移动端简易进度条的实现方法
2018/05/04 HTML / CSS
印尼极简主义和实惠的在线家具店:Fabelio
2019/03/27 全球购物
美国健康和保健平台:healtop
2020/07/02 全球购物
社区党员先进事迹
2014/01/22 职场文书
社区安全检查制度
2014/02/03 职场文书
人事部专员岗位职责
2014/03/04 职场文书
写求职信有哪些注意事项
2014/05/08 职场文书
119消防日活动总结
2014/08/29 职场文书
女性健康知识讲座通知
2015/04/23 职场文书
2015教师个人年度工作总结
2015/10/23 职场文书
Elasticsearch 批量操作
2022/04/19 Python
MySql中的json_extract函数处理json字段详情
2022/06/05 MySQL