@isdk/web-fetcher 0.2.12 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.action.cn.md +197 -155
  2. package/README.action.extract.cn.md +263 -0
  3. package/README.action.extract.md +263 -0
  4. package/README.action.md +202 -147
  5. package/README.cn.md +25 -15
  6. package/README.engine.cn.md +118 -14
  7. package/README.engine.md +115 -14
  8. package/README.md +19 -10
  9. package/dist/index.d.mts +667 -50
  10. package/dist/index.d.ts +667 -50
  11. package/dist/index.js +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/docs/README.md +19 -10
  14. package/docs/_media/README.action.md +202 -147
  15. package/docs/_media/README.cn.md +25 -15
  16. package/docs/_media/README.engine.md +115 -14
  17. package/docs/classes/CheerioFetchEngine.md +805 -135
  18. package/docs/classes/ClickAction.md +33 -33
  19. package/docs/classes/EvaluateAction.md +559 -0
  20. package/docs/classes/ExtractAction.md +33 -33
  21. package/docs/classes/FetchAction.md +39 -33
  22. package/docs/classes/FetchEngine.md +660 -122
  23. package/docs/classes/FetchSession.md +38 -16
  24. package/docs/classes/FillAction.md +33 -33
  25. package/docs/classes/GetContentAction.md +33 -33
  26. package/docs/classes/GotoAction.md +33 -33
  27. package/docs/classes/KeyboardPressAction.md +533 -0
  28. package/docs/classes/KeyboardTypeAction.md +533 -0
  29. package/docs/classes/MouseClickAction.md +533 -0
  30. package/docs/classes/MouseMoveAction.md +533 -0
  31. package/docs/classes/PauseAction.md +33 -33
  32. package/docs/classes/PlaywrightFetchEngine.md +820 -122
  33. package/docs/classes/SubmitAction.md +33 -33
  34. package/docs/classes/TrimAction.md +533 -0
  35. package/docs/classes/WaitForAction.md +33 -33
  36. package/docs/classes/WebFetcher.md +9 -9
  37. package/docs/enumerations/FetchActionResultStatus.md +4 -4
  38. package/docs/functions/fetchWeb.md +6 -6
  39. package/docs/globals.md +14 -0
  40. package/docs/interfaces/BaseFetchActionProperties.md +12 -12
  41. package/docs/interfaces/BaseFetchCollectorActionProperties.md +16 -16
  42. package/docs/interfaces/BaseFetcherProperties.md +32 -28
  43. package/docs/interfaces/Cookie.md +14 -14
  44. package/docs/interfaces/DispatchedEngineAction.md +4 -4
  45. package/docs/interfaces/EvaluateActionOptions.md +81 -0
  46. package/docs/interfaces/ExtractActionProperties.md +12 -12
  47. package/docs/interfaces/FetchActionInContext.md +15 -15
  48. package/docs/interfaces/FetchActionProperties.md +13 -13
  49. package/docs/interfaces/FetchActionResult.md +6 -6
  50. package/docs/interfaces/FetchContext.md +42 -38
  51. package/docs/interfaces/FetchEngineContext.md +37 -33
  52. package/docs/interfaces/FetchMetadata.md +5 -5
  53. package/docs/interfaces/FetchResponse.md +14 -14
  54. package/docs/interfaces/FetchReturnTypeRegistry.md +8 -8
  55. package/docs/interfaces/FetchSite.md +35 -31
  56. package/docs/interfaces/FetcherOptions.md +34 -30
  57. package/docs/interfaces/GotoActionOptions.md +14 -6
  58. package/docs/interfaces/KeyboardPressParams.md +25 -0
  59. package/docs/interfaces/KeyboardTypeParams.md +25 -0
  60. package/docs/interfaces/MouseClickParams.md +49 -0
  61. package/docs/interfaces/MouseMoveParams.md +41 -0
  62. package/docs/interfaces/PendingEngineRequest.md +3 -3
  63. package/docs/interfaces/StorageOptions.md +5 -5
  64. package/docs/interfaces/SubmitActionOptions.md +2 -2
  65. package/docs/interfaces/TrimActionOptions.md +27 -0
  66. package/docs/interfaces/WaitForActionOptions.md +5 -5
  67. package/docs/type-aliases/BaseFetchActionOptions.md +1 -1
  68. package/docs/type-aliases/BaseFetchCollectorOptions.md +1 -1
  69. package/docs/type-aliases/BrowserEngine.md +1 -1
  70. package/docs/type-aliases/FetchActionCapabilities.md +1 -1
  71. package/docs/type-aliases/FetchActionCapabilityMode.md +1 -1
  72. package/docs/type-aliases/FetchActionOptions.md +1 -1
  73. package/docs/type-aliases/FetchEngineAction.md +2 -2
  74. package/docs/type-aliases/FetchEngineType.md +1 -1
  75. package/docs/type-aliases/FetchReturnType.md +1 -1
  76. package/docs/type-aliases/FetchReturnTypeFor.md +1 -1
  77. package/docs/type-aliases/OnFetchPauseCallback.md +1 -1
  78. package/docs/type-aliases/ResourceType.md +1 -1
  79. package/docs/type-aliases/TrimPreset.md +13 -0
  80. package/docs/variables/DefaultFetcherProperties.md +1 -1
  81. package/docs/variables/FetcherOptionKeys.md +1 -1
  82. package/docs/variables/TRIM_PRESETS.md +11 -0
  83. package/package.json +11 -11
