Files
xspider/README.md
Flik a1a13aae65 feat(storage): 引入任务日志记录功能
- 新增 MongoLogRepository 类用于记录任务执行日志
- 在 FlowRunner 中集成日志记录,跟踪流程执行步骤
- 记录登录与业务流程的开始、成功、错误与中止状态
- 支持记录 XML 来源与快照(超长内容自动截断)- 扩展 README 文档说明日志字段与使用方式
-优化分页抓取逻辑,返回页面与记录统计数据
- 统一存储接口,暴露 MongoDB 客户端与数据库属性
- 增加步骤计数与执行时间统计功能
2025-10-20 22:38:13 +08:00

279 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# xspider 模板爬虫
基于 XML 模板驱动的浏览器自动化采集引擎,负责监听 Redis 队列获取任务模板,利用 DrissionPage 浏览器执行登录与业务流程,并将抽取结果写入 MongoDB。本项目同时集成变量服务、验证码识别及分页、下载等常见采集场景的基础能力。
## 功能亮点
- 使用标准化 XML 模板描述站点、流程与字段,无需改动代码即可上线新任务。
- Redis 队列拉取模板地址,支持多实例横向扩展。
- 内置 Redis 变量服务,自动处理站点作用域与全局作用域的变量读写。
- 提供可扩展的 Action 注册机制,可按需新增自定义动作。
- 采集结果默认落地 MongoDB可按数据类型、唯一键去重。
## 目录结构
```
├── main.py # 应用入口,初始化日志并启动循环
├── xspider/
│ ├── app.py # TemplateCrawlerApp协调整体流程
│ ├── runner.py # FlowRunner调度动作与抽取逻辑
│ ├── browser.py # 浏览器会话封装,对接 DrissionPage
│ ├── actions/ # 内置动作定义、注册
│ ├── extraction.py # HTML 解析与字段抽取
│ ├── storage.py # MongoDB 存储实现
│ ├── redis_queue.py # Redis BLPOP 队列封装
│ ├── variables.py # 变量服务访问与缓存
│ ├── xml_parser.py # XML 模板解析
│ └── utils/ # 辅助方法(选择器判断等)
```
## 环境要求
- Python 3.10+
- 浏览器驱动建议使用 [DrissionPage](https://github.com/g1879/DrissionPage) 配套环境
安装依赖示例:
```bash
pip install drissionpage redis requests pymongo lxml cssselect
```
如需验证码识别、自定义变量服务或下载逻辑,请在业务环境中提供对应服务。
## 环境变量
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `XSPIDER_REDIS_URL` | `redis://localhost:6379/0` | Redis 连接串 |
| `XSPIDER_REDIS_LIST_KEY` | `xspider:config` | 待消费的模板地址所在 `list` |
| `XSPIDER_REDIS_BLOCK_TIMEOUT` | `30` | Redis `BLPOP` 阻塞秒数 |
| `XSPIDER_MONGO_URI` | `mongodb://localhost:27017` | MongoDB 连接串 |
| `XSPIDER_MONGO_DB` | `xspider` | MongoDB 数据库名称 |
**变量作用域说明**
- 对变量名使用 `site:变量名` 将强制读取/写入当前站点作用域;`global:变量名` 将强制使用全局作用域。
- 无前缀的变量名会先从站点作用域(依赖 `site_id`)查找,未命中再自动回退到全局作用域。
- 写入变量时默认落到当前站点作用域(若缺少 `site_id` 则写入全局),也可通过 `var_scope="global"` 强制写入全局。
- `var_ttl``var_single_use` 会转换为 Redis 过期时间与一次性读取语义,所有数据均保存在 `XSPIDER_REDIS_URL` 指定的实例中。
## 启动方式
```bash
python main.py
```
程序将持续拉取 Redis 队列中的 XML 模板地址,下载模板后解析 site → flow → action并完成采集与存储。如需停止可使用 `Ctrl+C`
## XML 模板结构
### Schema 提示
若希望编辑器自动提示必填项与可选属性,可在 XML 顶部引入项目内置的 Schema
```xml
<site xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../schema/xspider.xsd"
id="example"
base="https://example.com">
...
</site>
```
- Schema 位于 `schema/xspider.xsd`,涵盖站点、配置、动作、抽取等全部节点。
- 修改为相对路径或网络路径后,可在常见 IDEVS Code、IntelliJ、XMLSpy 等)中获得自动补全、枚举提示与校验。
- Schema 对可扩展字段使用 `anyAttribute`,因此自定义参数不会报错。
### 顶层元素
| 元素 | 说明 |
| --- | --- |
| `<site id base?>` | 必填站点定义,可配置默认 `base` URL |
| `<config>` | 站点级配置:代理、重试、请求头等 |
| `<login>` | 登录流程,允许缺少 `extract`;执行完可配置校验选择器 |
| `<flows>` / `<flow>` | 业务流程,至少包含动作与抽取配置 |
### 模板示例
```xml
<site id="example" base="https://example.com">
<config enable_proxy="false" rotate_ua="false" retry="3">
<header name="Accept-Language" value="zh-CN,zh;q=0.9"/>
</config>
<login url="https://example.com/login" selector="div.dashboard" mode="css">
<action type="wait_dom_show" selector="form#login"/>
<action type="type" selector="//input[@name='username']" text="${account}" mode="xpath"/>
<action type="type" selector="//input[@name='password']" text="${password}" mode="xpath"/>
<action type="click" selector="button.submit"/>
</login>
<flows>
<flow id="orders" entry="/orders" data_type="sales" unique_keys="custom" columns="order_id">
<action type="wait_dom_show" selector="table.data-list"/>
<extract record_css="table.data-list tbody tr">
<field name="order_id" selector="td:nth-child(1)" mode="css"/>
<field name="customer" selector="td:nth-child(2)" mode="css"/>
</extract>
<paginate selector="button.next" mode="css" max_pages="10"/>
</flow>
</flows>
</site>
```
## Action 基础属性
所有 `<action>` 元素都会被解析为 `ActionConfig`,其通用属性如下:
| 属性 | 类型 / 默认值 | 说明 |
| --- | --- | --- |
| `type` | `str`(必填) | 决定实际执行的动作类 |
| `selector` | `str` / `None` | DOM 选择器;部分动作必须提供 |
| `mode` | `xpath`(默认)或 `css` | 选择器解析模式 |
| `timeout_ms` | `int` / `10_000` | 等待元素的超时,单位毫秒(仅在需要等待元素/动作时使用,部分动作会忽略该值) |
| `after_wait` | `int` / `0` | 执行完动作后的额外等待时间 |
| `params` | `dict` / `{}` | 其它自定义键值对,会透传给动作 |
`params` 支持 `${变量名}` 形式的变量占位符,运行时通过 `VariableResolver` 在站点上下文中解析。下文动作表格中出现的 `params.xxx` 表示 `<action>` 元素上的额外属性(除通用字段外),在解析时会放入 `params` 字典中。
**通用字段包括**`type``selector``mode``timeout_ms``after_wait`,这些字段会直接映射到 `ActionConfig` 对象,对应的属性不会出现在 `params` 中。
### `params` 字段举例
解析器会把 `<action>` 标签上的自定义属性收入 `params`,例如:
```xml
<!-- url 属性不是通用字段,因此会进入 params -->
<action type="goto" url="https://example.com/dashboard"/>
<!-- download_filename 也会进入 params执行时通过 self.config.params['download_filename'] 读取 -->
<action type="click"
selector="button.export"
download_filename="orders.xlsx"/>
<!-- 多个参数同样会被保留在 params 中 -->
<action type="set_var"
var_name="latest_token"
var_value="${token}"
var_scope="global"
var_ttl="86400"/>
```
上面的三个动作在 Python 中最终对应的 `ActionConfig.params` 分别为:
- `{"url": "https://example.com/dashboard"}`
- `{"download_filename": "orders.xlsx"}`
- `{"var_name": "latest_token", "var_value": "${token}", "var_scope": "global", "var_ttl": "86400"}`
动作类内部通过 `self.config.params[...]` 访问这些值,因此 README 中的 `params.url``params.download_filename``params.var_ttl` 等写法,正是指这些自定义属性在 `params` 字典里的访问方式。
## 内置 Action 类型
| 类型 | 必填字段 | 可选字段 | 说明 |
| --- | --- | --- | --- |
| `goto` | `params.url``selector` | 无 | 跳转到指定 URL为空时回退到 `entry_url`/`base_url`。 |
| `click` | `selector` | `params.button`(支持 `left/right/middle`)、`params.download_filename` | 点击元素,可触发下载。 |
| `type` | `selector``params.text` | 无 | 输入文本;执行前会清空原内容。 |
| `wait_dom_show` | `selector` | 无 | 等待元素出现并返回元素引用。 |
| `wait_dom_gone` | `selector` | 无 | 等待元素从 DOM 中移除。 |
| `wait_dom_hide` | `selector` | 无 | 等待元素隐藏style 含 `display: none`)。 |
| `wait_time` | 无 | `params.timeout_ms`(毫秒,若缺省使用 `timeout_ms` | 单纯 sleep 指定时间。 |
| `run_js` | `params.script``params.text` | 无 | 在当前页面执行 JavaScript并返回结果。 |
| `set_header` | `params.header_name``params.header_value` | 无 | 为当前会话追加请求头。 |
| `set_attr` | `selector``params.attr_name` | `params.attr_value`(默认空字符串) | 修改 DOM 属性值。 |
| `set_var` | `params.var_name``params.var_value` | `params.var_scope``params.var_ttl``params.var_single_use` | 将变量写入变量服务,可带过期策略。 |
| `captcha` | 无(若缺省会截取 `selector` 或整页截图) | `params.image``params.captcha_type``params.captcha_url``params.captcha_config``params.variable` | 调用远程接口识别验证码并写入变量。 |
> **提示**
> - 当 `selector` 与 `mode` 不匹配时(例如 CSS 模式中使用 XPath解析阶段会抛出异常。
> - 所有 `params` 字段均支持变量解析与自定义透传。
> - 若需要扩展新动作,继承 `BaseAction` 并设置 `type_name` 后自动被 `ActionRegistry` 注册。
### SetVarAction 使用示例
`SetVarAction` 用于把临时生成的数据写入 Redis 变量库,并同步到当前站点上下文,供后续步骤通过 `${变量名}``ctx.site_context[...]` 复用。其主要参数含义如下:
| 参数 | 说明 |
| --- | --- |
| `var_name` | 变量名称,支持 `${...}` 占位符,最终会作为变量服务的 `name` 字段。 |
| `var_value` | 变量值,可引用已存在的变量或静态字符串。 |
| `var_scope` | 变量作用域,可选,支持 `global`/`site`。未指定时默认写入当前站点作用域。 |
| `var_ttl` | 过期时间,单位秒,可选。变量服务可据此设置生命周期。 |
| `var_single_use` | 是否一次性变量,可选,常用于验证码、令牌等场景。 |
示例(写入站点作用域中的 `latest_token`,并设置 1 小时过期):
```xml
<action type="set_var"
var_name="latest_token"
var_value="${jwt_token}"
var_ttl="3600"
var_single_use="false"/>
```
动作执行步骤:
1. 解析所有参数并进行变量替换,例如 `${site_id}``${jwt_token}`
2. 根据 `var_scope` 与上下文自动选择站点或全局作用域,并附带 TTL/一次性读取标记写入 Redis。
3. `VariableResolver` 会将值缓存在内存中,后续解析 `${latest_token}``${site:latest_token}` 均可命中(前者会先查站点再回退全局)。
如需写入全局变量,可设置 `var_scope="global"` 或直接在变量名上使用 `global:token_name` 前缀。
### CaptchaAction 使用示例
`CaptchaAction` 会将验证码截图或指定的 base64 图片提交到远程接口,并把识别结果写入 Redis 变量库以及 `site_context`。使用方式如下:
```xml
<action type="captcha"
selector="//img[@id='captcha']"
mode="xpath"
captcha_type="text"
variable="login:captcha_code"
captcha_config='{"url": "https://captcha.example.com/api", "timeout": 60}'/>
```
执行流程:
- 若提供 `params.image`,会直接使用该 base64 字符串;否则优先根据 `selector` 截取元素截图,再退回到整页截图。
- 请求地址取 `params.captcha_url``captcha_config.url`,最后回退到默认 `https://captcha.lfei007s.workers.dev`
- 请求体默认为 `{"image": "...", "type": params.captcha_type}`,并支持通过 `captcha_config` 添加额外键值(如 `{"headers": {...}}`)。
- 服务返回 JSON 后,会尝试读取 `result``text``value``code``data` 等字段作为识别结果。
- 结果最终写入 Redis默认键为站点作用域下的 `captcha_result`,或使用 `params.variable` 覆盖),并同步到 `site_context` 供后续动作引用。
## 抽取与存储
- `<extract>` 支持 `record_css` / `record_xpath` 指定列表元素,`<field>` 定义字段名称、选择器、取值模式及可选的 `value_type`
- `<paginate>` 可配置下一页元素,并限制 `max_pages`
- `<excel_extract>` 用于声明需下载并解析 Excel 文件,当前实现会给出提示,具体逻辑可在 `FlowRunner._handle_excel_extract` 自行扩展。
- `MongoRepository.save_records` 会按照 `flow.unique_keys``unique_columns` 控制去重策略。
## 任务日志
每次执行登录流程或业务流程都会写入 MongoDB 的 `task_logs` 集合,记录执行状态与关键步骤,方便排查问题。
- 基础字段:
- `_id`UUID全局唯一的任务编号。
- `site_id` / `flow_id`:对应模板中的站点与流程。
- `status``start``success``error``aborted`
- `started_at` / `ended_at`UTC 时间戳,`duration_ms` 为执行耗时。
- `steps`:执行步骤列表,包含动作类型、分页点击、抽取结果等摘要。
- `metadata`:附加信息(如保存记录数量、分页页数、是否登录流程)。
- `xml_source`:原始 XML 的来源地址;`xml_snapshot`:当前执行所用 XML超出 50k 字符会截断并标记)。
- `error` / `abort_reason`:当流程异常或被中止时记录详细信息(包含堆栈)。
- 日志写入策略:
- 流程开始时插入 `status=start` 的文档;
- 正常结束时更新为 `success` 并补充 `steps``summary``metadata`
- 运行中出现异常时标记为 `error` 并写入堆栈;
- 收到 `KeyboardInterrupt` 或其它不可恢复的中断时标记为 `aborted`
可以根据 `site_id + flow_id + started_at` 建立索引用于快速查询,也可按 `status` 过滤待排查的任务。
## 开发与调试建议
- 日志级别默认 `INFO`,可自行设置环境变量 `PYTHONLOGGING` 或修改 `configure_logging`
- 建议在本地通过伪造 XML 模板直接调用 `FlowRunner.run_site`,避免频繁依赖 Redis。
- 若需要 Network/Proxy、自定义 Headers可在 `<config>` 中补充;所有配置都会在浏览器会话创建前应用。
- `CaptchaAction` 默认使用 `https://captcha.lfei007s.workers.dev`,如需替换可在 `params.captcha_config` 传入 JSON例如 `{"url": "...", "timeout": 60}`)。
## 扩展方向
- **自定义 Action**:在 `xspider/actions` 新增子类并继承 `BaseAction`,设置 `type_name``_execute` 逻辑即可使用。
- **变量服务实现**:可基于 `VariableService` 接口扩展自定义存储,例如不同的 Redis 集群或 REST 服务。
- **采集结果落地**:若不使用 MongoDB可实现新的存储类并在初始化 `FlowRunner` 时注入。