Metadata-Version: 2.1
Name: parser-engine
Version: 0.1.4
Summary: template-driven parser engine for scrapy
Home-page: https://gitlab.baixing.cn/spider/parser_engine
Author: huangzhen
Author-email: huangzhen@baixing.com
License: MIT
Description: ## Parser Engine 
        代号PE，为[scrapy](https://scrapy.org/)框架量身定制的 **"可配置化的响应解析器引擎"**。
        
        主要支持以下特性：
        - [x] 基于xpath、jsonpath等规则解析html和json格式的http请求响应体
        - [x] 输出值基于`scrapy.Item`，自动定位并返回实例化的`Item`
        - [x] 支持 **父节点**、**列表for循环解析**
        - [x] 字段枚举值的映射
        - [x] 设置字段为必有字段，缺失时丢弃整个`Item`
        - [x] 将list类型的字段拼接成字符串
        
        ### 安装
        - 安装最新体验版: 
            >`pip install git+https://github.com/Danceiny/parser_engine`
            
        - 安装稳定版：
            >`pip install -U parser_engine`
        
        ### 更新日志
        请移步[CHANGELOG.md](CHANGELOG.md)。
        
        ### 速览
        >如何使用PE从零开始快速编写一个网站的爬虫，并持久化数据？可移步[快速开始](./TUTORIAL.md)。
        
        - 极简版，使用`CrawlSpider`的rules机制。
        ```python
        from parser_engine import TemplateAnnotation
        from scrapy.spiders.crawl import CrawlSpider
        @TemplateAnnotation(tpls="demo")
        class DemoSpider4(CrawlSpider):
            name = "demo4"
            start_urls = [
                "http://github.cannot.cc/baixing-helper"
            ]
        ```
        
        - 使用scrapy_redis，解析start_urls的响应。
        ```python
        from parser_engine import TemplateAnnotation
        from parser_engine.clue.spider import ClueSpider
        @TemplateAnnotation(start_url_tpl=({
            "name": "zhongguozhongqi_xiaoshouwangluo",
            "itemname": "HuocheDealerItem",
            "parent": {
                "xpath": "//tr[@class=\"bgcolor2\"]"
            },
            "fields": [
                {
                    "key": "area",
                    "xpath": "td[1]/text()",
                    "value_type": "stripped_string"
                }, {
                    "key": "leads_name",
                    "xpath": "td[2]/text()",
                    "value_type": "stripped_string"
                }, {
                    "key": "address",
                    "xpath": "td[3]/text()",
                    "value_type": "stripped_string"
                }, {
                    "key": "phone",
                    "xpath": "td[5]/text()",
                    "value_type": "stripped_string"
                }
            ]
        }), channel='zhongguozhongqi', leads_src='中国重汽')
        class ZhongguozhongqiSpider(ClueSpider):
            name = 'zhongguozhongqi'
            def parse(self, response):
                items = self._parse_start_url(response)
                for item in items:
                    phone = item.get('phone')
                    if phone:
                        item['phone'] = phone.replace('、', ',')
                    yield item
                self.finish_clue(response, len(items))
        ```
        
        - 使用scrapy_redis，灵活运用多种PE特性。
        ```python
        from parser_engine.clue.spider import ClueSpider
        from parser_engine import TemplateAnnotation
        from parser_engine.clue.items import ClueItem
        from parser_engine.request import TaskRequest
        from scrapy import Request
        @TemplateAnnotation(start_url_tpl=({
                                               "name": "youka_shop_listing_api",
                                               "parent": {
                                                   "json_key": "data",
                                               },
                                               "fields": [{
                                                   "key": "totalPage",
                                                   "json_key": "totalPage",
        
                                               }, {
                                                   "key": "ids",
                                                   "json_path": "dataList[*].id"
                                               }]
                                           },),
            tpls=({
                "name": "youka_shop_detail_api",
                "itemname": "HuocheDealerItem",
                "parent": {
                    "json_key": "data",
                },
                "fields": [{
                    "key": "company_type",
                    "json_key": "category",
                    "mapper": {
                        1: "二手车直营店",
                        2: "4S店"
                    }
                }, {
                    "key": "dealer_id",
                    "json_key": "id",
                    "required": 1,
                }, {
                    "key": "leads_name",
                    "json_key": "shopName",
                }, {
                    "key": "area",
                    "json_path": "districtDto.districtName",
                    "value_type": "singleton"
                }, {
                    "key": "city",
                    "json_path": "cityDto.cityName",
                    "value_type": "singleton"
                }, {
                    "key": "service_phone",
                    "default_value": "",
                }, {
                    "key": "wechat",
                    "json_key": "wechat",
                },  {
                    "key": "tags",
                    "json_key": "tags",
                    "join": ","
                }]
            }), channel='youka', leads_src='优卡')
        class YoukaSpider(ClueSpider):
            name = 'youka'
            custom_settings = {
                'CONCURRENT_REQUESTS': 2,
                'CONCURRENT_REQUESTS_PER_DOMAIN': 1
            }
            def parse(self, response):
                items = self._parse_start_url(response)
                meta = response.meta
                clue_id = meta.get('clue_id')
                from_url = response.request.url
                if meta.get('open_pages'):
                    total_page = items[0]['totalPage']
                    import re
                    current_page = int(re.findall('page=(\\d+)', from_url)[0])
                    for i in range(1, total_page + 1):
                        if current_page == i:
                            continue
                        url = "http://www.china2cv.com/truck-foton-web/api/shop/v1/getShopList?page=%d&pageSize=10" % i
                        yield ClueItem({"project": "huoche", "spider": self.name, "req": TaskRequest(
                            url=url,
                            meta={"from_clue_id": clue_id}
                        )})
                for item in items:
                    for id in item['ids']:
                        r = Request(url="http://www.china2cv.com/truck-foton-web/api/shop/v1/getShopInfo?shopId=%d" % int(id),
                                    callback=self._response_downloaded)
                        r.meta.update(rule=0, from_clue_id=clue_id)
                        yield r
        
            def process_results(self, response, results):
                for item in results:
                    item['url'] = 'http://www.china2cv.com/storeDetail.html?typess=1&shopId=' + str(item['dealer_id'])
                return results
        ```
        
        完整示例请参考：[examples](./examples)。
        
        ### 原理
        - 解析器
            >PE向调用方提供一套简单、易懂的参数，实际会将其`编译`成较为复杂的xpath表达式，再借助scrapy封装的解析器将所需内容提取出来。
        
        - 返回值
            >通过配置`itemname`参数，PE将`反射`得到所需的`Item`类，按照配置的映射关系，从提取出的值创建一个`Item`实例（或者多个），并返回一个可迭代的`Item`实例列表。
            
        ### 已知问题
        
        - 如果提取规则较为复杂，建议直接使用xpath和css参数，因为PE的参数编译可能存在问题。
        - 如果对性能有比较强的需求，不建议使用。
        
        ### 特性介绍
        - [x] 支持表格、列表等形式的批量解析
            >通过在模板中定义一个父节点，从html页面中的表格、列表等组件中，批量抓取多个同类item
            
            >用法示例：使用`{"parent": {}, "fields": []}`这样的配置，将首先查找匹配`parent`的节点，然后遍历其每个子节点，对每个子节点应用`fields`规则，生成一个item。
            
        - [x] Clue Mechanism
            > 基于scrapy_redis的线索机制，可持久化（目前支持mysql）线索，方便追踪。
        
        - [x] 值映射
            >一个简单的需求场景：API返回的性别字段是0和1，但是需要将其转换成"男"和"女"。
            
        ### 待做清单
        - 优化
            - [ ] 支持直接在`Item`的类定义中定义模板
                >用法示例：原模板的`itemname`参数通过注解传参，其他的模板参数定义在`Item`类中，如下所示。
                ```
                class MyItem(scrapy.Item):
                    tpl = {"parent": {"xpath":"//div[@id=\"contentDiv\"]//table/tbody/tr[position()>1]"}
                    name = Field(xpath="//a[@href]/text()")
                ```
        
        ### scrapy配置参数
        
        - PARSER_ENGINE_CONFIG_FILE 
            > 模板配置文件的位置。默认是`parser_engine.json`，与`scrapy.cfg`文件同级。
        - MYSQL
            > MySQL配置信息，dict类型，包含以下字段：
                - DATABASE
                - USER
                - PASSWORD
                - PORT 默认3306
                - HOST 默认127.0.0.1
        
        下面的字段在MYSQL配置缺失时生效：
        - MYSQL_USER
        - MYSQL_PASSWORD
        - MYSQL_PORT
        - MYSQL_DATABASE
        
        ### 模板参数
        模板配置文件，如`parser_engine.json`，其构成结构如下：
        ```json
        {
          "templates": [
            {
              "name": "tpl1"   
            },
            {
              "name": "tpl2"
            }
          ]
        
        }
        
        ```
        模板配置文件中`templates`列表中的每一项，即为一个模板。
        
        所谓模板，对应的类是[PETemplate](./parser_engine/template.py)，其构成结构如下：
        
        - name 必要。相当于该模板在该模板文件中的id
        - parent 不必要。如果指定，将按照其指定的规则，从响应中取出某节点作为后续提取规则的根节点。
        - itemname 不必要。如果指定，将尝试加载实例化该item类；如果没有找到类或者没有指定，则返回`dict`类型的原始数据。
        - extract_keys 不必要。json格式专属，用于直接从某个json对象中提取一组键值，通常可以搭配`parent`参数使用，较为高效。
        - extract_keys_map 不必要。类似`extract_keys_map`，适用于需要转换原json对象中的键名的场景。
        - fields 不必要。是一个`字段`的数组。
        
        所谓字段，对应的类是[PEField](./parser_engine/template.py)，属于PE的核心，其构成结构如下：
        - key 必要。提取结果保存的键名，通常是item的某个`Field`的变量名。
        - xpath
        - css
        - tags 不必要。一组有序的html标签。如`["div","a"]`会被翻译成`//div/a`的xpath。
        - attr_name 不必要。对于html来说，经常需要获取某个tag的某个属性值。
        - attributes 不必要。支持多种结构。
            - string 直接作为`//div/a[{attributes}]`中的`{attributes}`
            - map `{"type": "password", "id": "id1"}` => `//div/a[@type=\"password\" and @id=\"id1\"]`
            - list `[["type","=","password"],["id", "!=","id1"]]` => `//div/a[@type=\"password\" and @id!=\"id1\"]`
        - json_path 不必要。`json_path`完全遵循[json_path协议](https://goessner.net/articles/JsonPath/)，[json_path在线调试](http://jsonpath.com/)。
        - json_key 不必要。直接作为json取值的键名。
        - value_type 不必要。**但很有用**，主要是因为不管是xpath还是json_path，提取出来都是一个list，尽管有时字段明明是完全确定的，如果设置value_type=`singleton`，则PE将提取list的第一个元素。
        - position 不必要。
        - mapper 不必要。dict类型，用于实现值映射，例如：`{1: "male", 2: "femail"}`。
        - join 不必要。str类型，用于实现`','.join([])`。
        
        ### TemplateAnnotation参数说明
        TemplateAnnotation注解中传进来的参数，除了下面列出的，其他的参数都会被塞到返回值中（当然，如果通过定义`itemname`，实例化item的时候会静默抛弃那些不属于item的值）。
        
        - start_url_tpl: 模板的数组，或者模板id的数组。
            >对应于start_urls的模板，会生成一个`_parse_start_url`方法绑定到spider类上，该方法有两个参数（不包括self）：
            - response 
            - tpl_index_or_id，默认是None
            
        - tpls: 模板的数组，或者模板id的数组
        
        具体请参考[decorator.py](./parser_engine/decorator.py)中的注释及源代码。
        
        #### Html格式
        举个简单的例子。
        
        目标：抓取[抖音用户关键字搜索抓包数据分析脚本使用指南](http://github.cannot.cc/baixing-helper/抖音用户关键字搜索抓包数据分析脚本使用指南.html)页面的几个步骤标题，每个步骤是一个`h3`标签，步骤标题在`id`属性里，并且需要去掉形如`1-`的前缀。
        那么相应的配置文件是：
        ```json
            {
              "name": "demo",
              "fields": [
                {
                  "dom_id": null,
                  "_css": null,
                  "xpath": null,
                  "tags": [
                    "h3"
                  ],
                  "classes": [],
                  "attributes": null,
                  "position": null,
                  "key": "步骤",
                  "value_type": null,
                  "regexp": "[\\d]{1,2}-(\\w+)",
                  "attr_name": "id"
                }
              ]
            }
        ```
        输出
        ```
        {'步骤': ['准备工作', '找到电脑的ip地址和端口', '确保手机与电脑建立连接', '抖音搜索关键词', '抓包数据导出', '提取用户信息', '推荐在线转换工具', 'python脚本导出']}
        ```
        
        如果只需要第二个步骤，将json配置中的`position`参数改为`2`，即可得到如下输出：
        ```
        {'步骤': ['找到电脑的ip地址和端口']}
        ```
        
        #### JSON格式
        ```json
            {
              "name": "json-api-demo",
              "fields": [
                {
                  "key": "poi_id",
                  "json_path": "$.pois[:1].id"
                },
                {
                  "key": "地名",
                  "json_path": "$.data.name",
                  "value_type": "singleton"
                },
                {
                  "key": "下级",
                  "json_path": "$.data.children[*].name"
                }
              ]
            }
        ```
        
        
        由于`json_path`解析总是返回一个list，对于一些确定的字段，比如通过调用API`http://172.31.1.4:30815/api/dict/area/0?childrenDepth=1`，想拿到该地区的name字段，则可以设置`value_type`为`singleton`，则PE会做一次转换。
        
Platform: UNKNOWN
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft
Classifier: Operating System :: POSIX
Classifier: Operating System :: Unix
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Description-Content-Type: text/markdown
