diff --git a/Skills/feapder-crawler/SKILL.md b/Skills/feapder-crawler/SKILL.md new file mode 100644 index 0000000..5322e7f --- /dev/null +++ b/Skills/feapder-crawler/SKILL.md @@ -0,0 +1,85 @@ +--- +name: feapder-crawler +description: 当用户要使用、生成、维护、审查或排查基于 feapder 框架的 Python 爬虫时使用;如果用户正在做 feapder 项目、要求用 feapder、安装/配置 feapder,或当前工作区代码/依赖已显示在使用 feapder,即使用户没再次说 feapder,只要在讨论分布式爬虫、断点续爬、批次采集、任务表消费、自动入库、Request/Response、Item/Pipeline、浏览器渲染、代理/去重、CLI 项目模板、运行时配置或 feapder.utils.tools 小工具,也要使用。若用户明确指定 Scrapy、requests-only、Playwright-only、FastAPI、Celery 等非 feapder 技术栈,则不要使用,除非任务是迁移到 feapder、对比 feapder,或用户明确要求改用这些技术。 +metadata: + short-description: Build and debug feapder crawlers +--- + +# Feapder Crawler + +这个 Skill 用于处理 feapder 爬虫相关工作:创建爬虫、选择爬虫类型、接入任务队列、解析响应、保存数据、配置运行环境,或诊断现有 feapder 项目。 + +## 第一轮判断 + +1. 先判断用户是在创建新爬虫、修改现有爬虫、排查运行问题、设计采集架构,还是配置入库/渲染/部署。 +2. 如果是现有项目,先读真实入口,不要只按印象判断: + - `setting.py` + - `main.py` + - `spiders/` + - `items/` + - 自定义 pipeline 模块 + - tests 或示例爬虫 +3. 如果当前目录不确定或不在项目根,先通过 `main.py`、`setting.py`、`spiders/`、`items/`、`requirements.txt` / `pyproject.toml` 中的 feapder 依赖定位项目根;确认项目根后再编辑配置或运行生成命令。 +4. 确认实际运行方式: + - 直接运行爬虫脚本 + - `main.py` + `ArgumentParser` + - TaskSpider 或 BatchSpider 的 master/worker 分离 + - feaplat 托管部署 +5. 配置优先级按这个顺序判断:爬虫类 `__custom_setting__` > 项目 `setting.py` > 环境变量 > `feapder/setting.py` 默认值。 +6. “feapder 项目上下文”不等于必须在 feapder 源码仓库里;用户可能在新文件夹、业务项目或空目录里创建 feapder 爬虫。只要用户要求用 feapder,或项目依赖/代码形态显示正在使用 feapder,就按 feapder 工作流处理。 + +## 爬虫类型选择 + +- 小型、单机、无需 Redis、无需分布式和断点续爬:用 `AirSpider`。 +- Redis 分布式、断点续爬、自动 item 缓冲、失败状态和大任务队列:用 `Spider`。 +- 种子任务来自 Redis/MySQL 或其他任务源,且需要区分任务下发与 worker 消费:用 `TaskSpider`。 +- 周期性批次采集,任务状态和批次记录必须落 MySQL:用 `BatchSpider`。 +- 多数据源共用一个 Spider 调度:用 `BaseParser` + `Spider.add_parser()`。 +- 多数据源共用一个 BatchSpider 批次调度:用 `BatchParser` + `BatchSpider.add_parser()`,任务行里要能标识 parser。 + +详细取舍和示例见 `references/spider-selection.md`。 + +## 按场景读取 + +- 新项目、CLI、项目结构和启动入口:读 `references/project-workflow.md`。 +- Request callback、中间件、校验、Response 提取:读 `references/request-response.md`。 +- Item、UpdateItem、数据库写入、CSV/MySQL/Mongo/custom pipeline:读 `references/item-pipeline.md`。 +- settings、Redis/MySQL/Mongo、重试、日志、报警、任务丢失、缓存:读 `references/settings-runtime.md`。 +- TaskSpider 和 BatchSpider 的 master/worker 流程:读 `references/batch-task-spider.md`。 +- 多 parser 集成:读 `references/parser-integration.md`。 +- feaplat 部署/平台运行定位:读 `references/feaplat-deploy.md`。 +- 浏览器渲染、Playwright/Selenium、代理、UA、去重:读 `references/rendering-proxy-dedup.md`。 +- feapder 自带小工具,如 cookies、URL、HTML、JSON、时间、hash、SQL、报警、CLI shell、文件工具:读 `references/utilities.md`。 +- 需要对源码做定位或验证时:读 `references/source-map.md`。 + +## 实施规则 + +- 优先使用 feapder 生成器和项目现有模式,不要手写一套不一致的脚手架。 +- 用户要新建标准 feapder 项目时,默认使用 `feapder create -p ` 生成项目结构,不要手写目录和模板;只有用户明确要求单文件脚本或自定义结构时才例外。 +- 在已有 feapder 标准项目里新增 spider 时,先定位项目根,再进入 `/spiders/` 执行 `feapder create -s `;不要在项目根直接执行 `-s`,也不要手写模板替代生成器,除非用户明确要求。 +- 已有项目新增或调整 Redis/MySQL/代理/线程/重试/渲染/pipeline 配置时,优先修改项目实际加载的 `setting.py`;只有配置确实只属于单个 spider,或项目已有模式使用 `__custom_setting__`,才放进 spider 类的 `__custom_setting__`。修改时增量合并用户已有配置,不要覆盖。 +- 用户明确要求 feapder 或当前项目使用 feapder 时,示例代码默认必须继承 feapder 的 Spider 类,并使用 `feapder.Request`、`feapder.Response`、`Item` / `UpdateItem`、`ITEM_PIPELINES` 等框架路径;AI 不允许自行退化成纯 `requests`、`Scrapy`、独立 Playwright、手写 CSV 或直接 SQL。若你判断确实需要脱离 feapder 路径,必须先向用户说明原因、影响和替代方案,并明确请求用户授权;用户确认前不要写非 feapder 实现。 +- 标准 feapder 项目里,数据模型默认放在 `items/_item.py`,优先用 `feapder create -i ` 生成 `Item` / `UpdateItem` 类;spider 里只导入 `XxxItem`、赋字段、`yield item`。不要默认在 spider 里写 `_build_xxx_item()` 并动态构造 `feapder.Item()` / `feapder.UpdateItem()`,除非是单文件 `AirSpider`、临时 demo,或用户明确要求快速脚本。 +- 用户询问“要不要脱离 feapder”“改成 requests 怎么样”这类方案判断时,只能先做风险/收益评估和替代方案说明;这不等于授权实现。只有用户明确确认“就改成非 feapder 实现”后,才可以写非 feapder 代码。 +- 不要假设 `AirSpider` 的配置行为等同于 Redis 分布式爬虫;先确认基类和启动参数。 +- 对 `BatchSpider` / `TaskSpider`,必须区分任务下发 `start_monitor_task()` 和 worker 采集 `start()`。 +- 多页面、多来源、多步骤解析时,优先把回调写成公开的 `parse_xxx` 方法,并在 `feapder.Request(..., callback=self.parse_xxx)` 显式绑定;不要把所有 URL 分支塞进默认 `parse()` 再转调 `_parse_xxx` 私有方法。默认 `parse()` 只适合未指定 callback 的入口或很小的单页爬虫。 +- 同一批种子可以直接构造多个并列来源 URL 时,在 `start_requests()` 中并列 `yield feapder.Request(..., callback=self.parse_source)`;不要先请求来源 A,再在 `parse_a()` 里创建来源 B 的请求。只有从当前 response 解析出来的派生 URL,才在对应 callback 内继续 `yield Request`。 +- 排查入库问题时,沿着 `yield Item` 或 `yield UpdateItem` 追到 `ITEM_PIPELINES`,再看具体 pipeline 和配置。 +- 排查解析问题时,先看 `Request.callback`、`parser_name`、`download_midware`、`validate`、`exception_request`、`failed_request`,不要急着改 scheduler。 +- 遇到 import/path 问题时,记住 feapder 从 `items/` 或 `spiders/` 启动时会把项目根目录插入 `sys.path`。 +- 做 PR 级别改动时,用最小相关测试或可运行示例验证,避免无关重构。 + +## 高信号源码 + +- `feapder/__init__.py`:公开 API 导出和导入路径行为。 +- `feapder/core/base_parser.py`:parser 生命周期钩子。 +- `feapder/core/spiders/`:AirSpider、Spider、TaskSpider、BatchSpider。 +- `feapder/network/request.py`:Request 参数、重试、渲染标记、缓存辅助。 +- `feapder/network/response.py`:xpath/css/re/json/bs4 提取和编码处理。 +- `feapder/network/item.py`:Item、UpdateItem、表名、指纹、单 item pipeline。 +- `feapder/pipelines/`:内置 pipeline 行为。 +- `feapder/utils/tools.py`:cookies、URL、JSON、日期、SQL、hash、文件、JS、报警和常见转换工具。 +- `feapder/commands/shell.py`:基于 `Request` 的 cURL/URL 响应调试器。 +- `feapder/setting.py`:默认运行时配置全集。 +- `feapder/commands/`:CLI 生成器。 diff --git a/Skills/feapder-crawler/agents/openai.yaml b/Skills/feapder-crawler/agents/openai.yaml new file mode 100644 index 0000000..4d7afc4 --- /dev/null +++ b/Skills/feapder-crawler/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Feapder Crawler" + short_description: "构建和排查 feapder 爬虫工作流" + default_prompt: "使用 $feapder-crawler 设计或排查一个 feapder 爬虫。" diff --git a/Skills/feapder-crawler/references/batch-task-spider.md b/Skills/feapder-crawler/references/batch-task-spider.md new file mode 100644 index 0000000..3a8e684 --- /dev/null +++ b/Skills/feapder-crawler/references/batch-task-spider.md @@ -0,0 +1,227 @@ +# TaskSpider 和 BatchSpider + +## TaskSpider + +`TaskSpider` 把种子任务下发和 worker 采集分开。 + +常见构造参数: + +```python +spider = TaskSpiderTest( + task_table="spider_task", + task_keys=["id", "url"], + redis_key="test:task_spider", + keep_alive=True, +) +``` + +Redis 任务源: + +```python +spider = TaskSpiderTest( + task_table="spider_task2", + task_table_type="redis", + redis_key="test:task_spider", + keep_alive=True, + use_mysql=False, +) +``` + +最小完整结构: + +```python +import feapder +from items.seed_result_item import SeedResultItem + + +class SeedTaskSpider(feapder.TaskSpider): + def add_task(self): + self._redisdb.zadd(self._task_table, {"id": 1, "url": "https://example.com"}) + + def start_requests(self, task): + task_id, url = task + yield feapder.Request(url, task_id=task_id) + + def parse(self, request, response): + item = SeedResultItem() + item.url = request.url + item.title = response.xpath("//title/text()").extract_first() + yield item + + +def create_mysql_task_spider(): + return SeedTaskSpider( + task_table="spider_task", + task_keys=["id", "url"], + redis_key="feapder:task_spider", + keep_alive=True, + ) + + +def create_redis_task_spider(): + return SeedTaskSpider( + task_table="spider_task_redis", + task_table_type="redis", + redis_key="feapder:task_spider", + keep_alive=True, + use_mysql=False, + ) +``` + +运行模式: + +```python +spider.start_monitor_task() # 下发和监控任务 +spider.start() # worker 采集 +``` + +`add_task()` 可在 `start_monitor_task()` 阶段塞种子任务。不要把它写成死循环。 + +`start_requests(self, task)` 接收一行任务。常见读取方式: + +```python +task_id, url = task +task_id = task.id +url = task["url"] +url = task.get("url") +``` + +## BatchSpider + +`BatchSpider` 用于周期性批次采集。它用 Redis 做请求调度,用 MySQL 维护任务和批次状态。 +不要把 BatchSpider 写成单入口普通脚本。它通常需要区分 master 下发/监控和 worker 采集,并在 request 上携带 `task_id`,解析完成后用 `update_task_batch()` 更新状态。 + +典型构造: + +```python +spider = ProductSpider( + redis_key="feapder:product", + task_table="product_task", + task_keys=["id", "url"], + task_state="state", + batch_record_table="product_batch_record", + batch_name="product daily crawl", + batch_interval=1, +) +``` + +最小完整结构: + +```python +import feapder +from items.product_price_item import ProductPriceItem + + +class ProductBatchSpider(feapder.BatchSpider): + def start_requests(self, task): + task_id, url = task + yield feapder.Request(url, task_id=task_id) + + def parse(self, request, response): + item = ProductPriceItem() + item.url = request.url + item.title = response.xpath("//title/text()").extract_first() + yield item + + yield self.update_task_batch(request.task_id, 1) + + def failed_request(self, request, response, e): + yield self.update_task_batch(request.task_id, -1) + + +def create_spider(): + return ProductBatchSpider( + redis_key="feapder:product_price", + task_table="product_task", + task_keys=["id", "url"], + task_state="state", + batch_record_table="product_batch_record", + batch_name="product price daily", + batch_interval=1, + ) + + +if __name__ == "__main__": + spider = create_spider() + # spider.start_monitor_task() # master: 下发和监控任务 + spider.start() # worker: 采集 +``` + +任务状态约定: + +- `0`:待抓取 +- `1`:已完成 +- `2`:抓取中或已下发 +- `-1`:无效或永久失败 + +每个批次默认会把非 `-1` 任务重置为 `0`。如果是增量采集,已完成任务不应重置,可以重写 `init_task()` 并置空。 + +## 任务完成状态 + +请求中携带 `task_id`: + +```python +def start_requests(self, task): + task_id, url = task + yield feapder.Request(url, task_id=task_id) +``` + +标记完成: + +```python +yield self.update_task_batch(request.task_id, 1) +``` + +超过最大重试后标记无效: + +```python +def failed_request(self, request, response, e): + yield self.update_task_batch(request.task_id, -1) +``` + +普通 `Spider` 失败处理通常不需要更新任务表,但可以记录失败、切换 cookie/proxy 或返回新请求: + +```python +def exception_request(self, request, response, e): + # 单次异常,可调整 request 后重试 + request.headers = {"User-Agent": "Mozilla/5.0"} + yield request + + +def failed_request(self, request, response, e): + # 超过最大重试次数后的最终失败 + self.logger.error(f"failed url={request.url}, error={e}") +``` + +不要在 `parse()` 里手写主重试循环;优先使用 `SPIDER_MAX_RETRY_TIMES`、`validate()`、`exception_request()` 和 `failed_request()`。 + +## Debug 模式 + +Spider: + +```python +debug_spider = SpiderTest.to_DebugSpider( + redis_key="feapder:spider", + request=feapder.Request("https://example.com"), +) +debug_spider.start() +``` + +BatchSpider: + +```python +debug_spider = BatchSpiderTest.to_DebugBatchSpider( + task_id=1, + redis_key="feapder:batch", + task_table="batch_task", + task_keys=["id", "url"], + task_state="state", + batch_record_table="batch_record", + batch_name="batch test", + batch_interval=1, +) +debug_spider.start() +``` + +Debug 模式默认通常不入库、不更新任务状态,除非显式配置。 +Debug 模式适合调单个 request、`parse`、`validate`、`download_midware` 或单个批次任务;它不是生产全链路验证,不能替代 master/worker、队列、pipeline 和部署环境检查。 diff --git a/Skills/feapder-crawler/references/feaplat-deploy.md b/Skills/feapder-crawler/references/feaplat-deploy.md new file mode 100644 index 0000000..449de0a --- /dev/null +++ b/Skills/feapder-crawler/references/feaplat-deploy.md @@ -0,0 +1,35 @@ +# feaplat 部署定位 + +feaplat 是 feapder 生态的爬虫管理系统。用户提到 feaplat、平台部署、平台调度、平台上不跑任务时,先做只读定位,不要直接改 spider 代码或本地启动方式。 + +## 只读定位顺序 + +1. 确认平台运行的是哪个项目包、哪个入口文件、哪个命令参数。 +2. 读取项目 `main.py`,确认 `ArgumentParser` 参数和实际 spider 构造。 +3. 读取目标 spider,确认继承类型:`AirSpider`、`Spider`、`TaskSpider`、`BatchSpider`。 +4. 读取 `setting.py` 和 spider `__custom_setting__`,按配置优先级判断实际 Redis/MySQL/pipeline 配置。 +5. 确认 master/worker 是否都运行: + - `TaskSpider` / `BatchSpider` 下发任务通常需要 `start_monitor_task()`。 + - worker 采集需要 `start()`。 +6. 检查 Redis 队列和失败队列 key 是否有数据: + - `{redis_key}:z_requests` + - `{redis_key}:z_failed_requests` + - `{redis_key}:s_failed_items` + - `{redis_key}:h_spider_status` +7. 检查 MySQL 任务表和批次记录表: + - `task_table` + - `task_state` + - `batch_record_table` +8. 检查日志中真实报错、配置值、入口参数和工作目录。 +9. 区分平台问题、配置问题、队列问题、数据库连接问题和 spider 解析代码问题。 + +## 常见判断 + +- 平台上“不跑任务”不等于 spider 代码错,可能是 master 没下发、worker 没启动、`redis_key` 不一致、任务表状态不对、配置没加载或包版本不是最新。 +- 平台部署后配置不生效时,优先确认平台注入的环境变量、项目 `setting.py`、spider `__custom_setting__`、工作目录和运行入口。 +- `BatchSpider` 本批次未结束时,下一批通常不会开始;先查批次记录和任务表状态。 +- 不要为了平台问题直接把 feapder 代码改成 `requests` 脚本;AI 判断必须脱离 feapder 时,也要先说明原因、影响、替代方案,并明确请求用户授权。 + +## 输出建议 + +先给用户一个定位清单和要读取/执行的只读命令。只有确认根因在代码或配置文件后,再做最小改动。 diff --git a/Skills/feapder-crawler/references/item-pipeline.md b/Skills/feapder-crawler/references/item-pipeline.md new file mode 100644 index 0000000..4395429 --- /dev/null +++ b/Skills/feapder-crawler/references/item-pipeline.md @@ -0,0 +1,231 @@ +# Item 和 Pipeline + +`Item` / `UpdateItem` 是 parser `yield` 给 feapder 的数据对象,后续由 `ItemBuffer` 交给 `ITEM_PIPELINES` 处理。 + +## 标准项目默认写法 + +在标准 feapder 项目里,优先把数据模型放到 `items/`,不要把表名、字段和更新逻辑散落在 spider 的 `_build_xxx_item()` 私有方法里。 + +如果 MySQL 表已存在,进入项目的 `items/` 目录生成 item: + +```bash +cd /items +feapder create -i app_info +feapder create -i app_relations +``` + +字段很多或接口 JSON 与表字段基本一致时,可用支持 dict 赋值的模板: + +```bash +feapder create -i app_info 1 +``` + +生成后按需要改成 `UpdateItem`,并把表名、更新字段、去重字段放在 item 类上: + +```python +from feapder import UpdateItem + + +class AppInfoItem(UpdateItem): + __table_name__ = "app_info" + __update_key__ = ["app_name", "app_type", "app_media", "app_icp", "app_company", "privacy_url", "source"] + __unique_key__ = ["pkg_name"] + + def __init__(self, *args, **kwargs): + self.pkg_name = None + self.app_name = None + self.app_type = None + self.app_media = None + self.app_icp = None + self.app_company = None + self.privacy_url = None + self.source = None + + def pre_to_db(self): + self.pkg_name = (self.pkg_name or "").strip() + self.app_name = (self.app_name or "").strip() +``` + +spider 中只负责解析和赋值: + +```python +from items.app_info_item import AppInfoItem +from items.app_relations_item import AppRelationsItem + + +def parse_yyb(self, request, response): + info = self.extract_app_info(response) + + item = AppInfoItem() + item.pkg_name = info.get("pkg_name") + item.app_name = info.get("app_name") + item.app_type = info.get("app_type") + item.app_media = info.get("app_media") + item.app_icp = info.get("app_icp") + item.app_company = info.get("app_company") + item.privacy_url = info.get("privacy_url") + item.source = "yyb" + yield item +``` + +关系表也应有自己的 item 类,不要在 spider 内动态拼: + +```python +from feapder import Item + + +class AppRelationsItem(Item): + __table_name__ = "app_relations" + __unique_key__ = ["source_pkg", "target_pkg", "source_store"] + + def __init__(self, *args, **kwargs): + self.source_pkg = None + self.target_pkg = None + self.source_store = None +``` + +使用: + +```python +rel = AppRelationsItem() +rel.source_pkg = source_pkg +rel.target_pkg = target_pkg +rel.source_store = "yyb" +yield rel +``` + +## UpdateItem 和唯一索引 + +更新已有数据时使用 `UpdateItem`。MySQL 更新语义依赖表上的主键或唯一索引;仅在 item 上写 `__update_key__`,但数据库没有唯一索引,不能实现按业务键更新。 + +推荐把更新字段写在类上: + +```python +from feapder import UpdateItem + + +class ProductItem(UpdateItem): + __table_name__ = "product" + __unique_key__ = ["product_id"] + __update_key__ = ["name", "price", "shop_id"] + + def __init__(self, *args, **kwargs): + self.product_id = None + self.name = None + self.price = None + self.shop_id = None +``` + +对应表需要类似: + +```sql +UNIQUE KEY uk_product_id (product_id) +``` + +已有 `Item` 也可以通过 `to_UpdateItem()` 转成更新对象,但标准项目里优先直接定义 `UpdateItem` 类。 + +## Item 去重指纹 + +Item 指纹默认由排序后的字段值计算 MD5。遇到采集时间、更新时间这类不应参与去重的字段时,要显式指定: + +```python +class ProductItem(feapder.Item): + __table_name__ = "product" + __unique_key__ = ["product_id"] +``` + +也可运行时指定: + +```python +item.unique_key = ["product_id"] +``` + +或者重写: + +```python +@property +def fingerprint(self): + return self.product_id +``` + +## Pipeline + +CSV/MySQL/Mongo 保存的主路径是 `yield Item` 或 `yield UpdateItem` 进入 `ITEM_PIPELINES`。AI 不允许把主要方案写成 parser 里直接 `open()` / `csv.writer` 写 CSV 或直接拼 SQL;这些只适合临时脚本或自定义 pipeline 内部实现。若判断确实要绕过 `ITEM_PIPELINES`,必须先向用户说明原因、影响和替代方案,并明确请求用户授权。 + +已有项目启用 CSV/MySQL/Mongo pipeline 时,先定位项目根和实际加载的 `setting.py`,在现有 `ITEM_PIPELINES` 中增量加入对应 pipeline,并补 `CSV_EXPORT_PATH` 等相关配置。不要只给孤立配置代码,也不要覆盖用户已有 pipeline。 + +```python +ITEM_PIPELINES = [ + "feapder.pipelines.mysql_pipeline.MysqlPipeline", + # "feapder.pipelines.mongo_pipeline.MongoPipeline", + # "feapder.pipelines.csv_pipeline.CsvPipeline", + # "feapder.pipelines.console_pipeline.ConsolePipeline", +] +CSV_EXPORT_PATH = "data/csv" +``` + +单个 item 可以指定自己的 pipeline,覆盖全局流向: + +```python +from feapder.pipelines.csv_pipeline import CsvPipeline + + +class SpiderDataItem(feapder.Item): + __pipelines__ = [CsvPipeline()] +``` + +## 自定义 Pipeline + +```python +from typing import Dict, List, Tuple +from feapder.pipelines import BasePipeline + + +class Pipeline(BasePipeline): + def save_items(self, table, items: List[Dict]) -> bool: + return True + + def update_items(self, table, items: List[Dict], update_keys=Tuple) -> bool: + return True +``` + +保存失败时返回 `False`,这样 feapder 会重试,并且不会把这批数据标记为已成功去重。 + +pipeline 在配置里写模块路径,例如: + +```python +ITEM_PIPELINES = ["pipeline.Pipeline"] +``` + +## 只在轻量场景允许动态 Item + +`feapder.Item()` / `feapder.UpdateItem()` 动态赋字段是官方允许的临时写法,但不要作为标准项目默认输出。 + +允许场景: + +- 单文件 `AirSpider`。 +- 临时验证字段或下载解析。 +- 用户明确要求快速 demo,不创建标准项目结构。 + +动态写法示例: + +```python +item = feapder.Item() +item.table_name = "spider_data" +item.title = title +yield item +``` + +反例,标准项目里不要默认这样写: + +```python +def _build_app_info_item(pkg, info, source=""): + item = feapder.UpdateItem() + item.table_name = "app_info" + item.update_key = "pkg_name" + item.pkg_name = pkg + item.app_name = info.get("app_name", "") + return item +``` + +更好的写法是把 `AppInfoItem` 放到 `items/app_info_item.py`,spider 里只实例化和赋值。 diff --git a/Skills/feapder-crawler/references/parser-integration.md b/Skills/feapder-crawler/references/parser-integration.md new file mode 100644 index 0000000..f79522e --- /dev/null +++ b/Skills/feapder-crawler/references/parser-integration.md @@ -0,0 +1,110 @@ +# Parser 集成 + +当多个站点或多个解析器共享一个调度器时,用 feapder 的 parser 集成。不要为每个站点都复制一套完整 Spider,除非它们的运行周期、队列、配置和部署都确实独立。 + +## Spider + BaseParser + +解析器继承 `feapder.BaseParser`,调度器使用 `Spider.add_parser()` 注册。 + +```python +import feapder +from items.news_item import NewsItem + + +class SinaNewsParser(feapder.BaseParser): + def start_requests(self): + yield feapder.Request("https://news.sina.com.cn/") + + def parse(self, request, response): + item = NewsItem() + item.source = "sina" + item.title = response.xpath("//title/text()").extract_first() + yield item + + +class TencentNewsParser(feapder.BaseParser): + def start_requests(self): + yield feapder.Request("https://news.qq.com/") + + def parse(self, request, response): + item = NewsItem() + item.source = "tencent" + item.title = response.xpath("//title/text()").extract_first() + yield item + + +if __name__ == "__main__": + spider = feapder.Spider(redis_key="feapder:news") + spider.add_parser(SinaNewsParser) + spider.add_parser(TencentNewsParser) + spider.start() +``` + +跨 parser 指定回调时,`Request` 可以带 `parser_name` 和 callback 名称: + +```python +yield feapder.Request( + detail_url, + parser_name="TencentNewsParser", + callback="parse_detail", +) +``` + +## BatchSpider + BatchParser + +批次集成时,解析器继承 `feapder.BatchParser`。任务表需要有一个字段标识这条任务应该分发到哪个 parser,常见字段名是 `parser_name`。 + +```python +import feapder +from items.news_item import NewsItem + + +class SinaBatchParser(feapder.BatchParser): + def start_requests(self, task): + task_id, url, parser_name = task + yield feapder.Request(url, task_id=task_id) + + def parse(self, request, response): + item = NewsItem() + item.source = "sina" + item.title = response.xpath("//title/text()").extract_first() + yield item + yield self.update_task_batch(request.task_id, 1) + + +class TencentBatchParser(feapder.BatchParser): + def start_requests(self, task): + task_id, url, parser_name = task + yield feapder.Request(url, task_id=task_id) + + def parse(self, request, response): + item = NewsItem() + item.source = "tencent" + item.title = response.xpath("//title/text()").extract_first() + yield item + yield self.update_task_batch(request.task_id, 1) + + +def create_spider(): + spider = feapder.BatchSpider( + task_table="news_task", + task_keys=["id", "url", "parser_name"], + task_state="state", + batch_record_table="news_batch_record", + batch_name="news batch", + batch_interval=1, + redis_key="feapder:news_batch", + ) + spider.add_parser(SinaBatchParser) + spider.add_parser(TencentBatchParser) + return spider +``` + +## 排查要点 + +- 确认 parser 类继承的是 `BaseParser` 或 `BatchParser`,不是完整 `Spider` / `BatchSpider`。 +- 确认 `add_parser()` 注册了所有 parser 类。 +- Batch 集成必须确认任务表字段能标识 parser。 +- 跨 parser 回调时检查 `parser_name` 和 callback 名称是否匹配。 +- 标准项目中 parser 产出的数据也应使用 `items/` 下的生成式 Item 类,不要默认在 parser 内动态 `feapder.Item()`。 +- `AirSpider` 不支持这种 parser 集成方式。 diff --git a/Skills/feapder-crawler/references/project-workflow.md b/Skills/feapder-crawler/references/project-workflow.md new file mode 100644 index 0000000..91e1ad9 --- /dev/null +++ b/Skills/feapder-crawler/references/project-workflow.md @@ -0,0 +1,110 @@ +# 项目工作流 + +## CLI + +feapder 内置 `feapder` 命令行工具。 + +用户要新建标准 feapder 项目时,默认使用生成器,不要手写目录和模板: + +```bash +feapder create -p +``` + +在已有标准项目里新增 spider 时,先定位项目根,再进入项目的 `spiders/` 目录运行 `-s`: + +```bash +cd /spiders +feapder create -s +``` + +不要在项目根直接执行 `feapder create -s `,否则生成位置可能不符合标准项目结构。也不要手写 spider 模板替代生成器,除非用户明确要求。 + +如果用户只要一个单文件、本地运行的轻量 `AirSpider`,可以不创建完整 `-p` 项目;可直接写单文件 `AirSpider`,或在目标目录执行 `feapder create -s ` 生成单 spider 模板,并说明它不包含标准项目的 `setting.py/main.py/items/spiders` 结构。 + +常用命令: + +```bash +feapder create -p my-project +feapder create -s my_spider +feapder create -i table_name +feapder create -t table_name +feapder create --setting +feapder create -j +feapder create -sj +feapder shell +feapder zip +``` + +`feapder create -p` 生成的典型项目结构: + +```text +my-project/ +├── items/ +├── spiders/ +├── main.py +└── setting.py +``` + +## 启动入口 + +简单爬虫可以直接运行 spider 文件。复杂项目通常把启动入口收敛到 `main.py`,并用 `feapder.ArgumentParser` 管理命令行参数。 + +典型 `main.py`: + +```python +from feapder import ArgumentParser +from spiders import * + + +def crawl_news(): + spider = news_spider.NewsSpider(redis_key="feapder:news") + spider.start() + + +if __name__ == "__main__": + parser = ArgumentParser(description="crawler") + parser.add_argument("--crawl_news", action="store_true", help="crawl news", function=crawl_news) + parser.start() +``` + +`BatchSpider` 常见做法是暴露数字模式: + +```python +def crawl_batch(args): + spider = product_spider.ProductSpider(...) + if args == 1: + spider.start_monitor_task() + elif args == 2: + spider.start() + elif args == 3: + spider.init_task() +``` + +## 生成 Item + +`feapder create -i table_name` 会读取 MySQL 表结构并生成 `Item` 类。数据库配置可以来自 `setting.py`、环境变量或命令行参数: + +```bash +feapder create -i spider_data --host localhost --db feapder --username feapder --password feapder123 +``` + +字段很多或接口返回 JSON 较完整时,可以选择支持 dict 赋值的 Item 模板。 + +## 维护现有项目时的阅读顺序 + +1. 先读 `main.py`,确认真实命令和 spider 构造参数。 +2. 再读 `setting.py`,确认 Redis、MySQL、pipeline、线程、重试、渲染、代理、去重和日志配置。 +3. 读目标 `spiders/` 文件。 +4. 读 `items/` 文件。 +5. 读自定义 pipeline 模块。 +6. 搜索同类型测试或示例。 + +如果当前工作目录不确定或不在项目根,先通过这些信号定位项目根: + +- `main.py` +- `setting.py` +- `spiders/` +- `items/` +- `requirements.txt` / `pyproject.toml` 中的 `feapder` 依赖 + +确认项目根后,再编辑配置或进入 `spiders/` 执行生成命令。 diff --git a/Skills/feapder-crawler/references/rendering-proxy-dedup.md b/Skills/feapder-crawler/references/rendering-proxy-dedup.md new file mode 100644 index 0000000..7a18103 --- /dev/null +++ b/Skills/feapder-crawler/references/rendering-proxy-dedup.md @@ -0,0 +1,120 @@ +# 渲染、代理和去重 + +## 浏览器渲染 + +动态页面只有在直接找接口不划算或不可行时,再使用浏览器渲染。 +在 feapder 任务里,浏览器渲染的主路径是 `feapder.Request(..., render=True)` 加 `RENDER_DOWNLOADER` 配置。AI 不允许直接改写成独立 Playwright/Selenium 脚本;如果判断确实需要脱离 feapder 渲染路径,必须先向用户说明原因、影响和替代方案,并明确请求用户授权。 + +```python +yield feapder.Request("https://example.com", render=True, render_time=2) +``` + +默认渲染 downloader 通常是 Selenium,除非配置改成 Playwright: + +```python +RENDER_DOWNLOADER = "feapder.network.downloader.SeleniumDownloader" +# RENDER_DOWNLOADER = "feapder.network.downloader.PlaywrightDownloader" +``` + +Selenium 配置: + +```python +WEBDRIVER = { + "pool_size": 1, + "load_images": True, + "user_agent": None, + "proxy": None, + "headless": False, + "driver_type": "CHROME", + "timeout": 30, + "window_size": (1024, 800), + "executable_path": None, + "render_time": 0, + "custom_argument": ["--ignore-certificate-errors", "--disable-blink-features=AutomationControlled"], + "xhr_url_regexes": None, + "auto_install_driver": True, + "download_path": None, + "use_stealth_js": False, +} +``` + +Playwright 配置: + +```python +PLAYWRIGHT = { + "user_agent": None, + "proxy": None, + "headless": False, + "driver_type": "chromium", + "timeout": 30, + "window_size": (1024, 800), + "render_time": 0, + "wait_until": "networkidle", + "url_regexes": None, + "save_all": False, +} +``` + +如果渲染后页面内容发生变化,可以先更新 `response.text` 再提取。 + +## 代理和 User-Agent + +常见配置: + +```python +PROXY_EXTRACT_API = None +PROXY_ENABLE = True +PROXY_MAX_FAILED_TIMES = 5 +PROXY_POOL = "feapder.network.proxy_pool.ProxyPool" + +RANDOM_HEADERS = True +USER_AGENT_TYPE = "chrome" +DEFAULT_USERAGENT = "Mozilla/5.0 ..." +USE_SESSION = False +``` + +请求级参数优先于配置: + +```python +yield feapder.Request( + url, + headers={"User-Agent": "..."}, + proxies={"http": "http://host:port", "https": "http://host:port"}, +) +``` + +需要按请求动态设置 cookie/header/proxy 时,优先用 `download_midware`。 + +## 去重 + +Request 去重: + +```python +REQUEST_FILTER_ENABLE = True +REQUEST_FILTER_SETTING = { + "filter_type": 3, + "expire_time": 2592000, +} +``` + +Item 去重: + +```python +ITEM_FILTER_ENABLE = True +ITEM_FILTER_SETTING = { + "filter_type": 1, +} +``` + +filter 类型: + +- `1`:永久 BloomFilter +- `2`:内存去重 +- `3`:临时过期去重 +- `4`:轻量去重 + +单个请求跳过去重: + +```python +yield feapder.Request(url, filter_repeat=False) +``` diff --git a/Skills/feapder-crawler/references/request-response.md b/Skills/feapder-crawler/references/request-response.md new file mode 100644 index 0000000..f6b4721 --- /dev/null +++ b/Skills/feapder-crawler/references/request-response.md @@ -0,0 +1,166 @@ +# Request 和 Response + +## Request + +`feapder.Request` 封装了 `requests` 参数,并额外增加 feapder 框架参数。 + +常见框架参数: + +- `url`:目标 URL。 +- `callback`:parser 回调函数或函数名。 +- `parser_name`:跨 parser 回调时使用的 parser 类名。 +- `priority`:优先级,数值越小越优先,默认通常是 `300`。 +- `filter_repeat`:当 `REQUEST_FILTER_ENABLE` 开启时,控制当前请求是否去重。 +- `auto_request`:设为 `False` 时,parse 收到的 `response=None`,需要自己下载。 +- `request_sync`:让 yield 出去的请求立即同步处理,而不是进入异步队列。 +- `use_session`:使用 session downloader。 +- `random_user_agent`:配合随机 headers 配置使用。 +- `download_midware`:当前请求专用下载中间件。 +- `is_abandoned`:异常时是否放弃重试。 +- `render`:是否使用浏览器渲染。 +- `render_time`:渲染后等待多久再取 HTML。 +- 其他 `**kwargs`:可通过 `request.` 读取。 + +带 callback 和参数透传的示例: + +```python +def start_requests(self): + yield feapder.Request( + "https://example.com/list", + callback=self.parse_list, + category="books", + ) + + +def parse_list(self, request, response): + category = request.category + for href in response.css("a.detail::attr(href)").extract(): + yield feapder.Request(href, callback=self.parse_detail, category=category) +``` + +多来源或多步骤解析时,不要把所有页面都扔进默认 `parse()` 再用 URL 判断分发到 `_parse_xxx` 私有方法。优先让每条链路在 `Request` 上显式绑定 callback。 + +如果同一批种子能直接构造多个并列来源 URL,要在 `start_requests()` 中并列下发。不要先请求来源 A,再在 `parse_a()` 里创建来源 B 的请求;只有从当前 response 解析出来的派生 URL,才在对应 callback 内继续 yield。 + +```python +YYB_URL = "https://sj.qq.com/appdetail/{}" +WDJ_URL = "https://www.wandoujia.com/apps/{}" +MI_URL = "https://app.mi.com/details?id={}" + + +def start_requests(self): + for pkg in self.seed_packages: + yield feapder.Request(YYB_URL.format(pkg), callback=self.parse_yyb, pkg=pkg) + yield feapder.Request(WDJ_URL.format(pkg), callback=self.parse_wandoujia, pkg=pkg) + yield feapder.Request(MI_URL.format(pkg), callback=self.parse_mi, pkg=pkg) + + +def parse_yyb(self, request, response): + pkg = request.pkg + for related_pkg in self.extract_yyb_related(response): + # 这是从应用宝响应里解析出的派生链路,所以留在 parse_yyb 中递归。 + yield feapder.Request( + YYB_URL.format(related_pkg), + callback=self.parse_yyb, + pkg=related_pkg, + ) + + +def parse_wandoujia(self, request, response): + pass + + +def parse_mi(self, request, response): + pass +``` + +反例: + +```python +def parse(self, request, response): + if "sj.qq.com" in response.url: + yield from self._parse_yyb(request, response) + elif "wandoujia.com" in response.url: + yield from self._parse_wandoujia(request, response) +``` + +这个反例能跑,但会隐藏 feapder 的 callback 链路,不利于调试 `request.callback_name`、失败重试、跨 parser 回调和后续维护。只有很小的单页爬虫才适合只写默认 `parse()`。 + +另一个反例是把并列来源串成伪流程: + +```python +def parse_yyb(self, request, response): + # 错:豌豆荚 URL 能由同一个 pkg 直接构造,不应该藏在应用宝 callback 里。 + yield feapder.Request(WDJ_URL.format(request.pkg), callback=self.parse_wandoujia) +``` + +如果豌豆荚和应用宝都是同一批 pkg 的独立补充来源,应在 `start_requests()` 中并列创建;`parse_yyb()` 只处理应用宝响应和应用宝响应派生出的推荐链。 + +## Parser 钩子 + +`BaseParser` 和各类 spider 都支持这些常见钩子: + +```python +def start_requests(self): + pass + +def download_midware(self, request): + return request + +def validate(self, request, response): + pass + +def parse(self, request, response): + pass + +def exception_request(self, request, response, e): + pass + +def failed_request(self, request, response, e): + pass + +def start_callback(self): + pass + +def end_callback(self): + pass +``` + +`validate` 语义: + +- 抛异常:触发重试。 +- 返回 `False`:丢弃当前请求。 +- 返回 `True` 或 `None`:继续进入 parser callback。 + +## Response + +`feapder.Response` 封装了 `requests.Response`。 + +常用提取方式: + +```python +response.xpath("//title/text()").extract_first() +response.xpath("//a/@href").extract() +response.css("a::attr(href)").extract() +response.re(r"(.*?)") +response.re_first(r"id=(\d+)", default=None) +response.bs4().title +response.json +response.text +response.extract() +response.open() +``` + +和 `requests.Response` 的重要差异: + +- JSON 用 `response.json`,不是 `response.json()`。 +- 编码可用 `response.encoding`,也支持简写 `response.code`。 +- HTML 相对链接会按配置自动转为绝对链接。 + +常用构造方式: + +```python +response = feapder.Response(raw_requests_response) +response = feapder.Response.from_text(text=html, url="https://example.com") +response = feapder.Response.from_dict(response_dict) +``` diff --git a/Skills/feapder-crawler/references/settings-runtime.md b/Skills/feapder-crawler/references/settings-runtime.md new file mode 100644 index 0000000..958cb86 --- /dev/null +++ b/Skills/feapder-crawler/references/settings-runtime.md @@ -0,0 +1,126 @@ +# 配置和运行时 + +## 配置来源 + +优先级: + +1. Spider 类里的 `__custom_setting__` +2. 项目 `setting.py` +3. 支持的环境变量 +4. `feapder/setting.py` 默认值 + +示例: + +```python +class DemoSpider(feapder.Spider): + __custom_setting__ = { + "REDISDB_IP_PORTS": "localhost:6379", + "SPIDER_MAX_RETRY_TIMES": 20, + } +``` + +## 已有项目改配置 + +已有项目新增或调整 Redis/MySQL/Mongo、代理、线程数、重试、渲染、pipeline 等配置时,先定位项目根和实际加载的 `setting.py`。 + +默认优先改项目 `setting.py`,因为它是项目级配置入口。只有这些情况才优先放到 spider 类的 `__custom_setting__`: + +- 配置确实只属于某一个 spider。 +- 项目已有模式就是在目标 spider 里维护 `__custom_setting__`。 +- 用户明确要求某个 spider 覆盖全局配置。 + +修改前检查: + +- `main.py` 是否从项目根启动。 +- 当前工作目录是否会影响 `setting.py` 加载。 +- 目标 spider 是否已有 `__custom_setting__`。 +- 环境变量是否覆盖同名配置。 +- 当前 spider 类型是否真的使用该配置。 + +修改时增量合并现有配置,不要覆盖用户已有 `ITEM_PIPELINES`、Redis/MySQL 参数或渲染配置。 + +## 关键配置 + +数据库: + +```python +MYSQL_IP = "localhost" +MYSQL_PORT = 3306 +MYSQL_DB = "feapder" +MYSQL_USER_NAME = "feapder" +MYSQL_USER_PASS = "secret" + +REDISDB_IP_PORTS = "localhost:6379" +REDISDB_USER_PASS = "" +REDISDB_DB = 0 +REDISDB_SERVICE_NAME = "" +REDISDB_KWARGS = {} + +MONGO_IP = "localhost" +MONGO_PORT = 27017 +MONGO_DB = "feapder" +MONGO_URL = None +``` + +爬虫运行: + +```python +COLLECTOR_TASK_COUNT = 32 +SPIDER_THREAD_COUNT = 1 +SPIDER_SLEEP_TIME = 0 +SPIDER_MAX_RETRY_TIMES = 10 +SPIDER_AUTO_START_REQUESTS = True +KEEP_ALIVE = False +REQUEST_LOST_TIMEOUT = 600 +REQUEST_TIMEOUT = 22 +``` + +Item 入库: + +```python +ITEM_MAX_CACHED_COUNT = 5000 +ITEM_UPLOAD_BATCH_MAX_SIZE = 1000 +ITEM_UPLOAD_INTERVAL = 1 +EXPORT_DATA_MAX_FAILED_TIMES = 10 +EXPORT_DATA_MAX_RETRY_TIMES = 10 +``` + +缓存和失败重试: + +```python +RETRY_FAILED_REQUESTS = False +RETRY_FAILED_ITEMS = False +SAVE_FAILED_REQUEST = True +RESPONSE_CACHED_ENABLE = False +RESPONSE_CACHED_EXPIRE_TIME = 3600 +RESPONSE_CACHED_USED = False +DELETE_KEYS = [] +``` + +日志: + +```python +LOG_LEVEL = "DEBUG" +LOG_PATH = "log/%s.log" % LOG_NAME +LOG_IS_WRITE_TO_CONSOLE = True +LOG_IS_WRITE_TO_FILE = False +``` + +## 配置不生效排查 + +1. 先检查 spider 类里是否有 `__custom_setting__` 覆盖。 +2. 确认进程工作目录和实际加载的 `setting.py`。 +3. 检查环境变量是否也设置了同名值。 +4. 确认当前 spider 类型是否支持该配置。例如 `AirSpider` 不像 `Spider` 那样使用 Redis 请求队列。 +5. 确认入口在正确项目路径下 import `feapder`。 + +## Redis Key + +多数队列 key 都来自 `redis_key`: + +- 请求队列:`{redis_key}:z_requests` +- 失败请求:`{redis_key}:z_failed_requests` +- 失败 item:`{redis_key}:s_failed_items` +- 爬虫状态:`{redis_key}:h_spider_status` + +排查遗留任务、失败请求、重复消费时优先看这些 key。 diff --git a/Skills/feapder-crawler/references/source-map.md b/Skills/feapder-crawler/references/source-map.md new file mode 100644 index 0000000..6c83136 --- /dev/null +++ b/Skills/feapder-crawler/references/source-map.md @@ -0,0 +1,69 @@ +# 源码地图 + +当文档不足或需要核对真实行为时,按这个地图查源码。 + +## 公开 API + +- `feapder/__init__.py` + - 导出 `AirSpider`、`Spider`、`TaskSpider`、`BatchSpider`、`BaseParser`、`TaskParser`、`BatchParser`、`Request`、`Response`、`Item`、`UpdateItem`、`ArgumentParser`。 + - 当当前工作目录以 `items` 或 `spiders` 结尾时,会把项目根目录插入 `sys.path`。 + +## Parser 生命周期 + +- `feapder/core/base_parser.py` + - `BaseParser`:`start_requests`、`download_midware`、`validate`、`parse`、`exception_request`、`failed_request`、`start_callback`、`end_callback`。 + - `TaskParser`:任务状态辅助方法,包括批量更新任务状态。 + - `BatchParser`:批次时间和批次 parser 行为。 + +## Spider 运行时 + +- `feapder/core/spiders/air_spider.py`:轻量本地调度。 +- `feapder/core/spiders/spider.py`:Redis 分布式爬虫。 +- `feapder/core/spiders/task_spider.py`:任务源型爬虫。 +- `feapder/core/spiders/batch_spider.py`:批次调度和任务状态流转。 +- `feapder/core/scheduler.py`:通用调度行为、parser 注册、生命周期、状态、心跳、失败状态。 +- `feapder/core/collector.py`:从队列抓取 request 到内存。 +- `feapder/core/parser_control.py`:downloader 和 parser callback 调度。 +- `feapder/buffer/request_buffer.py`:批量写入 request 队列。 +- `feapder/buffer/item_buffer.py`:批量处理 item、pipeline 和任务更新。 + +## 网络模型 + +- `feapder/network/request.py`:request 对象、框架参数、指纹/缓存辅助、downloader 调度。 +- `feapder/network/response.py`:response 封装、编码、绝对链接、xpath/css/re/json/bs4。 +- `feapder/network/selector.py`:selector 封装。 +- `feapder/network/downloader/`:requests、session、Selenium、Playwright downloader。 +- `feapder/network/proxy_pool/`:当前代理池。 +- `feapder/network/user_pool/`:guest/normal/gold 用户池。 + +## 数据模型 + +- `feapder/network/item.py`:`Item`、`UpdateItem`、表名、`to_dict`、SQL 转换、unique keys、单 item pipelines。 +- `feapder/pipelines/__init__.py`:`BasePipeline` 接口。 +- `feapder/pipelines/mysql_pipeline.py`:MySQL save/update。 +- `feapder/pipelines/mongo_pipeline.py`:Mongo save/update。 +- `feapder/pipelines/csv_pipeline.py`:CSV save/update、字段缓存、路径处理。 +- `feapder/pipelines/console_pipeline.py`:控制台输出 pipeline。 + +## 配置 + +- `feapder/setting.py`:Redis、MySQL、Mongo、pipeline、并发、重试、浏览器渲染、缓存、代理、去重、报警、日志默认值。 +- `feapder/templates/project_template/setting.py`:项目生成器使用的配置模板。 + +## CLI + +- `feapder/commands/cmdline.py`:顶层 CLI。 +- `feapder/commands/create/`:project、spider、item、table、settings、cookies、params、JSON 生成器。 +- `feapder/commands/shell.py`:响应调试。 +- `feapder/commands/retry.py`:失败 request/item 重试辅助。 +- `feapder/commands/zip.py`:项目打包。 + +## 测试和示例 + +- `tests/spider/`:标准 Spider 示例。 +- `tests/air-spider/`:AirSpider 示例。 +- `tests/batch-spider/`:BatchSpider 示例。 +- `tests/spider-integration/`:Spider parser 集成。 +- `tests/batch-spider-integration/`:BatchSpider parser 集成。 +- `tests/test-pipeline/`:自定义和 CSV pipeline 示例。 +- `tests/test_csv_pipeline/`:CSV pipeline 行为和性能测试。 diff --git a/Skills/feapder-crawler/references/spider-selection.md b/Skills/feapder-crawler/references/spider-selection.md new file mode 100644 index 0000000..97dff22 --- /dev/null +++ b/Skills/feapder-crawler/references/spider-selection.md @@ -0,0 +1,114 @@ +# 爬虫类型选择 + +feapder 的几类爬虫共用一套解析模型:`start_requests`、`parse`、`Request`、`Response`、`Item` 和 parser 钩子整体一致。真正的差异在运行状态、任务来源和是否需要批次管理。 + +## 选择 AirSpider + +适合这些情况: + +- 爬虫是小脚本或本地工具。 +- 不需要 Redis 任务队列。 +- 不需要分布式、断点续爬或强状态任务恢复。 +- 用户要最快上手。 + +如果用户只要一个单文件、本地运行的轻量 `AirSpider`,可以不创建完整 `feapder create -p` 项目;直接写单文件,或在目标目录执行 `feapder create -s ` 生成单 spider 模板。要说明这种方式不包含标准项目的 `setting.py/main.py/items/spiders` 结构。 + +典型结构: + +```python +import feapder + + +class DemoSpider(feapder.AirSpider): + def start_requests(self): + yield feapder.Request("https://example.com") + + def parse(self, request, response): + print(response.xpath("//title/text()").extract_first()) + + +if __name__ == "__main__": + DemoSpider(thread_count=5).start() +``` + +## 选择 Spider + +适合这些情况: + +- 有 Redis,任务需要在重启后继续。 +- 多进程或多机器消费同一个队列。 +- item 需要经过 `ItemBuffer` 自动批量入库。 +- 失败请求、重试状态、任务防丢比较重要。 + +典型结构(标准项目里优先使用 `items/` 下的生成式 Item 类): + +```python +from items.news_item import NewsItem + + +class NewsSpider(feapder.Spider): + __custom_setting__ = { + "REDISDB_IP_PORTS": "localhost:6379", + "REDISDB_DB": 0, + } + + def start_requests(self): + yield feapder.Request("https://news.example.com") + + def parse(self, request, response): + yield feapder.Request("https://news.example.com/detail", callback=self.parse_detail) + + def parse_detail(self, request, response): + item = NewsItem() + item.title = response.xpath("//h1/text()").extract_first() + yield item + + +NewsSpider(redis_key="feapder:news").start() +``` + +## 选择 TaskSpider + +当种子任务由任务源管理,而不是只靠代码里的 `start_requests` 生产时,用 `TaskSpider`。 + +- `start_monitor_task()` 负责下发和监控种子任务。 +- `start()` 负责 worker 消费请求。 +- `add_task()` 可在 master 阶段塞种子任务。 +- 任务行可来自 MySQL 或 Redis。 + +适合长驻任务消费、外部种子源、任务下发与 worker 分离的系统。 + +## 选择 BatchSpider + +当每次采集都属于某个周期批次,且任务完成状态必须记录在 MySQL 时,用 `BatchSpider`。 + +核心概念: + +- `task_table`:MySQL 任务种子表。 +- `task_keys`:从任务表读取并传给 `start_requests(self, task)` 的字段。 +- `task_state`:任务状态字段,常见约定是 `0` 待抓取、`1` 完成、`2` 抓取中、`-1` 无效或失败。 +- `batch_record_table`:批次记录表,由框架创建或维护。 +- `batch_interval`:批次周期,单位是天;小时级可写成 `1 / 24`。 + +运行方式: + +- master:`spider.start_monitor_task()` +- worker:`spider.start()` +- 可选重置/初始化:`spider.init_task()` + +## Parser 集成 + +当一个调度器需要管理多个数据源时,使用 parser 集成。 + +- `Spider` 集成时,解析器继承 `feapder.BaseParser`,再通过 `spider.add_parser(ParserClass)` 注册。 +- `BatchSpider` 集成时,解析器继承 `feapder.BatchParser`,任务行中要包含 parser 类名/名称字段,再通过 `add_parser` 注册。 +- `AirSpider` 不支持这种集成方式。 + +## 反例 + +- 需要 Redis 分布式、断点续爬或多机器消费时,不要选 `AirSpider`。 +- 需要周期批次、MySQL 任务状态和批次记录时,不要用普通 `Spider` 代替 `BatchSpider`。 +- 用户明确说 feapder 时,不要把“小爬虫”自动写成 `requests + BeautifulSoup`;如果 AI 判断必须这么做,先说明原因、影响、替代方案,并明确请求用户授权。 +- 用户只是询问“脱离 feapder 是否更好”时,只做方案评估,不要直接写非 feapder 代码;等用户明确确认后再实现。 +- 页面需要 JS 渲染且仍在 feapder 方案内时,不要直接跳到独立 Playwright;先用 `Request(render=True)` 和渲染配置。AI 要脱离 feapder 时必须先请求用户授权。 +- 要 CSV/MySQL/Mongo 入库时,不要绕过 `Item` / `ITEM_PIPELINES` 写手动保存逻辑。AI 要绕过时必须先请求用户授权。 diff --git a/Skills/feapder-crawler/references/utilities.md b/Skills/feapder-crawler/references/utilities.md new file mode 100644 index 0000000..5f8ea65 --- /dev/null +++ b/Skills/feapder-crawler/references/utilities.md @@ -0,0 +1,267 @@ +# 小工具 + +feapder 在 `feapder.utils.tools` 中内置了很多爬虫常用工具。维护 feapder 项目时,如果项目已经使用 `from feapder.utils import tools`,优先考虑复用这些工具。 + +## 导入 + +```python +from feapder.utils import tools +``` + +## HTTP 和调试 + +直接可用的函数: +遇到 cURL、cookie、header、URL 参数、JSON、时间格式化、SQL 拼接这类 feapder 项目内的小工具问题,先检查 `feapder shell` 和 `feapder.utils.tools`。AI 不允许先引入第三方转换工具(如 `curlconverter`)或写一套纯 `requests` 调试脚本;若判断确实要绕过 feapder 工具,必须先向用户说明原因、影响和替代方案,并明确请求用户授权。 + +- `tools.get_html_by_requests(...)` +- `tools.get_json_by_requests(...)` +- `tools.download_file(...)` +- `tools.is_valid_proxy(proxy, check_url=None)` +- `tools.is_valid_url(url)` + +CLI 调试器: + +```bash +feapder shell --url https://example.com +feapder shell --curl +``` + +`feapder shell --curl` 会读取剪贴板里的 cURL 命令,解析 URL、headers、cookies、params、body、method、auth 等信息,然后打开带有 `response` 变量的 IPython 会话。 + +相关源码: + +- `feapder/commands/shell.py` +- `feapder.utils.tools.parse_url_params` +- `feapder.utils.tools.get_cookies_from_str` + +## Cookie 工具 + +常用函数: + +- `get_cookies(response)` +- `get_cookies_from_str(cookie_str)` +- `get_cookies_jar(cookies)` +- `get_cookies_from_selenium_cookie(cookies)` +- `cookiesjar2str(cookies)` +- `cookies2str(cookies)` + +示例: + +```python +cookies = tools.get_cookies_from_str("a=1; b=2") +cookie_str = tools.cookies2str(cookies) +``` + +## URL 工具 + +常用函数: + +- `get_urls(html, ...)` +- `get_full_url(root_url, sub_url)` +- `joint_url(url, params)` +- `canonicalize_url(url)` +- `get_url_md5(url)` +- `fit_url(urls, identis)` +- `get_param(url, key)` +- `get_all_params(url)` +- `parse_url_params(url)` +- `urlencode(params)` +- `urldecode(url)` +- `quote_url(url)` +- `unquote_url(url)` +- `quote_chinese_word(text)` +- `get_domain(url)` +- `get_index_url(url)` + +示例: + +```python +url, params = tools.parse_url_params("https://example.com/list?page=1&q=a") +full_url = tools.get_full_url("https://example.com/a/", "../b") +``` + +## HTML 和文本工具 + +常用函数: + +- `get_info(html, regexs, allow_repeat=True, fetch_one=False, split=None)` +- `table_json(table, save_one_blank=True)` +- `get_table_row_data(table)` +- `rows2json(rows, keys=None)` +- `get_form_data(form)` +- `get_text(soup, *args)` +- `del_html_tag(content, save_line_break=True, save_p=False, save_img=False)` +- `del_html_js_css(content)` +- `is_have_chinese(content)` +- `is_have_english(content)` +- `get_chinese_word(content)` +- `get_english_words(content)` +- `replace_str(source_str, regex, replace_str="")` +- `del_redundant_blank_character(text)` +- `unescape(text)` +- `excape(text)` + +快速清洗可以用这些函数。页面主解析优先用 `Response.xpath`、`Response.css` 或 `Response.re`。 + +## JSON 工具 + +常用函数: + +- `get_json(json_str)` +- `jsonp2json(jsonp)` +- `dumps_json(data, indent=4, sort_keys=False)` +- `get_json_value(json_object, key)` +- `get_all_keys(datas, depth=None)` +- `format_json_key(json_data)` +- `quick_to_json(text)` +- `print_pretty(obj)` +- `print_params2json(url)` +- `print_cookie2json(cookie_str_or_list)` + +示例: + +```python +data = tools.get_json(response.text) +token = tools.get_json_value(data, "token") +``` + +## 日期和时间工具 + +常用函数: + +- `date_to_timestamp(date, time_format="%Y-%m-%d %H:%M:%S")` +- `timestamp_to_date(timestamp, time_format="%Y-%m-%d %H:%M:%S")` +- `get_current_timestamp()` +- `get_current_date(date_format="%Y-%m-%d %H:%M:%S")` +- `get_date_number(year=None, month=None, day=None)` +- `get_between_date(begin_date, end_date=None, ...)` +- `get_between_months(begin_date, end_date=None)` +- `get_today_of_day(day_offset=0)` +- `get_days_of_month(year, month)` +- `get_firstday_of_month(date)` +- `get_lastday_of_month(date)` +- `get_firstday_month(month_offset=0)` +- `get_lastday_month(month_offset=0)` +- `get_last_month(month_offset=0)` +- `get_year_month_and_days(month_offset=0)` +- `get_month(month_offset=0)` +- `format_date(date, old_format="", new_format="%Y-%m-%d %H:%M:%S")` +- `format_time(release_time, date_format="%Y-%m-%d %H:%M:%S")` +- `to_date(date_str, date_format="%Y-%m-%d %H:%M:%S")` +- `get_before_date(...)` +- `delay_time(sleep_time=60)` +- `format_seconds(seconds)` + +示例: + +```python +publish_time = tools.format_time("昨天") +``` + +## Hash、编码、随机值和转换 + +常用函数: + +- `get_md5(*args)` +- `get_sha1(*args)` +- `get_base64(data)` +- `get_uuid(key1="", key2="")` +- `get_hash(text)` +- `cut_string(text, length)` +- `get_random_string(length=1)` +- `get_random_password(length=8, special_characters="")` +- `get_random_email(length=None, email_types=None, special_characters="")` +- `dumps_obj(obj)` +- `loads_obj(obj_str)` +- `ensure_int(n)` +- `ensure_float(n)` +- `flatten(x)` +- `iflatten(x)` +- `key2underline(key, strict=True)` +- `key2hump(key)` +- `transform_lower_num(data_str)` +- `to_chinese(unicode_str)` + +## SQL 工具 + +常用函数: + +- `format_sql_value(value)` +- `list2str(datas)` +- `make_insert_sql(table, data, ...)` +- `make_update_sql(table, data, condition)` +- `make_batch_sql(...)` + +常规入库优先用 `Item` 和 pipeline。SQL 工具更适合一次性 SQL 生成或自定义 pipeline 内部逻辑。 + +## 文件、工作目录和动态导入 + +常用函数: + +- `get_conf_value(config_file, section, key)` +- `mkdir(path)` +- `get_cache_path(filename, root_dir=None, local=False)` +- `write_file(filename, content, mode="w", encoding="utf-8")` +- `read_file(filename, readlines=False, encoding="utf-8")` +- `is_html(url)` +- `is_exist(file_path)` +- `get_file_list(path, ignore=[])` +- `rename_file(old_name, new_name)` +- `del_file(path, ignore=())` +- `get_file_type(file_name)` +- `get_file_path(file_path)` +- `switch_workspace(project_path)` +- `import_cls(cls_info)` +- `get_method(obj, name)` +- `make_item(cls, data)` + +这些函数是给 feapder 应用代码使用的。作为 agent 修改仓库文件时,仍按当前环境的文件编辑规则执行。 + +## JavaScript 工具 + +依赖可用的 Node/execjs 环境: + +- `exec_js(js_code)` +- `compile_js(js_func)` + +适合复刻前端签名、加密、参数生成逻辑。 + +## 报警和 Metrics + +报警函数: + +- `dingding_warning(...)` +- `email_warning(...)` +- `linkedsee_warning(...)` +- `wechat_warning(...)` +- `feishu_warning(...)` +- `qmsg_warning(...)` +- `send_msg(...)` +- `reach_freq_limit(rate_limit, *key)` + +metrics 入口在 `feapder/utils/metrics.py`: + +- `metrics.init(...)` +- `metrics.emit_counter(...)` +- `metrics.emit_timer(...)` +- `metrics.emit_store(...)` +- `metrics.flush()` +- `metrics.close()` + +在 spider 内部优先使用框架配置好的报警路径。自定义脚本才考虑直接调用这些函数。 + +## 装饰器和运行时辅助 + +常用函数/类: + +- `Singleton` +- `LazyProperty` +- `log_function_time` +- `run_safe_model(module_name)` +- `memoizemethod_noargs` +- `retry(retry_times=3, interval=0)` +- `retry_asyncio(retry_times=3, interval=0)` +- `func_timeout(timeout)` +- `aio_wrap(loop=None, executor=None)` + +`func_timeout` 使用 Unix signal,不适合 Windows。