@isdk/web-searcher 0.1.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.
- package/README.cn.md +274 -0
- package/README.md +274 -0
- package/dist/index.d.mts +321 -0
- package/dist/index.d.ts +321 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/docs/README.md +278 -0
- package/docs/classes/GoogleSearcher.md +695 -0
- package/docs/classes/WebSearcher.md +661 -0
- package/docs/globals.md +26 -0
- package/docs/interfaces/CustomTimeRange.md +29 -0
- package/docs/interfaces/PaginationConfig.md +86 -0
- package/docs/interfaces/SearchContext.md +41 -0
- package/docs/interfaces/SearchOptions.md +105 -0
- package/docs/interfaces/StandardSearchResult.md +58 -0
- package/docs/type-aliases/SafeSearchLevel.md +11 -0
- package/docs/type-aliases/SearchCategory.md +11 -0
- package/docs/type-aliases/SearchTimeRange.md +11 -0
- package/docs/type-aliases/SearchTimeRangePreset.md +11 -0
- package/docs/type-aliases/SearcherConstructor.md +23 -0
- package/package.json +87 -0
package/README.cn.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# 搜索模块 (Search Module)
|
|
2
|
+
|
|
3
|
+
Search 模块提供了一个基于类的高级框架,用于构建搜索引擎抓取工具。它构建在 `@isdk/web-fetcher` 之上,扩展了**多页导航**、**会话持久化**和**结果标准化**的能力。
|
|
4
|
+
|
|
5
|
+
## 🌟 为什么要使用搜索模块?
|
|
6
|
+
|
|
7
|
+
构建一个健壮的搜索抓取工具不仅仅是请求一个 URL。通常你需要:
|
|
8
|
+
|
|
9
|
+
- **分页**: 自动点击“下一页”或修改 URL 参数,直到获取足够的结果。
|
|
10
|
+
- **会话管理**: 在多个搜索查询之间维护 Cookie 和 Header。
|
|
11
|
+
- **数据清洗**: 解析原始 HTML 并处理重定向链接。
|
|
12
|
+
- **灵活性**: 轻松切换 HTTP(快速)和 Browser(抗反爬)模式。
|
|
13
|
+
|
|
14
|
+
本模块将这些通用模式封装在一个可复用的 `Searcher` 类中。
|
|
15
|
+
|
|
16
|
+
## 🚀 快速开始
|
|
17
|
+
|
|
18
|
+
### 1. 一次性搜索 (One-off Search)
|
|
19
|
+
|
|
20
|
+
使用静态方法 `Searcher.search` 处理快速、用完即弃的任务。它会自动创建会话、抓取结果并进行清理。
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Searcher } from '@isdk/web-fetcher/search';
|
|
24
|
+
import { GoogleSearcher } from '@isdk/web-fetcher/search/engines/google';
|
|
25
|
+
|
|
26
|
+
// 注册引擎 (只需执行一次)
|
|
27
|
+
Searcher.register(GoogleSearcher);
|
|
28
|
+
|
|
29
|
+
// 搜索!
|
|
30
|
+
// 'limit' 参数确保我们会自动翻页直到获取 20 条结果。
|
|
31
|
+
// 注意:引擎名称区分大小写,且由类名自动提取(例如:'GoogleSearcher' -> 'Google')
|
|
32
|
+
const results = await Searcher.search('Google', 'open source', { limit: 20 });
|
|
33
|
+
|
|
34
|
+
console.log(results);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. 有状态会话 (Stateful Session)
|
|
38
|
+
|
|
39
|
+
由于 `Searcher` 继承自 `FetchSession`,您可以实例化它以在多个请求之间保持 Cookie 和存储。这对于需要登录的搜索或通过模拟人类行为来避免反爬虫非常有用。
|
|
40
|
+
|
|
41
|
+
**配置优先级:**
|
|
42
|
+
创建会话时,选项按以下顺序合并:
|
|
43
|
+
1. **模板默认 (Template Default)**:在 Searcher 类中定义(结构化选项的优先级最高)。
|
|
44
|
+
2. **用户选项 (User Options)**:传递给构造函数的选项(可填充缺失的默认值,或在允许的情况下进行覆盖)。
|
|
45
|
+
|
|
46
|
+
*注:如果模板设置了 `engine: 'auto'`(默认值),则会尊重用户提供的 `engine` 选项。*
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// 创建一个持久化会话
|
|
50
|
+
const google = new GoogleSearcher({
|
|
51
|
+
headless: false, // 覆盖默认选项 (例如显示浏览器)
|
|
52
|
+
proxy: 'http://my-proxy:8080',
|
|
53
|
+
timeoutMs: 30000 // 为请求设置全局超时
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// 第一次查询
|
|
58
|
+
// 您还可以传递运行时选项来覆盖会话默认值或注入变量
|
|
59
|
+
const results1 = await google.search('term A', {
|
|
60
|
+
timeoutMs: 60000, // 仅针对此搜索覆盖超时时间
|
|
61
|
+
extraParam: 'value' // 可以在模板中通过 ${extraParam} 使用
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 第二次查询 (复用同一个浏览器窗口/Cookies)
|
|
65
|
+
const results2 = await google.search('term B');
|
|
66
|
+
} finally {
|
|
67
|
+
// 务必销毁以关闭浏览器/释放资源
|
|
68
|
+
await google.dispose();
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 🛠️ 实现一个新的搜索引擎
|
|
73
|
+
|
|
74
|
+
要支持一个新的网站,请创建一个继承自 `Searcher` 的类。
|
|
75
|
+
|
|
76
|
+
### 步骤 1: 定义模板 (Template)
|
|
77
|
+
|
|
78
|
+
要支持一个新的网站,请创建一个继承自 `Searcher` 的类。引擎名称默认由类名自动提取(例如:`MyBlogSearcher` -> `MyBlog`),但您可以通过静态属性自定义名称和别名。
|
|
79
|
+
|
|
80
|
+
`template` 属性定义了搜索的“蓝图”。它是一个标准的 `FetcherOptions` 对象,但支持**变量注入**。
|
|
81
|
+
|
|
82
|
+
支持的变量:
|
|
83
|
+
|
|
84
|
+
- `${query}`: 搜索关键词。
|
|
85
|
+
- `${page}`: 当前页码 (根据配置从 0 或 1 开始)。
|
|
86
|
+
- `${offset}`: 当前条目偏移量 (例如 0, 10, 20)。
|
|
87
|
+
- `${limit}`: 请求的限制数量。
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { Searcher } from '@isdk/web-fetcher/search';
|
|
91
|
+
import { FetcherOptions } from '@isdk/web-fetcher/types';
|
|
92
|
+
|
|
93
|
+
export class MyBlogSearcher extends Searcher {
|
|
94
|
+
static name = 'blog'; // 自定义名称 (区分大小写)
|
|
95
|
+
static alias = ['myblog', 'news'];
|
|
96
|
+
|
|
97
|
+
protected get template(): FetcherOptions {
|
|
98
|
+
return {
|
|
99
|
+
engine: 'http', // 如果网站有反爬虫,请使用 'browser'
|
|
100
|
+
// 带有变量的动态 URL
|
|
101
|
+
url: 'https://blog.example.com/search?q=${query}&page=${page}',
|
|
102
|
+
actions: [
|
|
103
|
+
{
|
|
104
|
+
id: 'extract',
|
|
105
|
+
storeAs: 'results', // 必须将结果存储在这里
|
|
106
|
+
params: {
|
|
107
|
+
type: 'array',
|
|
108
|
+
selector: 'article.post',
|
|
109
|
+
items: {
|
|
110
|
+
title: { selector: 'h2' },
|
|
111
|
+
url: { selector: 'a', attribute: 'href' }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 步骤 2: 配置分页 (Pagination)
|
|
122
|
+
|
|
123
|
+
告诉 `Searcher` 如何导航到下一页。实现 `pagination` 获取器。
|
|
124
|
+
|
|
125
|
+
#### 方案 A: URL 参数 (Offset/Page)
|
|
126
|
+
|
|
127
|
+
最适合无状态的 HTTP 抓取。
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
protected override get pagination() {
|
|
131
|
+
return {
|
|
132
|
+
type: 'url-param',
|
|
133
|
+
paramName: 'page',
|
|
134
|
+
startValue: 1, // 第一页是 1
|
|
135
|
+
increment: 1 // 下一页加 1
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### 方案 B: 点击“下一页”按钮
|
|
141
|
+
|
|
142
|
+
最适合 SPA 或复杂的基于会话的网站。需要 `engine: 'browser'`。
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
protected override get pagination() {
|
|
146
|
+
return {
|
|
147
|
+
type: 'click-next',
|
|
148
|
+
nextButtonSelector: 'a.next-page-btn'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 步骤 3: 转换与清洗数据 (Transform)
|
|
154
|
+
|
|
155
|
+
重写 `transform` 以清洗数据。由于 `Searcher` 本身就是一个 `FetchSession`,您还可以使用 `this` 发起额外的请求(如解析重定向)。
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
protected override async transform(outputs: Record<string, any>) {
|
|
159
|
+
const results = outputs['results'] || [];
|
|
160
|
+
|
|
161
|
+
// 清洗数据或过滤
|
|
162
|
+
return results.map(item => ({
|
|
163
|
+
...item,
|
|
164
|
+
title: item.title.trim(),
|
|
165
|
+
url: new URL(item.url, 'https://blog.example.com').href
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 🧠 高级概念
|
|
171
|
+
|
|
172
|
+
### 自动分页与过滤
|
|
173
|
+
|
|
174
|
+
`Searcher` 是智能的。如果您请求 `limit: 10`,但第一页只返回了 5 条结果(或者如果您的 `transform` 过滤掉了一些结果),它会自动抓取下一页,直到满足限制。
|
|
175
|
+
|
|
176
|
+
### 用户自定义转换 (User-defined Transforms)
|
|
177
|
+
|
|
178
|
+
用户可以在调用 `search` 时提供自己的 `transform`。它会在引擎内置的转换**之后**运行。
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
await google.search('test', {
|
|
182
|
+
transform: (results) => results.filter(r => r.url.endsWith('.pdf'))
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
如果用户过滤掉了结果,自动分页逻辑会启动以抓取更多页面来满足请求的 limit。
|
|
187
|
+
|
|
188
|
+
### 标准化搜索选项
|
|
189
|
+
|
|
190
|
+
在调用 `search()` 时,您可以提供标准化的选项,搜索引擎会将其映射到特定的参数:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const results = await google.search('open source', {
|
|
194
|
+
limit: 20,
|
|
195
|
+
timeRange: 'month', // 'day', 'week', 'month', 'year'
|
|
196
|
+
// 或自定义范围:
|
|
197
|
+
// timeRange: { from: '2023-01-01', to: '2023-12-31' },
|
|
198
|
+
category: 'news', // 'all', 'images', 'videos', 'news'
|
|
199
|
+
region: 'US', // ISO 3166-1 alpha-2
|
|
200
|
+
language: 'en', // ISO 639-1
|
|
201
|
+
safeSearch: 'strict', // 'off', 'moderate', 'strict'
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
要在您自己的引擎中支持这些选项,请重写 `formatOptions` 方法:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
protected override formatOptions(options: SearchOptions): Record<string, any> {
|
|
209
|
+
const vars: Record<string, any> = {};
|
|
210
|
+
if (options.timeRange === 'day') vars.tbs = 'qdr:d';
|
|
211
|
+
// ... 将其他选项映射到模板变量
|
|
212
|
+
return vars;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
然后在您的 `template.url` 中使用这些变量:
|
|
217
|
+
`url: 'https://www.google.com/search?q=${query}&tbs=${tbs}'`
|
|
218
|
+
|
|
219
|
+
### 自定义变量
|
|
220
|
+
|
|
221
|
+
您可以向 `search()` 传递自定义变量并在模板中使用它们。
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// 调用
|
|
225
|
+
await google.search('test', { category: 'news' });
|
|
226
|
+
|
|
227
|
+
// 模板
|
|
228
|
+
url: 'https://site.com?q=${query}&cat=${category}'
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## 分页指南
|
|
232
|
+
|
|
233
|
+
### 1. 基于偏移量 (Offset-based) - 如 Google
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
protected override get pagination() {
|
|
237
|
+
return {
|
|
238
|
+
type: 'url-param',
|
|
239
|
+
paramName: 'start',
|
|
240
|
+
startValue: 0,
|
|
241
|
+
increment: 10 // 每页跳过 10 条
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
URL: `search?q=...&start=${offset}`
|
|
247
|
+
|
|
248
|
+
### 2. 基于页码 (Page-based) - 如 Bing
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
protected override get pagination() {
|
|
252
|
+
return {
|
|
253
|
+
type: 'url-param',
|
|
254
|
+
paramName: 'page',
|
|
255
|
+
startValue: 1,
|
|
256
|
+
increment: 1
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
URL: `search?q=...&page=${page}`
|
|
262
|
+
|
|
263
|
+
### 3. 基于点击 (Click-based) - SPA
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
protected override get pagination() {
|
|
267
|
+
return {
|
|
268
|
+
type: 'click-next',
|
|
269
|
+
nextButtonSelector: '.pagination .next'
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
引擎将点击此选择器并等待网络空闲,然后抓取下一批数据。
|
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Search Module
|
|
2
|
+
|
|
3
|
+
The Search module provides a high-level, class-based framework for building search engine scrapers. It is built on top of `@isdk/web-fetcher` and extends its capabilities to handle **multi-page navigation**, **session persistence**, and **result standardization**.
|
|
4
|
+
|
|
5
|
+
## 🌟 Why use the Search Module?
|
|
6
|
+
|
|
7
|
+
Building a robust search scraper involves more than just fetching a URL. You often need to:
|
|
8
|
+
|
|
9
|
+
- **Pagination**: Automatically click "Next" or modify URL parameters until you have enough results.
|
|
10
|
+
- **Session Management**: Maintain cookies and headers across multiple search queries.
|
|
11
|
+
- **Data Cleaning**: Parse raw HTML and resolve redirect links.
|
|
12
|
+
- **Flexibility**: Switch between HTTP (fast) and Browser (anti-bot) modes easily.
|
|
13
|
+
|
|
14
|
+
This module encapsulates these patterns into a reusable `Searcher` class.
|
|
15
|
+
|
|
16
|
+
## 🚀 Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. One-off Search
|
|
19
|
+
|
|
20
|
+
Use the static `Searcher.search` method for quick, disposable tasks. It automatically creates a session, fetches results, and cleans up.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Searcher } from '@isdk/web-fetcher/search';
|
|
24
|
+
import { GoogleSearcher } from '@isdk/web-fetcher/search/engines/google';
|
|
25
|
+
|
|
26
|
+
// Register the engine (only needs to be done once)
|
|
27
|
+
Searcher.register(GoogleSearcher);
|
|
28
|
+
|
|
29
|
+
// Search!
|
|
30
|
+
// The 'limit' parameter ensures we fetch enough pages to get 20 results.
|
|
31
|
+
// Note: The engine name is case-sensitive and derived from the class name (e.g., 'GoogleSearcher' -> 'Google')
|
|
32
|
+
const results = await Searcher.search('Google', 'open source', { limit: 20 });
|
|
33
|
+
|
|
34
|
+
console.log(results);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Stateful Session
|
|
38
|
+
|
|
39
|
+
Since `Searcher` extends `FetchSession`, you can instantiate it to keep cookies and storage alive across multiple requests. This is useful for authenticated searches or avoiding bot detection by behaving like a human.
|
|
40
|
+
|
|
41
|
+
**Configuration Precedence:**
|
|
42
|
+
When creating a session, options are merged in the following order:
|
|
43
|
+
1. **Template Default**: Defined in the Searcher class (highest priority for structural options).
|
|
44
|
+
2. **User Options**: Passed to the constructor (can fill missing defaults or override if allowed).
|
|
45
|
+
|
|
46
|
+
*Note: If the template sets `engine: 'auto'` (default), user-provided `engine` option will be respected.*
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Create a persistent session
|
|
50
|
+
const google = new GoogleSearcher({
|
|
51
|
+
headless: false, // Override default options (e.g., show browser)
|
|
52
|
+
proxy: 'http://my-proxy:8080',
|
|
53
|
+
timeoutMs: 30000 // Set a global timeout for requests
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// First query
|
|
58
|
+
// You can also pass runtime options to override session defaults or inject variables
|
|
59
|
+
const results1 = await google.search('term A', {
|
|
60
|
+
timeoutMs: 60000, // Override timeout just for this search
|
|
61
|
+
extraParam: 'value' // Can be used in template as ${extraParam}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Second query (reuses the same browser window/cookies)
|
|
65
|
+
const results2 = await google.search('term B');
|
|
66
|
+
} finally {
|
|
67
|
+
// Always dispose to close the browser/release resources
|
|
68
|
+
await google.dispose();
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 🛠️ Implementing a New Search Engine
|
|
73
|
+
|
|
74
|
+
To support a new website, create a class that extends `Searcher`.
|
|
75
|
+
|
|
76
|
+
### Step 1: Define the Template
|
|
77
|
+
|
|
78
|
+
To support a new website, create a class that extends `Searcher`. The engine name is automatically derived from the class name (e.g., `MyBlogSearcher` -> `MyBlog`), but you can customize it and add aliases using static properties.
|
|
79
|
+
|
|
80
|
+
The `template` property defines the "Blueprint" for your search. It's a standard `FetcherOptions` object but supports **variable injection**.
|
|
81
|
+
|
|
82
|
+
Supported variables:
|
|
83
|
+
|
|
84
|
+
- `${query}`: The search string.
|
|
85
|
+
- `${page}`: Current page number (starts at 0 or 1 based on config).
|
|
86
|
+
- `${offset}`: Current item offset (e.g., 0, 10, 20).
|
|
87
|
+
- `${limit}`: The requested limit.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { Searcher } from '@isdk/web-fetcher/search';
|
|
91
|
+
import { FetcherOptions } from '@isdk/web-fetcher/types';
|
|
92
|
+
|
|
93
|
+
export class MyBlogSearcher extends Searcher {
|
|
94
|
+
static name = 'blog'; // Custom name (case-sensitive)
|
|
95
|
+
static alias = ['myblog', 'news'];
|
|
96
|
+
|
|
97
|
+
protected get template(): FetcherOptions {
|
|
98
|
+
return {
|
|
99
|
+
engine: 'http', // Use 'browser' if the site has anti-bot
|
|
100
|
+
// Dynamic URL with variables
|
|
101
|
+
url: 'https://blog.example.com/search?q=${query}&page=${page}',
|
|
102
|
+
actions: [
|
|
103
|
+
{
|
|
104
|
+
id: 'extract',
|
|
105
|
+
storeAs: 'results', // MUST store results here
|
|
106
|
+
params: {
|
|
107
|
+
type: 'array',
|
|
108
|
+
selector: 'article.post',
|
|
109
|
+
items: {
|
|
110
|
+
title: { selector: 'h2' },
|
|
111
|
+
url: { selector: 'a', attribute: 'href' }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Step 2: Configure Pagination
|
|
122
|
+
|
|
123
|
+
Tell the `Searcher` how to navigate to the next page. Implement the `pagination` getter.
|
|
124
|
+
|
|
125
|
+
#### Option A: URL Parameters (Offset/Page)
|
|
126
|
+
|
|
127
|
+
Best for stateless HTTP scraping.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
protected override get pagination() {
|
|
131
|
+
return {
|
|
132
|
+
type: 'url-param',
|
|
133
|
+
paramName: 'page',
|
|
134
|
+
startValue: 1, // First page is 1
|
|
135
|
+
increment: 1 // Add 1 for next page
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Option B: Click "Next" Button
|
|
141
|
+
|
|
142
|
+
Best for SPAs or complex session-based sites. Requires `engine: 'browser'`.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
protected override get pagination() {
|
|
146
|
+
return {
|
|
147
|
+
type: 'click-next',
|
|
148
|
+
nextButtonSelector: 'a.next-page-btn'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Step 3: Transform & Clean Data
|
|
154
|
+
|
|
155
|
+
Override `transform` to clean data. Since `Searcher` is a `FetchSession`, you can also make extra requests (like resolving redirects) using `this`.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
protected override async transform(outputs: Record<string, any>) {
|
|
159
|
+
const results = outputs['results'] || [];
|
|
160
|
+
|
|
161
|
+
// Clean data or filter
|
|
162
|
+
return results.map(item => ({
|
|
163
|
+
...item,
|
|
164
|
+
title: item.title.trim(),
|
|
165
|
+
url: new URL(item.url, 'https://blog.example.com').href
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 🧠 Advanced Concepts
|
|
171
|
+
|
|
172
|
+
### Auto-Pagination & Filtering
|
|
173
|
+
|
|
174
|
+
The `Searcher` is smart. If you request `limit: 10`, but the first page only returns 5 results (or if your `transform` filters out results), it will automatically fetch the next page until the limit is met.
|
|
175
|
+
|
|
176
|
+
### User-defined Transforms
|
|
177
|
+
|
|
178
|
+
Users can provide their own `transform` when calling `search`. This runs **after** the engine's built-in transform.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
await google.search('test', {
|
|
182
|
+
transform: (results) => results.filter(r => r.url.endsWith('.pdf'))
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If the user filters out results, the auto-pagination logic will kick in to fetch more pages to meet the requested limit.
|
|
187
|
+
|
|
188
|
+
### Standardized Search Options
|
|
189
|
+
|
|
190
|
+
When calling `search()`, you can provide standardized options that the search engine will map to specific parameters:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const results = await google.search('open source', {
|
|
194
|
+
limit: 20,
|
|
195
|
+
timeRange: 'month', // 'day', 'week', 'month', 'year'
|
|
196
|
+
// Or custom range:
|
|
197
|
+
// timeRange: { from: '2023-01-01', to: '2023-12-31' },
|
|
198
|
+
category: 'news', // 'all', 'images', 'videos', 'news'
|
|
199
|
+
region: 'US', // ISO 3166-1 alpha-2
|
|
200
|
+
language: 'en', // ISO 639-1
|
|
201
|
+
safeSearch: 'strict', // 'off', 'moderate', 'strict'
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
To support these in your own engine, override the `formatOptions` method:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
protected override formatOptions(options: SearchOptions): Record<string, any> {
|
|
209
|
+
const vars: Record<string, any> = {};
|
|
210
|
+
if (options.timeRange === 'day') vars.tbs = 'qdr:d';
|
|
211
|
+
// ... map other options to template variables
|
|
212
|
+
return vars;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Then use these variables in your `template.url`:
|
|
217
|
+
`url: 'https://www.google.com/search?q=${query}&tbs=${tbs}'`
|
|
218
|
+
|
|
219
|
+
### Custom Variables
|
|
220
|
+
|
|
221
|
+
You can pass custom variables to `search()` and use them in your template.
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// Call
|
|
225
|
+
await google.search('test', { category: 'news' });
|
|
226
|
+
|
|
227
|
+
// Template
|
|
228
|
+
url: 'https://site.com?q=${query}&cat=${category}'
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Pagination Guide
|
|
232
|
+
|
|
233
|
+
### 1. Offset-based (e.g., Google)
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
protected override get pagination() {
|
|
237
|
+
return {
|
|
238
|
+
type: 'url-param',
|
|
239
|
+
paramName: 'start',
|
|
240
|
+
startValue: 0,
|
|
241
|
+
increment: 10 // Jump 10 items per page
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
URL: `search?q=...&start=${offset}`
|
|
247
|
+
|
|
248
|
+
### 2. Page-based (e.g., Bing)
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
protected override get pagination() {
|
|
252
|
+
return {
|
|
253
|
+
type: 'url-param',
|
|
254
|
+
paramName: 'page',
|
|
255
|
+
startValue: 1,
|
|
256
|
+
increment: 1
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
URL: `search?q=...&page=${page}`
|
|
262
|
+
|
|
263
|
+
### 3. Click-based (SPA)
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
protected override get pagination() {
|
|
267
|
+
return {
|
|
268
|
+
type: 'click-next',
|
|
269
|
+
nextButtonSelector: '.pagination .next'
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The engine will click this selector and wait for network idle before scraping the next batch.
|