- 新增 MongoLogRepository 类用于记录任务执行日志 - 在 FlowRunner 中集成日志记录,跟踪流程执行步骤 - 记录登录与业务流程的开始、成功、错误与中止状态 - 支持记录 XML 来源与快照(超长内容自动截断)- 扩展 README 文档说明日志字段与使用方式 -优化分页抓取逻辑,返回页面与记录统计数据 - 统一存储接口,暴露 MongoDB 客户端与数据库属性 - 增加步骤计数与执行时间统计功能
279 lines
15 KiB
Markdown
279 lines
15 KiB
Markdown
# 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`,涵盖站点、配置、动作、抽取等全部节点。
|
||
- 修改为相对路径或网络路径后,可在常见 IDE(VS 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` 时注入。
|