@@ -90,6 +90,8 @@ export class FillAction extends FetchAction {
90
90
  * ...其他导航选项,如 **`waitUntil`**, **`timeout`**,这些选项会传递给引擎。
91
91
  * **`returns`**: `response`
92
92
 
93
+ > **注意**:此 Action 可以在单个会话脚本中多次调用。引擎确保每次导航都已完成且其对应的 Action 循环已稳定,然后再处理序列中的下一个 Action。
94
+
93
95
  #### `click`
94
96
 
95
97
  点击由 **`selector`** (CSS 选择器) 指定的元素。
@@ -123,6 +125,41 @@ export class FillAction extends FetchAction {
123
125
  * **`selector`** (string, optional): 表单元素的选择器。
124
126
  * **`returns`**: `none`
125
127
 
128
+ #### `trim`
129
+
130
+ 从 DOM 中移除特定元素以在提取前清理页面。这会对当前会话的页面状态进行持久修改。
131
+
132
+ * **`id`**: `trim`
133
+ * **`params`**:
134
+ * **`selectors`** (string | string[], optional): 一个或多个要移除元素的 CSS 选择器。
135
+ * **`presets`** (string | string[], optional): 预定义的移除元素组。支持的预设:
136
+ * `scripts`: 移除所有 `<script>` 标签。
137
+ * `styles`: 移除所有 `<style>` 和 `<link rel="stylesheet">` 标签。
138
+ * `svgs`: 移除所有 `<svg>` 元素。
139
+ * `images`: 移除 `<img>`, `<picture>` 和 `<canvas>` 元素。
140
+ * `comments`: 移除 HTML 注释。
141
+ * `hidden`: 移除带有 `hidden` 属性或内联 `display:none` 的元素。在 **browser** 模式下,它还会检测并移除通过外部 CSS(如样式表中的 `display: none` 或 `visibility: hidden`)隐藏的元素。
142
+ * `all`: 包含上述所有预设。
143
+ * **`returns`**: `none`
144
+
145
+ **示例:在提取前清理页面**
146
+
147
+ ```json
148
+ {
149
+ "actions": [
150
+ { "action": "goto", "params": { "url": "https://example.com" } },
151
+ {
152
+ "action": "trim",
153
+ "params": {
154
+ "selectors": ["#ad-banner", ".popup"],
155
+ "presets": ["scripts", "styles", "comments"]
156
+ }
157
+ },
158
+ { "action": "extract", "params": { "schema": { "content": "#main-content" } } }
159
+ ]
160
+ }
161
+ ```
162
+
126
163
  #### `waitFor`
127
164
 
128
165
  暂停执行,以等待一个或多个条件的满足。
@@ -208,199 +245,110 @@ await fetchWeb({
208
245
  * **`params`**: (无)
209
246
  * **`returns`**: `response`
210
247
 
211
- #### `extract`
212
-
213
- 使用一个强大且声明式的 **`ExtractSchema`** 从当前页面中提取结构化数据。这是进行数据采集的核心 Action。
214
-
215
- * **`id`**: `extract`
216
- * **`params`**: 一个 **`ExtractSchema`** 对象, 用于定义提取规则。
217
- * **`returns`**: `any` (提取出的数据)
218
-
219
- ##### 提取 Schema 详解
220
-
221
- `params` 对象本身就是一个 Schema, 用于描述您想提取的数据结构。
222
-
223
- ###### 1. 提取单个值
224
-
225
- 最基础的提取,可以指定 **`selector`** (CSS 选择器), **`attribute`** (要提取的属性名), **`type`** (string, number, boolean, html), 以及 **`mode`** (text, innerText)。
226
-
227
- ```json
228
- {
229
- "id": "extract",
230
- "params": {
231
- "selector": "h1.main-title",
232
- "type": "string",
233
- "mode": "innerText"
234
- }
235
- }
236
- ```
237
-
238
- > **提取模式 (Extraction Modes):**
239
- >
240
- > * **`text`** (默认): 提取元素的 `textContent`。
241
- > * **`innerText`**: 提取渲染后的文本,尊重 CSS 样式并处理换行。
242
- > * **`html`**: 返回元素的 `innerHTML`。
243
- > * **`outerHTML`**: 返回包含标签自身的完整 HTML。对于保留元素结构非常有用。
244
- > 上例将使用 `innerText` 模式提取 class 为 `main-title` 的 `<h1>` 标签的文本内容。
245
-
246
- ###### 2. 提取对象
247
-
248
- 通过 **`type: 'object'`** 和 **`properties`** 字段来定义一个结构化对象。
249
-
250
- ```json
251
- {
252
- "id": "extract",
253
- "params": {
254
- "type": "object",
255
- "selector": ".author-bio",
256
- "properties": {
257
- "name": { "selector": ".author-name" },
258
- "email": { "selector": "a.email", "attribute": "href" }
259
- }
260
- }
261
- }
262
- ```
263
-
264
- ###### 3. 提取数组 (便捷用法)
248
+ #### `mouseMove`
265
249
 
266
- 通过 **`type: 'array'`** 来提取一个列表。为了让最常见的操作更简单,我们提供了一些便捷用法。
250
+ 将鼠标指针移动到指定的坐标或元素。在 `browser` 模式下,它使用 **贝塞尔曲线 (Bézier curve)** 来模拟真实人类的非线性移动轨迹,并带有轻微的抖动以增加真实感。
267
251
 
268
- * **提取文本数组 (默认行为)**: 当您想提取一个文本列表时,只需提供选择器,省略 `items` 即可。这是最常见的用法。
252
+ * **`id`**: `mouseMove`
253
+ * **`params`**:
254
+ * `x` (number, 可选): 绝对 X 坐标。
255
+ * `y` (number, 可选): 绝对 Y 坐标。
256
+ * `selector` (string, 可选): CSS 选择器。如果提供,鼠标将移动到该元素的中心。
257
+ * `steps` (number, 可选): 轨迹的中间步数(默认:`-1`)。设置为 `-1` 可根据距离动态计算步数(模拟自然移动速度)。
258
+ * **`returns`**: `none`
269
259
 
270
- ```json
271
- {
272
- "id": "extract",
273
- "params": {
274
- "type": "array",
275
- "selector": ".tags li"
276
- }
277
- }
278
- ```
260
+ #### `mouseClick`
279
261
 
280
- > 上例将返回一个包含所有 `<li>` 标签文本的数组, 如 `["tech", "news"]`。
262
+ 在当前位置或指定坐标触发鼠标点击。如果提供了 `selector`,光标会先平滑地移动到目标元素(使用动态步数),然后再执行点击。
281
263
 
282
- * **提取属性数组 (快捷方式)**: 当您只想提取一个属性列表(例如所有链接的 **`href`**)时,也无需嵌套 `items`。直接在 `array` 定义中声明 **`attribute`** 即可。
264
+ * **`id`**: `mouseClick`
265
+ * **`params`**:
266
+ * `x` (number, 可选): 点击的绝对 X 坐标。
267
+ * `y` (number, 可选): 点击的绝对 Y 坐标。
268
+ * `selector` (string, 可选): CSS 选择器。如果提供,鼠标会先移动到该元素。
269
+ * `button` (string, 可选): 使用的鼠标按键 (`left`, `right`, 或 `middle`)。默认为 `left`。
270
+ * `clickCount` (number, 可选): 点击次数(例如:2 表示双击)。默认为 1。
271
+ * `delay` (number, 可选): mousedown 和 mouseup 之间的延迟(毫秒)。
272
+ * **`returns`**: `none`
283
273
 
284
- ```json
274
+ #### `keyboardType`
285
275
 
286
- {
287
- "id": "extract",
288
- "params": {
289
- "type": "array",
290
- "selector": ".gallery img",
291
- "attribute": "src"
292
- }
293
- }
294
- ```
276
+ 模拟真人在当前获得焦点的元素中输入文本。
295
277
 
296
- > 上例将返回一个包含所有 `<img>` 标签 `src` 属性的数组。
278
+ * **`id`**: `keyboardType`
279
+ * **`params`**:
280
+ * `text` (string): 要输入的文本。
281
+ * `delay` (number, 可选): 按键之间的延迟(毫秒,默认:100)。
282
+ * **`returns`**: `none`
297
283
 
298
- * **数组提取模式**: 在提取数组时,引擎支持不同的模式来处理各种 DOM 结构。
284
+ #### `keyboardPress`
299
285
 
300
- * **`nested`** (默认): `selector` 匹配每个项目的包裹元素。
301
- * **`columnar`** (原 Zip 策略): `selector` 指向**容器**,`items` 中的字段是平行的列,按索引缝合在一起。
302
- * **`segmented`**: `selector` 指向**容器**,项目通过“锚点”字段进行分段提取。
286
+ 模拟按下单个按键或组合键(例如:`Enter`, `Control+A`)。
303
287
 
304
- ###### 4. 列对齐模式 (Columnar Mode,原 Zip 策略)
288
+ * **`id`**: `keyboardPress`
289
+ * **`params`**:
290
+ * `key` (string): 按键名称(例如:`Enter`, `Tab`, `Backspace`, `ArrowUp`)。
291
+ * `delay` (number, 可选): 按键后的延迟(毫秒)。
292
+ * **`returns`**: `none`
305
293
 
306
- `selector` 指向一个**容器**(如结果列表)且项目数据以平行的独立列散落在其中时使用。
294
+ #### `evaluate`
307
295
 
308
- ```json
309
- {
310
- "id": "extract",
311
- "params": {
312
- "type": "array",
313
- "selector": "#search-results",
314
- "mode": "columnar",
315
- "items": {
316
- "title": { "selector": ".item-title" },
317
- "link": { "selector": "a.item-link", "attribute": "href" }
318
- }
319
- }
320
- }
321
- ```
296
+ 在页面上下文中执行 JavaScript 函数或表达式。这是一个强大的 Action,用于执行内置 Action 未涵盖的自定义逻辑。
322
297
 
323
- > **启发式自动检测:** 如果省略了 `mode`,且 `selector` 恰好只匹配到一个元素,同时 `items` 中包含选择器,引擎会自动使用 **columnar** 模式。
298
+ * **`id`**: `evaluate`
299
+ * **`params`**:
300
+ * **`fn`** (string | function): 要执行的函数或表达式。
301
+ * **`args`** (any, 可选): 传递给函数的单个参数。如需传递多个参数,请使用数组或对象。
302
+ * **`returns`**: `any` (执行结果)
324
303
 
325
- **Columnar 配置参数:**
304
+ > **💡 跨引擎兼容性**:
305
+ >
306
+ > * **`browser`**: 直接在浏览器中运行。
307
+ > * **`http`**: 在 Node.js 中运行,并提供模拟环境(提供 `window`, `document` 和 `$`)。
308
+ * **`args`**: `any` - 传递给函数的单个参数。如需传递多个值,请使用数组或对象。
326
309
 
327
- * **`strict`** (boolean, 默认: `true`): 如果为 `true`,当不同字段匹配到的数量不一致时将抛出错误。
328
- * **`inference`** (boolean, 默认: `false`): 如果为 `true`,当字段数量不匹配时,尝试通过 DOM 树自动寻找“包裹元素”来修复错位的列表。
310
+ **核心特性:**
329
311
 
330
- ###### 5. 分段扫描模式 (Segmented Mode)
312
+ - **自动导航检测**:如果代码修改了 `window.location.href` 或调用了 `assign()`/`replace()`,引擎会自动触发并等待导航完成。
313
+ - **增强型 Mock DOM (HTTP 模式)**:支持常用的 DOM 方法,如 `querySelector`, `querySelectorAll`, `getElementById`, `getElementsByClassName`,以及 `document.body` 和 `document.title` 等属性。
314
+ - **沙箱安全**:在 HTTP 模式下使用 `util-ex` 的 `newFunction`,防止全局状态污染。
331
315
 
332
- 适用于完全“平铺”且没有包裹元素的结构。它使用第一个字段(或指定的 `anchor`)作为锚点来对容器内容进行分段。
316
+ **示例 (数组参数):**
333
317
 
334
318
  ```json
335
319
  {
336
- "id": "extract",
320
+ "action": "evaluate",
337
321
  "params": {
338
- "type": "array",
339
- "selector": "#flat-container",
340
- "mode": { "type": "segmented", "anchor": "title" },
341
- "items": {
342
- "title": { "selector": "h3" },
343
- "desc": { "selector": "p" }
344
- }
322
+ "fn": "([a, b]) => a + b",
323
+ "args": [1, 2]
345
324
  }
346
325
  }
347
326
  ```
348
327
 
349
- > 在此模式下,每当引擎发现一个 `h3`,就会开启一个新项目。随后找到的 `p` 标签(直到下一个 `h3` 出现前)都归属于该项目。
350
-
351
- ###### 6. 隐式对象提取 (最简语法)
352
-
353
- 为了让对象提取更简单,你可以省略 `type: 'object'` 和 `properties`。如果 schema 对象包含非保留关键字(如 `selector`, `attribute`, `type` 等)的键,它将被视为对象 schema,其中的键作为属性名。
328
+ **示例 (导航):**
354
329
 
355
330
  ```json
356
331
  {
357
- "id": "extract",
332
+ "action": "evaluate",
358
333
  "params": {
359
- "selector": ".author-bio",
360
- "name": { "selector": ".author-name" },
361
- "email": { "selector": "a.email", "attribute": "href" }
334
+ "fn": "() => { window.location.href = '/new-page'; }"
362
335
  }
363
336
  }
364
337
  ```
365
338
 
366
- > 这等同于示例 2,但更简洁。
367
-
368
- ###### 6. 精确筛选: `has` 和 `exclude`
369
-
370
- 您可以在任何包含 **`selector`** 的 Schema 中使用 **`has`** 和 **`exclude`** 字段来精确控制元素的选择。
371
-
372
- * **`has`**: 一个 CSS 选择器,用于确保所选元素**必须包含**匹配此选择器的后代元素。
373
- * **`exclude`**: 一个 CSS 选择器,用于从结果中**排除**匹配此选择器的元素。
339
+ #### `extract`
374
340
 
375
- **完整示例: 提取包含图片且未被标记为"草稿"的文章链接**
341
+ 使用强大且声明式的 **`ExtractSchema`** 从页面中提取结构化数据。
376
342
 
377
- ```json
378
- {
379
- "actions": [
380
- { "id": "goto", "params": { "url": "https://example.com/articles" } },
381
- {
382
- "id": "extract",
383
- "params": {
384
- "type": "array",
385
- "selector": "div.article-card",
386
- "has": "img.cover-image",
387
- "exclude": ".draft",
388
- "items": {
389
- "selector": "a.title-link",
390
- "attribute": "href"
391
- }
392
- }
393
- }
394
- ]
395
- }
396
- ```
343
+ * **`id`**: `extract`
344
+ * **`params`**: 一个 **`ExtractSchema`** 对象。
345
+ * **`returns`**: 提取出的结构化数据。
397
346
 
398
- > 上述 `extract` Action 会:
347
+ > **📚 详细手册**: 由于 `extract` 功能非常丰富(包含数组模式、作用域控制、锚点跳转等),我们准备了专门的详细文档:
399
348
  >
400
- > 1. 找到所有 `div.article-card` 元素。
401
- > 2. 筛选出其中必须包含 `<img class="cover-image">` 的元素。
402
- > 3. 再从结果中排除掉自身带有 `.draft` 类的元素。
403
- > 4. 对于最终剩下的每个 `div.article-card`, 找到其后代 `a.title-link` 并提取 `href` 属性。
349
+ > 👉 **[点击查看 Extract 操作详解](./README.action.extract.cn.md)**
350
+
351
+ ---
404
352
 
405
353
  ### 通过“组合”构建高级语义 Action
406
354
 
@@ -545,3 +493,97 @@ await fetchWeb({
545
493
  * `protected onAfterExec?()`: 在 `onExecute` 执行后调用。
546
494
 
547
495
  对于需要管理复杂状态或资源的 Action,可以实现这些钩子。通常,对于组合式 Action,直接在 `onExecute` 中编写逻辑已经足够。
496
+
497
+ ---
498
+
499
+ ## 💎 7. Action 返回类型与状态管理 (进阶)
500
+
501
+ 在 `@isdk/web-fetcher` 中,Action 的 `static returnType` 不仅仅是一个类型提示。它定义了框架如何管理 **Session 状态** 以及如何在执行后自动同步数据。
502
+
503
+ ### 7.1 返回类型详解
504
+
505
+ #### 🟢 `response` (页面响应)
506
+
507
+ * **定义**: 包含 HTTP 状态码、响应头、正文和 Cookie 的 `FetchResponse` 对象。
508
+ * **用途**: 将最新的页面内容和状态同步到会话中。
509
+ * **用法**: 适用于执行导航、刷新或获取当前页面快照的动作。
510
+ * **系统行为**: 框架在 `afterExec` 阶段会自动将此结果更新到 `context.lastResponse`。后续动作可以通过上下文访问它。
511
+ * **典型 Action**: `goto`, `getContent`, `fill`(在某些引擎中)。
512
+ * **示例**:
513
+
514
+ ```typescript
515
+ export class MyNavigateAction extends FetchAction {
516
+ static override id = 'myGoto';
517
+ static override returnType = 'response' as const;
518
+
519
+ async onExecute(context, options) {
520
+ // 返回 FetchResponse 的逻辑
521
+ return await this.delegateToEngine(context, 'goto', options.params.url);
522
+ }
523
+ }
524
+ ```
525
+
526
+ #### 🟡 `any` (通用数据 - 默认)
527
+
528
+ * **定义**: 任何可序列化的数据结构(对象、数组、字符串等)。
529
+ * **用途**: 业务数据提取的主要机制。
530
+ * **用法**: 当你的动作产生处理后的业务数据(而不是代表整个页面或系统状态)时使用。
531
+ * **系统行为**: 如果 Action 配置包含 `storeAs: "key"`,框架会自动将 `result` 保存到 `context.outputs["key"]` 中。如果目标键已包含一个对象且新结果也是一个对象,它们将被合并(浅合并)而不是覆盖。这允许通过多个 `extract` 动作将数据累积到同一个输出键中。
532
+ * **典型 Action**: `extract`。
533
+ * **示例**:
534
+
535
+ ```typescript
536
+ static override returnType = 'any' as const;
537
+ async onExecute(context, options) {
538
+ return { title: 'Hello', price: 99 }; // 如果设置了 storeAs,则保存到 outputs
539
+ }
540
+ ```
541
+
542
+ #### ⚪ `none` (无返回)
543
+
544
+ * **定义**: `void`。
545
+ * **用途**: 纯交互或副作用,不输出数据。
546
+ * **用法**: 执行 UI 交互或时间控制的动作。
547
+ * **典型 Action**: `click`, `submit`, `pause`, `trim`, `waitFor`。
548
+ * **示例**:
549
+
550
+ ```typescript
551
+ static override returnType = 'none' as const;
552
+ async onExecute(context, options) {
553
+ await this.delegateToEngine(context, 'click', options.params.selector);
554
+ // 不需要返回值
555
+ }
556
+ ```
557
+
558
+ #### 🔵 `outputs` (累积结果)
559
+
560
+ * **定义**: 整个 `context.outputs` 记录 (`Record<string, any>`)。
561
+ * **用途**: 获取当前会话期间提取并存储的所有数据。
562
+ * **用法**: 通常用作链条末尾的“总结”动作或用于调试。
563
+ * **典型 Action**: 自定义数据总结动作。
564
+
565
+ #### 🟣 `context` (会话快照)
566
+
567
+ * **定义**: 完整的 `FetchContext` 对象。
568
+ * **用途**: 元编程和深度调试。
569
+ * **用法**: 允许调用者检查当前的会话配置(超时、代理、请求头)和内部引擎元数据。
570
+
571
+ ---
572
+
573
+ ### 7.2 结果包装机制 (`FetchActionResult`)
574
+
575
+ `onExecute` 返回的每个值都会被 `FetchAction.execute` 方法自动包装成 `FetchActionResult` 对象。这确保了所有动作之间一致的错误处理和元数据追踪。
576
+
577
+ **`FetchActionResult` 的结构**:
578
+
579
+ * `status`: `Success` (成功), `Failed` (失败), 或 `Skipped` (跳过)。
580
+ * `returnType`: 匹配 Action 的 `static returnType`。
581
+ * `result`: `onExecute` 返回的原始数据。
582
+ * `error`: 如果动作失败,捕获到的错误对象。
583
+ * `meta`: 诊断信息,包括执行时间、引擎类型和重试次数。
584
+
585
+ ### 7.3 开发者最佳实践
586
+
587
+ 1. **为导航选择 `response`**: 对于跳转到新 URL 的动作,务必使用 `response`,以确保会话的“当前页面”保持同步。
588
+ 2. **利用 `any` + `storeAs`**: 对于数据提取,将数据作为 `any` 返回,并让用户通过 JSON 脚本中的 `storeAs` 决定存储键。
589
+ 3. **明确使用 `none`**: 使用 `none` 可以清晰地表明该动作用于其副作用(如点击或等待),使工作流更易于理解。
@@ -0,0 +1,263 @@
1
+ # 🔍 Extract 操作深度指南 (The Data Surgeon's Manual)
2
+
3
+ [English](./README.action.extract.md) | 简体中文
4
+
5
+ `extract` 是 `@isdk/web-fetcher` 的灵魂。它不只是一个“抓取器”,更像是一个**智能转换器**,能把杂乱无章的 HTML 变成你想要的精美 JSON。
6
+
7
+ ---
8
+
9
+ ## ⚡ 1. 快速开始:简写魔法 (Shorthand Magic)
10
+
11
+ **场景**:结构简单的标准网页。当你只想快点看到结果,不想写长长的 JSON 配置时。
12
+
13
+ ```json
14
+ {
15
+ "action": "extract",
16
+ "params": {
17
+ "title": "h1", // 把 h1 里的文字抓出来存为 'title'
18
+ "link": { "selector": "a.main", "attribute": "href" }, // 精确提取链接属性
19
+ "tags": { "type": "array", "selector": ".tag-item" } // 一口气抓下一组标签
20
+ }
21
+ }
22
+ ```
23
+
24
+ ---
25
+
26
+ ## 📄 2. 提取基本值:手术镊子 (The Tweezers)
27
+
28
+ **场景**:处理网页上具体的某一个数据点,如价格、标题或 ID。
29
+
30
+ ### 镊子参数解析 (Value Properties)
31
+
32
+ * **`selector`**: CSS 选择器,告诉镊子要去哪里夹东西。
33
+ * **`type`**: 自动类型转换。支持 `string`, `number` (自动剔除货币符号、单位,只留数字), `boolean`, `html`。
34
+ * **`mode`**: 提取的模式。
35
+ * `innerText`: **(强烈推荐使用)** 就像人眼观察一样,它只取网页上可见的文字,并自动处理换行,过滤掉隐藏的 HTML 噪音。
36
+ * `text`: 原始文本内容,对应源码里的 `textContent`,包含隐藏字符。
37
+ * `outerHTML`: 抓取包含标签自身的完整 HTML 代码。
38
+ * **`attribute`**: 如果你要抓的不是文字,而是属性(如 `href`, `src`),就在这里指定属性名。
39
+ * **`depth`**: 向上回溯。匹配到元素后,先向上跳 N 层父节点再提取(比如你想抓一个按钮的父容器上的 ID)。
40
+
41
+ **示例**:
42
+
43
+ ```json
44
+ {
45
+ "selector": ".price-tag",
46
+ "type": "number", // 自动把 "¥99.00" 转换成数字 99
47
+ "mode": "innerText", // 确保拿到的是干净的文字
48
+ "attribute": "data-v", // 提取 data-v 属性
49
+ "depth": 1 // 抓取该元素父节点的属性
50
+ }
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 📦 3. 提取对象:便当盒 (The Bento Box)
56
+
57
+ **场景**:当你需要把多个相关的字段(如用户的“姓名”、“头像”、“简介”)打包在一起,形成一个完整的结构时。
58
+
59
+ ### 便当盒参数解析 (Object Properties)
60
+
61
+ * **`type: "object"`**: 明确告诉引擎,这是一个需要打包的对象。
62
+ * **`selector`**: **对象的根容器**。这非常重要!内部所有属性的 `selector` 都会相对于这个容器查找。如果你把容器定在 `.user-card`,内部写 `img` 就会只抓这张卡片里的图。
63
+ * **`properties`**: **核心配置**。在这里定义你想要的 JSON 键名(Key)和对应的提取规则。
64
+ * **`required`**: 如果你认为某个字段是“灵魂”,抓不到它这个对象就没意义,请设为 `true`。此时若提取失败,整个对象会返回 `null`。
65
+ * **`depth`**: 用于触发“向上探索”逻辑的深度(详见进阶章节)。
66
+
67
+ **示例:打包用户信息**
68
+
69
+ ```json
70
+ {
71
+ "type": "object",
72
+ "selector": ".user-card",
73
+ "properties": {
74
+ "name": { "selector": ".username", "required": true },
75
+ "bio": ".user-description",
76
+ "avatar": { "selector": "img.avatar", "attribute": "src" }
77
+ }
78
+ }
79
+ ```
80
+
81
+ > **💡 透明盒子 (隐式对象)**:如果你省略 `type: "object"` 且不写 `selector`,这个盒子就成了“透明的”。如果内部所有字段都抓不到结果,整个盒子会自动消失。在过滤列表中的广告或空项时非常神奇。
82
+
83
+ ---
84
+
85
+ ## 📑 4. 提取数组:分拣模式 (The Sorting Machines)
86
+
87
+ **场景**:处理成排出现的数据,如搜索结果列表、新闻瀑布流、或一组分类标签。
88
+
89
+ ### 分拣机参数解析 (Array Properties)
90
+
91
+ * **`type: "array"`**: 明确声明你要提取的是一个列表。
92
+ * **`selector`**: **扫描范围**。在默认模式下,它匹配每一个项目的“外壳”;在平铺模式下,它匹配整个大的列表容器。
93
+ * **`items`**: **每一项的图纸**。注意,数组里用的是 `items` 而不是 `properties`。它定义了每一行数据长什么样。
94
+ * **`mode`**: 决定分拣机的工作模式。
95
+ * `"nested"`: (默认) 每一个项目都有自己的“格子”(容器)。
96
+ * `"columnar"`: 数据按列排布,没有行容器(像 Excel 表格)。
97
+ * `"segmented"`: 数据完全平铺,通过“锚点”来切分段落。
98
+ * **`limit`**: 限制抓取的最大条数,防止数据量过大。
99
+ * **`inference`**: 启发式推断。当列表结构不规范时,开启它可以让引擎自动尝试修正错位。
100
+
101
+ ### 4.1 Nested (蛋托模式):最稳定、首选
102
+
103
+ **场景**:每个项目都被包裹在 `<li>` 或 `.item` 这种容器里。这是最稳、性能最好的选择。
104
+
105
+ ```json
106
+ {
107
+ "type": "array",
108
+ "selector": ".product-list .item", // 匹配每一个产品的外壳
109
+ "items": {
110
+ "name": "h3",
111
+ "price": ".price"
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### 4.2 Columnar (列对齐模式):处理表格类平铺
117
+
118
+ **场景**:数据像表格一样。左边一列是标题,右边一列是价格,但它们没有共同的父节点包裹。
119
+ **广播功能 (Broadcasting)**:如果某个字段在每一项里都一样(如列表的分类),你只需在总容器提取一次,它会自动“广播”给每一行。
120
+
121
+ ```json
122
+ {
123
+ "type": "array",
124
+ "selector": "#table-container",
125
+ "mode": "columnar",
126
+ "items": {
127
+ "name": ".title-cols",
128
+ "date": "" // selector 为空,自动广播总容器里的日期信息
129
+ }
130
+ }
131
+ ```
132
+
133
+ ### 4.3 Segmented (切割模式):处理无序平铺
134
+
135
+ **场景**:页面结构极其糟糕,所有内容都是兄弟节点并排,只能靠某个标志(如 `h2` 标题)来区分段落。
136
+
137
+ ```json
138
+ {
139
+ "type": "array",
140
+ "selector": ".content-body",
141
+ "mode": { "type": "segmented", "anchor": "h2" }, // 看到 h2 就切一刀
142
+ "items": {
143
+ "section_title": "h2",
144
+ "text": "p"
145
+ }
146
+ }
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 🛡️ 5. 质量控制:过滤器与筛子 (Filters & Sieves)
152
+
153
+ **场景**:抓取时混入了广告、缺货提示或者不需要的垃圾项。
154
+
155
+ * **`has`**:**精华过滤器**。只有包含特定子元素时才抓取。例如:`"has": ".promo-tag"` 表示只有带优惠标签的商品我才要。
156
+ * **`exclude`**:**垃圾剔除器**。排除匹配的元素。例如:`"exclude": ".is-advertisement"`。
157
+ * **`strict`**:**强迫症模式**。一旦发现必填项缺失,直接报错停机,而不是默默返回 `null`。
158
+
159
+ ---
160
+
161
+ ## 🧠 6. 核心进阶:边界与作用域控制 (Boundary Mastery)
162
+
163
+ 这是处理那些没有 ID、没有类名、结构平铺的烂网页的“手术刀”。
164
+
165
+ ### 6.1 顺序切片:解决“双胞胎标签”定位难题
166
+
167
+ * **痛点**:网页上一排 `<span>` 或 `p`,长得一模一样。你用同一个选择器去抓,每次都只能抓到第一个。
168
+ * **解决方案**:`relativeTo: "previous"` (相对于上一个)。
169
+ * **原理解析**:引擎会像“读书”一样记住上次读到了哪。当第一个字段提取完后,引擎会夹个“书签”。提取下一个字段时,引擎会**从书签之后**开始找。
170
+ * **关键点**:由于这是按顺序寻找,建议显式设置 `order: ["name", "job"]` 来确保引擎不会因为 JSON 键名的乱序而导致书签位置错乱,虽然大多数js引擎是按照对象keys的声明顺序来排序的。
171
+ * **示例场景**:
172
+
173
+ ```html
174
+ <p>姓名</p> <span>张三</span>
175
+ <p>职位</p> <span>经理</span>
176
+ ```
177
+
178
+ * **配置示例**:
179
+
180
+ ```json
181
+ {
182
+ "relativeTo": "previous",
183
+ "order": ["k1", "v1", "k2", "v2"], // 强制顺序,防止书签错位
184
+ "properties": {
185
+ "k1": "p", // 抓到第一个 p (姓名)
186
+ "v1": "span", // 从 k1 之后找,抓到张三
187
+ "k2": "p", // 从 v1 之后找,抓到第二个 p (职位)
188
+ "v2": "span" // 从 k2 之后找,抓到经理
189
+ }
190
+ }
191
+ ```
192
+
193
+ ### 6.2 分段隔离与 LCA:筑起“逻辑围墙”
194
+
195
+ * **痛点**:平铺结构中提取列表,如果没有墙,Item 1 的搜索可能会越界抓到 Item 2 的内容。
196
+ * **解决方案**:使用 `mode: "segmented"` 配合 `anchor`。
197
+ * **原理解析**:引擎计算两个锚点之间的“最小公共祖先 (LCA)”,并逻辑上筑起围墙,确保搜索范围死死锁在 Item 内部。
198
+ * **示例场景**:
199
+
200
+ ```html
201
+ <h2 class="title">文章1</h2>
202
+ <p class="summary">摘要1</p>
203
+ <h2 class="title">文章2</h2>
204
+ <p class="summary">摘要2</p>
205
+ ```
206
+
207
+ * **配置示例**:
208
+
209
+ ```json
210
+ {
211
+ "type": "array",
212
+ "mode": { "type": "segmented", "anchor": "h2.title" }, // 以 h2 为墙
213
+ "items": {
214
+ "title": "h2",
215
+ "desc": "p"
216
+ }
217
+ }
218
+ ```
219
+
220
+ ### 6.3 向上探索 (Bubble-up):给提取一次“后悔药”
221
+
222
+ * **痛点**:作用域定在卡片内部,但关键数据(如卡片 ID)在卡片外面的父容器上。
223
+ * **解决方案**:设置 `depth` (向上回退层数) 并配合 `required`。
224
+ * **原理解析**:
225
+ 1. 引擎在当前范围内找必填项,没找到。
226
+ 2. 它会根据 `depth: 1` 自动把视野“向后退一步”,退到父节点。
227
+ 3. 在更大的视野里重新扫描,从而找齐那些“由于视野太窄而漏掉”的数据。
228
+ * **示例场景**:
229
+
230
+ ```html
231
+ <div class="wrapper" data-id="ID_001">
232
+ <div class="content-box">
233
+ <h3 class="title">产品名称</h3>
234
+ </div>
235
+ </div>
236
+ ```
237
+
238
+ * **配置示例**:
239
+
240
+ ```json
241
+ {
242
+ "selector": ".content-box", // 初始视角太窄
243
+ "depth": 1, // 允许退后一级
244
+ "properties": {
245
+ "product_id": {
246
+ "selector": "..", // ".." 代表向上找父节点
247
+ "attribute": "data-id",
248
+ "required": true // 没找到 ID 就触发向上回溯
249
+ },
250
+ "name": "h3"
251
+ }
252
+ }
253
+ ```
254
+
255
+ ---
256
+
257
+ ## 💡 7. 最佳实践提示
258
+
259
+ 1. **容器优先**:只要 HTML 里有 `.item` 这种包裹容器,永远优先选择默认的 `Nested` 模式,这是性能和稳定性最好的选择。
260
+ 2. **innerText 推荐作为首选**:在 90% 的场景下,它都比 `text` 好用,因为它拿到的数据最干净,且能自动处理换行。
261
+ 3. **遇到平铺找锚点**:当你看到一堆兄弟节点并排且没有外壳包裹时,第一时间就该想到 `segmented` + `anchor` 的组合。
262
+ 4. **善用 required 过滤空数据**:如果你抓到一个列表,有些项是空的(比如预留位),给核心字段加上 `required: true`,那些空项就会自动变成 `null`,方便你在代码里过滤。
263
+ 5. **调试必备:先看 Scope**:如果抓不到数据,先检查是不是 `selector` 写得太死,或者尝试把 `depth` 调大一点。