# spiderflow **Repository Path**: goodffff/spiderflow ## Basic Information - **Project Name**: spiderflow - **Description**: ^_^ - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-09 - **Last Updated**: 2026-06-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpiderFlow 基于 [Crawlee](https://crawlee.dev) 与 [TypeORM](https://typeorm.io) 的声明式爬虫框架。用装饰器或一个配置对象声明「抓什么」,框架负责「怎么抓」:调度、翻页、进详情页、滚动、登录、提取、入库。 - **语言**:TypeScript(ESM,Node ≥ 18) - **两条引擎**:`cheerio`(纯 HTTP,快)/ `playwright`(无头浏览器,可渲染 JS、可交互) - **两种写法**:装饰器实体(配合 TypeORM)/ `defineSpider` 配置对象(无装饰器、无数据库) --- ## 目录 1. [安装](#1-安装) 2. [第一个爬虫](#2-第一个爬虫) 3. [装饰器 + 数据库](#3-装饰器--数据库) 4. [字段提取](#4-字段提取) 5. [翻页与进详情页](#5-翻页与进详情页) 6. [动态页面:滚动 / 登录 / 复杂选择器 / 交互](#6-动态页面) 7. [生命周期钩子](#7-生命周期钩子) 8. [引擎与存储](#8-引擎与存储) 9. [配置速查](#9-配置速查) 10. [开发命令与目录](#10-开发命令与目录) 子包文档:[`@spiderflow/core`](packages/core/README.md)(完整 API 参考)、[`@spiderflow/koa`](packages/koa/README.md)(HTTP/WebSocket 服务)、[`examples`](examples/README.md)(可运行示例)。 --- ## 1. 安装 ```bash npm install @spiderflow/core crawlee reflect-metadata # 可选:仅当使用默认的 TypeORM 入库时 npm install typeorm better-sqlite3 # 可选:仅当使用 playwright 引擎时,需安装一次浏览器 npx playwright install chromium ``` - `crawlee` 是 peer 依赖,必装。 - `typeorm` 是可选 peer 依赖,只有用内置 `TypeOrmSink` 时才需要。 - `date-fns`、`p-retry` 已作为普通依赖随包安装,无需手动安装。 ESM 项目要求:`package.json` 中 `"type": "module"`,`tsconfig.json` 中 `"experimentalDecorators": true`、`"emitDecoratorMetadata": true`。 --- ## 2. 第一个爬虫 最简路径:`defineSpider` 配置对象 + 内存存储,不需要装饰器和数据库。 ```ts import { crawl, defineSpider, MemorySink, t } from '@spiderflow/core'; interface Book { title: string; price: number; } const sink = new MemorySink(); const Book = defineSpider({ name: 'Book', startUrls: 'https://books.toscrape.com/catalogue/page-1.html', itemSelector: 'article.product_pod', // 列表页中「一条数据」的容器 paginate: { next: 'li.next > a', limit: 2 }, // 翻页,最多 2 页 fields: { title: 'h3 > a', // 字符串 = 选择器,取文本 price: { selector: 'p.price_color', transform: t.number }, // "£51.77" → 51.77 }, }); await crawl({ sink }, Book); console.log(sink.items); // [{ title, price }, ...] ``` 要点: - `itemSelector` 把列表页切成多条数据;每个字段选择器在**单条容器内**解析。 - 字段值为字符串时即选择器;要做转换/校验/取属性时用对象写法。 - `t` 是内置转换库,`t.number` 自动从 `"£51.77"` 提取数字。 - 不设 `itemSelector` 时,整页视为一条数据。 --- ## 3. 装饰器 + 数据库 用装饰器声明 TypeORM 实体,抓取结果直接入库。 ### 3.1 数据源 ```ts import 'reflect-metadata'; import { DataSource } from 'typeorm'; import { Book } from './book.entity'; export const dataSource = new DataSource({ type: 'better-sqlite3', database: './data.sqlite', entities: [Book], synchronize: true, // 仅开发环境 }); ``` ### 3.2 实体 ```ts import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Spider, Field, Paginate, t } from '@spiderflow/core'; @Entity({ name: 'books' }) @Spider({ startUrls: 'https://books.toscrape.com/catalogue/page-1.html', itemSelector: 'article.product_pod', }) @Paginate({ next: 'li.next > a', limit: 2 }) export class Book { @PrimaryGeneratedColumn() id!: number; @Column('text') @Field({ selector: 'h3 > a', attr: 'title' }) title!: string; @Column('real') @Field({ selector: 'p.price_color', transform: t.number }) price!: number; } ``` ### 3.3 启动 ```ts import { crawl } from '@spiderflow/core'; import { dataSource } from './data-source'; import { Book } from './book.entity'; await dataSource.initialize(); await crawl({ dataSource }, Book); await dataSource.destroy(); ``` `crawl(runtime, ...entities)` 是入口;`runtime` 至少提供 `dataSource`(TypeORM 入库)或 `sink`(自定义存储)之一。 --- ## 4. 字段提取 ### 4.1 约定规则 不写选择器/属性/类型时,框架按字段名推断: | 规则 | 行为 | | --- | --- | | 选择器 | `productTitle` → `[data-product-title]` | | 名字以 `url`/`link`/`href` 结尾 | 取 `href` 属性,并解析为绝对 URL | | 名字以 `src`/`image`/`img` 结尾 | 取 `src` 属性 | | 其他 | 取文本 | ### 4.2 `@Field` 选项 | 选项 | 说明 | | --- | --- | | `selector` | CSS 选择器(支持[选择器迷你语言](#63-复杂选择器)),缺省按约定推断 | | `kind` | 提取类型:`TEXT`/`HTML`/`ATTR`/`EXISTS`/`COUNT` | | `attr` | `kind: ATTR` 时读取的属性名 | | `transform` | 单个或数组形式的转换管线,从左到右执行,可异步 | | `validate` | 终值校验,返回 `true` 通过,返回字符串作为错误信息 | | `required` | 为真且取不到值时抛错 | | `default` | 取不到值时的默认值 | | `many` | 收集所有匹配为数组(而非取第一个) | | `absolute` | 是否把值解析为绝对 URL(`href`/`src` 默认开启) | | `nested` | 提取结构化子对象(见 4.4) | | `compute` | 从整条数据派生本字段(见 4.5) | ### 4.3 快捷装饰器 | 装饰器 | 等价 | | --- | --- | | `@Link()` | 取 `href`,解析为绝对 URL | | `@Image()` | 取 `src` | | `@Attr(name)` | 取指定属性 | | `@Html()` | 取内部 HTML | | `@Many()` | 收集所有匹配为数组 | | `@Exists()` | 是否存在匹配(布尔) | | `@Count()` | 匹配数量(数字) | | `@Nested()` | 结构化子对象 | | `@Compute()` | 派生字段 | | `@Date()` | 解析日期 | ### 4.4 内置转换库 `t` `t.number`、`t.integer`、`t.boolean`、`t.text`、`t.trim`、`t.lower`、`t.upper`、`t.list`、`t.replace(a, b)`、`t.match(re)`、`t.url`、`t.absoluteUrl`、`t.date(opts)`、`t.fallback(v)`。 ```ts // 单个 @Field({ selector: '.price', transform: t.number }) // 管线 @Field({ selector: '.rating', transform: [t.replace('star-rating', ''), t.text] }) ``` ### 4.5 嵌套提取 `nested` 让一个字段产出结构化子对象(配合 `many` 产出数组)。`selector` 定位容器,`nested` 内各子字段在容器内解析。 ```ts @Field({ selector: '.review', many: true, nested: { author: '.review-author', stars: { selector: '.stars', transform: t.number }, }, }) reviews!: { author: string; stars: number }[]; ``` ### 4.6 派生字段 `compute` 在所有字段提取完成后运行,可读取同条数据的其他字段: ```ts @Field({ compute: (_v, item) => item.salePrice < item.listPrice }) onSale!: boolean; ``` --- ## 5. 翻页与进详情页 ### 5.1 翻页 `@Paginate` ```ts // 方式一:next 选择器 @Paginate({ next: 'li.next > a', limit: 5 }) // 方式二:计算下一页 URL @Paginate({ nextUrl: (ctx) => buildNextUrl(ctx.url), limit: 5 }) ``` `limit` 限制翻页页数;`next` 与 `nextUrl` 二选一(同时存在时 `nextUrl` 优先)。 ### 5.2 进详情页 `@Follow` 列表页只有链接、数据在详情页时使用: ```ts @Spider({ startUrls: '...', /* 注意:用 Follow 时通常不设 itemSelector */ }) @Follow({ selector: 'h3 > a' }) // 跟进每个详情链接 export class Book { @Field({ selector: 'div.product_main h1' }) title!: string; // ... 字段在详情页解析 } ``` 不指定 `target` 时,每个详情页产出一条当前实体;`target` 可指向另一个实体或其 thunk(`() => Other`)。 ### 5.3 抓取流转 `startUrls`(LIST)→ 若有 `@Follow` 则把详情链接入队为 DETAIL,否则按 `itemSelector` 就地提取 → 跟进 `@Paginate` 翻页 → 每条数据经钩子后批量写入 Sink。 --- ## 6. 动态页面 四块能力覆盖「需要浏览器交互」的场景,**完全向后兼容**:不用时行为不变。滚动 / 表单登录 / 交互脚本需 `engine: 'playwright'`;在 `cheerio` 引擎上会跳过并给一次清晰警告。Cookie/Token 注入与选择器迷你语言**两种引擎均可用**。 ### 6.1 无限滚动 / 加载更多 ```ts @Spider({ startUrls: 'https://example.com/feed', engine: 'playwright', itemSelector: 'div.card', scroll: { itemSelector: 'div.card', // 按元素数量判断是否还在加载 maxScrolls: 20, // 安全上限 delayMs: 600, // 每次滚动后等待加载 stableRounds: 2, // 连续 2 轮无增长即停止 // loadMoreSelector: 'button.more', // 改为点击「加载更多」按钮 }, }) class Card {} ``` 等价装饰器:`@Scroll({ ... })`。框架在提取前滚动页面,使懒加载内容进入 DOM。 ### 6.2 登录 / 会话保持 两种方式可单用或组合。 **表单登录** `auth.form`:抓取前执行一次登录脚本,`successSelector` 用于校验登录成功(失败抛错,避免静默抓到未登录页): ```ts @Spider({ startUrls: 'https://example.com/', engine: 'playwright', auth: { form: { loginUrl: 'https://example.com/login', steps: [ { action: 'fill', selector: 'input#email', value: process.env.USER! }, { action: 'fill', selector: 'input#pass', value: process.env.PASS! }, { action: 'click', selector: 'button[type="submit"]' }, ], successSelector: 'a[href="/logout"]', }, }, }) class Item {} ``` **Cookie / Token 注入** `auth.session`:已持有会话时免登录往返,`cheerio` 引擎亦可用: ```ts @Spider({ startUrls: 'https://api.example.com/items', engine: 'cheerio', auth: { session: { cookies: [{ name: 'session', value: process.env.SESSION! }], headers: { Authorization: `Bearer ${process.env.TOKEN}` }, }, }, }) class Item {} ``` 等价装饰器:`@Auth({ form, session })`。 ### 6.3 复杂选择器 每个选择器字符串支持在 CSS 之上的可选前缀层。**纯 CSS 字符串行为不变。** | 写法 | 含义 | | --- | --- | | `.title` / `css:.title` | CSS(默认) | | `xpath://h1[@class]` | XPath(仅 playwright 引擎) | | `text:加入购物车` | 文本精确等于 | | `text*:购物车` | 文本包含 | | `.row >> text:购买` | 在左侧匹配范围内解析右侧 | | `.item \|nth=2` | 第 3 个匹配(从 0 计);另有 `\|first`、`\|last` | | `a \|href~=^/products/` | 属性正则匹配 | | `.btn \|has-text=保存` | 匹配包含指定文本者 | 也可用链式 `Sel` 构造器(带自动补全): ```ts import { Sel } from '@spiderflow/core'; Sel.css('.product').nth(2) // ".product |nth=2" Sel.css('.tags a').attrMatches('href', /^\/tag\//) Sel.text('加入购物车') Sel.css('.row').within(Sel.text('购买')) // ".row >> text:购买" ``` ### 6.4 交互脚本 横幅关闭、展开内容、切换标签等长尾场景,用 `@Interact` 在提取前执行: ```ts @Interact([ { action: 'click', selector: 'text:同意 Cookie' }, { action: 'waitForSelector', selector: '.content' }, ]) ``` 步骤类型:`waitForSelector` / `wait` / `click` / `fill` / `press` / `goto` / `run`(任意函数)。 --- ## 7. 生命周期钩子 在 `runtime.hooks` 或 `@Spider({ hooks })` 中提供,均可异步: | 钩子 | 时机 | | --- | --- | | `transformRequest(req)` | 入队前改写/丢弃请求(返回 `false` 丢弃) | | `afterExtract(item, ctx)` | 提取后入库前改写/丢弃(返回 `false` 丢弃) | | `beforeSave(batch)` | 批量写入前 | | `afterSave(batch, total)` | 批量写入后 | | `onError(err, ctx)` | 页面处理抛错时(爬取继续) | 运行结束会产出结构化 `RunReport`(页数、条数、错误分类、各域名计数、耗时),通过 `runtime.onReport(report, entity)` 获取。 --- ## 8. 引擎与存储 ### 8.1 引擎 - `cheerio`(默认):纯 HTTP + Cheerio 解析,快,不渲染 JS。 - `playwright`:无头 Chromium,可渲染 JS、可交互。 - 自定义:`registerEngine(name, factory)` 注册后用 `engine: name` 引用。 逃生舱:`@Spider({ crawlerOptions })` 中可传任意 Crawlee 原生选项(代理、会话、`preNavigationHooks` 等)。 ### 8.2 存储 - 默认 `TypeOrmSink`(提供 `dataSource` 时自动启用)。 - 内置 `MemorySink`(结果存内存数组,测试/调试用)。 - 自定义:实现 `Sink` 接口的单个方法 `saveBatch(items)`(可选 `close()`),通过 `runtime.sink` 提供。批量、计数、钩子由框架负责。 --- ## 9. 配置速查 `@Spider` / `defineSpider` 主要选项: | 选项 | 说明 | | --- | --- | | `startUrls` | 入口 URL(字符串或数组) | | `itemSelector` | 列表页单条数据容器选择器 | | `engine` | `cheerio`(默认)/ `playwright` / 自定义名 | | `concurrency` | 最大并发 | | `maxRequests` | 总请求上限 | | `maxItems` | 收集到 N 条即停(可中途停) | | `retries` | 单请求失败重试次数 | | `requestTimeoutMs` | 单页处理超时 | | `requestDelayMs` | 同域请求间隔 | | `dedupeKey` | 按键去重数据(函数或 `true`) | | `scroll` | 无限滚动配置 | | `auth` | 登录/会话配置 | | `interact` | 每页交互脚本 | | `hooks` | 爬虫级钩子 | | `crawlerOptions` | Crawlee 原生选项逃生舱 | `runtime`(`crawl` 第一参数):`dataSource` / `sink` / `batchSize` / `saveRetries` / `verbose` / `onProgress` / `onReport` / `hooks`。 --- ## 10. 开发命令与目录 ```text packages/core 核心框架(装饰器、引擎、提取、存储、运行器) packages/koa Koa 集成(REST 触发/查询 + WebSocket 任务流) examples 可运行示例 ``` ```bash pnpm install # 安装依赖 pnpm build # 构建所有包 pnpm typecheck # 类型检查 pnpm lint # 代码检查 pnpm example:scroll # 运行无限滚动示例(其余见 examples/README.md) ``` 合规提示:抓取前阅读目标站 `robots.txt` 与服务条款,设置合理的 `concurrency` / `requestDelayMs`,遵守当地法律与数据保护规定。示例默认指向专供抓取练习的沙箱站点。