@d-zero/beholder 0.1.28 → 2.0.0
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/CHANGELOG.md +15 -0
- package/README.md +172 -477
- package/dist/debug.d.ts +4 -1
- package/dist/debug.js +5 -2
- package/dist/dom-evaluation.d.ts +72 -14
- package/dist/dom-evaluation.js +169 -43
- package/dist/index.d.ts +20 -3
- package/dist/index.js +15 -3
- package/dist/is-error.d.ts +8 -0
- package/dist/is-error.js +10 -0
- package/dist/keyword-check.d.ts +5 -3
- package/dist/keyword-check.js +5 -3
- package/dist/parse-url.d.ts +14 -0
- package/dist/parse-url.js +23 -0
- package/dist/scraper.d.ts +39 -13
- package/dist/scraper.js +300 -263
- package/dist/types.d.ts +286 -214
- package/dist/types.js +6 -0
- package/package.json +7 -10
- package/src/debug.ts +5 -2
- package/src/dom-evaluation.ts +195 -65
- package/src/index.ts +27 -3
- package/src/is-error.spec.ts +33 -0
- package/src/is-error.ts +10 -0
- package/src/keyword-check.spec.ts +45 -4
- package/src/keyword-check.ts +5 -3
- package/src/parse-url.spec.ts +35 -0
- package/src/parse-url.ts +26 -0
- package/src/scraper.ts +338 -300
- package/src/types.ts +345 -258
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/events.d.ts +0 -32
- package/dist/events.js +0 -15
- package/dist/fetch-destination.d.ts +0 -8
- package/dist/fetch-destination.js +0 -145
- package/dist/net-timeout-error.d.ts +0 -3
- package/dist/net-timeout-error.js +0 -3
- package/dist/sub-process-runner.d.ts +0 -12
- package/dist/sub-process-runner.js +0 -180
- package/dist/sub-process.d.ts +0 -1
- package/dist/sub-process.js +0 -67
- package/dist/utils.d.ts +0 -16
- package/dist/utils.js +0 -69
- package/src/events.ts +0 -21
- package/src/fetch-destination.ts +0 -173
- package/src/net-timeout-error.ts +0 -3
- package/src/sub-process-runner.ts +0 -220
- package/src/sub-process.ts +0 -86
- package/src/utils.ts +0 -89
package/README.md
CHANGED
|
@@ -1,602 +1,297 @@
|
|
|
1
1
|
# `@d-zero/beholder`
|
|
2
2
|
|
|
3
|
-
Web
|
|
3
|
+
Puppeteer を使用してWebページをスクレイピングし、メタデータ・リンク・画像・ネットワークリソースを収集するライブラリです。
|
|
4
4
|
|
|
5
|
-
**名前の由来**: **[Beholder](https://forgottenrealms.fandom.com/wiki/Beholder)**
|
|
5
|
+
**名前の由来**: **[Beholder](https://forgottenrealms.fandom.com/wiki/Beholder)** という名前は、多くの目を持つ神話上の生物に由来しています。この生物のように、このツールはWebページを正確かつ徹底的にスクレイピングして記録し、詳細を見逃しません。
|
|
6
6
|
|
|
7
7
|
## インストール
|
|
8
8
|
|
|
9
|
-
```bash
|
|
10
|
-
npm install @d-zero/beholder
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
または
|
|
14
|
-
|
|
15
9
|
```bash
|
|
16
10
|
yarn add @d-zero/beholder
|
|
17
11
|
```
|
|
18
12
|
|
|
19
13
|
## 概要
|
|
20
14
|
|
|
21
|
-
`@d-zero/beholder` は、Puppeteer
|
|
22
|
-
|
|
23
|
-
## エクスポートされるAPI
|
|
15
|
+
`@d-zero/beholder` は、Puppeteer の `Page` オブジェクトを受け取り、単一ページのスクレイピングを行うインプロセス型のスクレイパーです。
|
|
24
16
|
|
|
25
|
-
|
|
17
|
+
**主な特徴:**
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import Scraper from '@d-zero/beholder';
|
|
33
|
-
```
|
|
19
|
+
- 結果は `ScrapeResult` として戻り値で返却(イベント経由ではない)
|
|
20
|
+
- ストリーミングイベント(`changePhase`, `resourceResponse`)で進捗を監視可能
|
|
21
|
+
- キーワード・パス除外によるページスキップ
|
|
22
|
+
- 複数デバイスプリセット対応のレスポンシブ画像キャプチャ
|
|
23
|
+
- ブラウザ管理は呼び出し側の責任(Scraperはページ操作のみ)
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
###### `scrapeStart(url: ExURL, options?: Partial<ScraperOptions>, isSkip?: boolean): Promise<PageData | void>`
|
|
38
|
-
|
|
39
|
-
指定されたURLのスクレイピングを開始します。
|
|
40
|
-
|
|
41
|
-
**パラメータ:**
|
|
42
|
-
|
|
43
|
-
- `url` (`ExURL`): スクレイピング対象のURL
|
|
44
|
-
- `options` (`Partial<ScraperOptions>`, オプション): スクレイピングオプション
|
|
45
|
-
- `isExternal` (`boolean`): 外部URLかどうか (デフォルト: `false`)
|
|
46
|
-
- `isGettingImages` (`boolean`): 画像情報を取得するかどうか (デフォルト: `true`)
|
|
47
|
-
- `excludeKeywords` (`string[]`): 除外するキーワードのリスト (デフォルト: `[]`)
|
|
48
|
-
- `executablePath` (`string | null`): Chromeの実行可能ファイルパス (デフォルト: `null`)
|
|
49
|
-
- `isTitleOnly` (`boolean`): タイトルのみを取得するかどうか (デフォルト: `false`)
|
|
50
|
-
- `screenshot` (`string | null`): スクリーンショットの保存パス (デフォルト: `null`)
|
|
51
|
-
- `disableQueries` (`boolean`, オプション): クエリパラメータを無効にするかどうか
|
|
52
|
-
- `isSkip` (`boolean`, オプション): スキップするかどうか (デフォルト: `false`)
|
|
53
|
-
|
|
54
|
-
**戻り値:**
|
|
25
|
+
## エクスポートされるAPI
|
|
55
26
|
|
|
56
|
-
|
|
27
|
+
### `Scraper`(デフォルトエクスポート)
|
|
57
28
|
|
|
58
|
-
|
|
29
|
+
ページレベルのスクレイパークラスです。`TypedAwaitEventEmitter` を継承しています。
|
|
59
30
|
|
|
60
31
|
```typescript
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// イベントリスナーを登録
|
|
64
|
-
scraper.on('scrapeEnd', async (event) => {
|
|
65
|
-
console.log('スクレイピング完了:', event.result);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
scraper.on('error', async (event) => {
|
|
69
|
-
console.error('エラー発生:', event.error);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// スクレイピングを開始
|
|
73
|
-
const url = parseUrl('https://example.com');
|
|
74
|
-
await scraper.scrapeStart(url, {
|
|
75
|
-
isGettingImages: true,
|
|
76
|
-
excludeKeywords: ['広告', 'スポンサー'],
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// 完了後にクリーンアップ
|
|
80
|
-
await scraper.destroy(false);
|
|
32
|
+
import Scraper from '@d-zero/beholder';
|
|
81
33
|
```
|
|
82
34
|
|
|
83
|
-
|
|
35
|
+
#### `scrapeStart(page, url, options?, isSkip?)`
|
|
84
36
|
|
|
85
|
-
|
|
37
|
+
Puppeteer ページ上でスクレイピングを実行します。
|
|
86
38
|
|
|
87
39
|
**パラメータ:**
|
|
88
40
|
|
|
89
|
-
- `
|
|
41
|
+
- `page` (`Page`) — Puppeteer ページインスタンス
|
|
42
|
+
- `url` (`ExURL`) — スクレイピング対象のURL
|
|
43
|
+
- `options` (`Partial<ScraperOptions>`, 省略可) — スクレイピングオプション
|
|
44
|
+
- `isSkip` (`boolean`, 省略可) — `true` でネットワークリクエストなしにスキップ
|
|
90
45
|
|
|
91
|
-
**戻り値:**
|
|
46
|
+
**戻り値:** `Promise<ScrapeResult>`
|
|
92
47
|
|
|
93
|
-
- `
|
|
48
|
+
- `type: "success"` — `pageData` にスクレイピング結果を格納
|
|
49
|
+
- `type: "skipped"` — `ignored` にスキップ理由を格納
|
|
50
|
+
- `type: "error"` — `error` にエラー詳細を格納
|
|
94
51
|
|
|
95
52
|
**使用例:**
|
|
96
53
|
|
|
97
54
|
```typescript
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
##### イベント
|
|
102
|
-
|
|
103
|
-
`Scraper` クラスは `TypedAwaitEventEmitter` を継承しており、以下のイベントを発行します:
|
|
55
|
+
import Scraper from '@d-zero/beholder';
|
|
56
|
+
import { parseUrl } from '@d-zero/shared/parse-url';
|
|
57
|
+
import { launch } from 'puppeteer';
|
|
104
58
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
- `scrapeEnd`: スクレイピングが完了した場合
|
|
108
|
-
- `destroyed`: スクレイパーが破棄された場合
|
|
109
|
-
- `error`: エラーが発生した場合
|
|
110
|
-
- `changePhase`: スクレイピングフェーズが変更された場合
|
|
59
|
+
const browser = await launch();
|
|
60
|
+
const page = await browser.newPage();
|
|
111
61
|
|
|
112
|
-
|
|
62
|
+
const scraper = new Scraper();
|
|
113
63
|
|
|
114
|
-
|
|
64
|
+
// 進捗イベントを監視
|
|
115
65
|
scraper.on('changePhase', async (event) => {
|
|
116
|
-
console.log(
|
|
66
|
+
console.log(`[${event.pid}] ${event.name}: ${event.message}`);
|
|
117
67
|
});
|
|
118
68
|
|
|
119
|
-
|
|
120
|
-
|
|
69
|
+
// スクレイピングを実行
|
|
70
|
+
const url = parseUrl('https://example.com');
|
|
71
|
+
const result = await scraper.scrapeStart(page, url, {
|
|
72
|
+
captureImages: true,
|
|
73
|
+
excludeKeywords: ['広告'],
|
|
74
|
+
isExternal: false,
|
|
121
75
|
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
#### `SubProcessRunner`
|
|
125
|
-
|
|
126
|
-
サブプロセスでスクレイパーを実行するためのクラスです。メモリ効率的なスクレイピングを実現します。
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
import { SubProcessRunner } from '@d-zero/beholder';
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
##### コンストラクタ
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
new SubProcessRunner(resetTime: number)
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
**パラメータ:**
|
|
139
|
-
|
|
140
|
-
- `resetTime` (`number`): サブプロセスをリセットするまでのスクレイピング回数
|
|
141
|
-
|
|
142
|
-
##### プロパティ
|
|
143
|
-
|
|
144
|
-
###### `state: 'waiting' | 'running'` (読み取り専用)
|
|
145
|
-
|
|
146
|
-
現在のサブプロセスの状態を取得します。
|
|
147
|
-
|
|
148
|
-
##### メソッド
|
|
149
|
-
|
|
150
|
-
###### `start(url: ExURL, options: ScraperOptions, isSkip: boolean, interval: number): void`
|
|
151
|
-
|
|
152
|
-
サブプロセスでスクレイピングを開始します。
|
|
153
|
-
|
|
154
|
-
**パラメータ:**
|
|
155
|
-
|
|
156
|
-
- `url` (`ExURL`): スクレイピング対象のURL
|
|
157
|
-
- `options` (`ScraperOptions`): スクレイピングオプション
|
|
158
|
-
- `isSkip` (`boolean`): スキップするかどうか
|
|
159
|
-
- `interval` (`number`): スクレイピング間隔(ミリ秒)
|
|
160
|
-
|
|
161
|
-
**例外:**
|
|
162
|
-
|
|
163
|
-
- サブプロセスが既に実行中の場合はエラーをスローします
|
|
164
|
-
|
|
165
|
-
**使用例:**
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
const runner = new SubProcessRunner(10); // 10回ごとにリセット
|
|
169
76
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
77
|
+
if (result.type === 'success') {
|
|
78
|
+
console.log('タイトル:', result.pageData?.meta.title);
|
|
79
|
+
console.log('リンク数:', result.pageData?.anchorList.length);
|
|
80
|
+
console.log('画像数:', result.pageData?.imageList.length);
|
|
81
|
+
console.log('サブリソース数:', result.resources.length);
|
|
82
|
+
}
|
|
175
83
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
isExternal: false,
|
|
180
|
-
isGettingImages: true,
|
|
181
|
-
excludeKeywords: [],
|
|
182
|
-
executablePath: null,
|
|
183
|
-
isTitleOnly: false,
|
|
184
|
-
screenshot: null,
|
|
185
|
-
},
|
|
186
|
-
false,
|
|
187
|
-
1000,
|
|
188
|
-
);
|
|
84
|
+
// クリーンアップはブラウザレベルで行う
|
|
85
|
+
await page.close();
|
|
86
|
+
await browser.close();
|
|
189
87
|
```
|
|
190
88
|
|
|
191
|
-
|
|
89
|
+
#### イベント
|
|
192
90
|
|
|
193
|
-
|
|
91
|
+
| イベント名 | 説明 |
|
|
92
|
+
| ------------------ | ---------------------------------------------- |
|
|
93
|
+
| `changePhase` | スクレイピングフェーズが遷移した場合 |
|
|
94
|
+
| `resourceResponse` | サブリソースのレスポンスがキャプチャされた場合 |
|
|
194
95
|
|
|
195
|
-
|
|
96
|
+
### `ScraperOptions`
|
|
196
97
|
|
|
197
|
-
|
|
98
|
+
| プロパティ | 型 | デフォルト | 説明 |
|
|
99
|
+
| ------------------- | ---------- | ---------- | ---------------------------------------------------------------- |
|
|
100
|
+
| `isExternal` | `boolean` | `false` | 外部URLかどうか |
|
|
101
|
+
| `captureImages` | `boolean` | `true` | 画像データを取得するかどうか |
|
|
102
|
+
| `excludeKeywords` | `string[]` | `[]` | HTML内にマッチしたらスキップするキーワード |
|
|
103
|
+
| `metadataOnly` | `boolean` | `false` | メタデータのみ取得(ブラウザスクレイピングなし) |
|
|
104
|
+
| `imageLoadTimeout` | `number` | `5000` | 画像読み込み待機のタイムアウト(ms) |
|
|
105
|
+
| `disableQueries` | `boolean` | - | URLパース時にクエリパラメータを除去するかどうか |
|
|
106
|
+
| `retries` | `number` | - | ネットワーク操作のリトライ回数 |
|
|
107
|
+
| `headCheckResult` | `PageData` | - | 事前取得したHEADチェック結果(省略時はHEADリクエストをスキップ) |
|
|
108
|
+
| `navigationTimeout` | `number` | `60000` | `page.goto()` のタイムアウト(ms) |
|
|
198
109
|
|
|
199
|
-
|
|
200
|
-
runner.destory();
|
|
201
|
-
```
|
|
110
|
+
### ユーティリティ関数
|
|
202
111
|
|
|
203
|
-
|
|
112
|
+
#### `isError(status)`
|
|
204
113
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
**使用例:**
|
|
114
|
+
HTTPステータスコードがエラーかどうかを判定します。200-399 は成功、それ以外はエラーです。
|
|
208
115
|
|
|
209
116
|
```typescript
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
###### `getUndeadPid(): number[]`
|
|
214
|
-
|
|
215
|
-
終了できなかった(ゾンビ)プロセスのPIDリストを取得します。
|
|
216
|
-
|
|
217
|
-
**戻り値:**
|
|
218
|
-
|
|
219
|
-
- `number[]`: ゾンビプロセスのPIDリスト
|
|
117
|
+
import { isError } from '@d-zero/beholder';
|
|
220
118
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
const zombiePids = runner.getUndeadPid();
|
|
225
|
-
console.log('ゾンビプロセス:', zombiePids);
|
|
119
|
+
isError(200); // false
|
|
120
|
+
isError(404); // true
|
|
226
121
|
```
|
|
227
122
|
|
|
228
|
-
|
|
123
|
+
#### `detectCompress(headers)` / `detectCDN(headers)`
|
|
229
124
|
|
|
230
|
-
-
|
|
231
|
-
- `scrapeEvent`: スクレイピングイベントが発生した場合
|
|
232
|
-
- `changePhase`: フェーズが変更された場合
|
|
233
|
-
- `error`: エラーが発生した場合
|
|
125
|
+
レスポンスヘッダーから圧縮方式・CDNプロバイダを検出します(`@d-zero/shared` からの再エクスポート)。
|
|
234
126
|
|
|
235
127
|
### 型定義
|
|
236
128
|
|
|
237
|
-
#### `
|
|
129
|
+
#### `ScrapeResult`
|
|
238
130
|
|
|
239
|
-
|
|
131
|
+
スクレイピング操作の結果を表します。
|
|
240
132
|
|
|
241
133
|
```typescript
|
|
242
|
-
type
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
isHTTP: boolean; // HTTPまたはHTTPSかどうか
|
|
249
|
-
isSecure: boolean; // HTTPSかどうか
|
|
250
|
-
username: string | null; // 認証ユーザー名
|
|
251
|
-
password: string | null; // 認証パスワード
|
|
252
|
-
hostname: string; // ホスト名
|
|
253
|
-
port: string | null; // ポート番号
|
|
254
|
-
pathname: string | null; // パス
|
|
255
|
-
paths: string[]; // パスの配列
|
|
256
|
-
depth: number; // パスの深さ
|
|
257
|
-
dirname: string | null; // ディレクトリ名
|
|
258
|
-
basename: string | null; // ベース名(拡張子なしのファイル名)
|
|
259
|
-
isIndex: boolean; // インデックスページかどうか
|
|
260
|
-
extname: string | null; // ファイル拡張子
|
|
261
|
-
query: string | null; // クエリ文字列
|
|
262
|
-
hash: string | null; // ハッシュ
|
|
134
|
+
type ScrapeResult = {
|
|
135
|
+
type: 'success' | 'skipped' | 'error';
|
|
136
|
+
pageData?: PageData;
|
|
137
|
+
resources: ResourceEntry[];
|
|
138
|
+
ignored?: { url: ExURL; matchedText: string; excludeKeywords: string[] };
|
|
139
|
+
error?: { name: string; message: string; stack?: string; shutdown: boolean };
|
|
263
140
|
};
|
|
264
141
|
```
|
|
265
142
|
|
|
266
143
|
#### `PageData`
|
|
267
144
|
|
|
268
|
-
|
|
145
|
+
スクレイピング成功時のページデータです。
|
|
269
146
|
|
|
270
147
|
```typescript
|
|
271
148
|
type PageData = {
|
|
272
|
-
url: ExURL;
|
|
273
|
-
redirectPaths: string[];
|
|
274
|
-
isTarget: boolean;
|
|
275
|
-
isExternal: boolean;
|
|
276
|
-
status: number;
|
|
277
|
-
statusText: string;
|
|
278
|
-
contentType: string | null;
|
|
279
|
-
contentLength: number | null;
|
|
280
|
-
responseHeaders: Record<string, string | string[] | undefined> | null;
|
|
281
|
-
meta: Meta;
|
|
282
|
-
anchorList: AnchorData[];
|
|
283
|
-
imageList: ImageElement[];
|
|
284
|
-
html: string;
|
|
285
|
-
isSkipped: false;
|
|
149
|
+
url: ExURL;
|
|
150
|
+
redirectPaths: string[];
|
|
151
|
+
isTarget: boolean;
|
|
152
|
+
isExternal: boolean;
|
|
153
|
+
status: number;
|
|
154
|
+
statusText: string;
|
|
155
|
+
contentType: string | null;
|
|
156
|
+
contentLength: number | null;
|
|
157
|
+
responseHeaders: Record<string, string | string[] | undefined> | null;
|
|
158
|
+
meta: Meta;
|
|
159
|
+
anchorList: AnchorData[];
|
|
160
|
+
imageList: ImageElement[];
|
|
161
|
+
html: string;
|
|
162
|
+
isSkipped: false;
|
|
286
163
|
};
|
|
287
164
|
```
|
|
288
165
|
|
|
289
166
|
#### `Meta`
|
|
290
167
|
|
|
291
|
-
|
|
168
|
+
ページの `<head>` から抽出されたメタデータです。
|
|
292
169
|
|
|
293
170
|
```typescript
|
|
294
171
|
type Meta = {
|
|
295
|
-
lang?: string;
|
|
296
|
-
title: string;
|
|
297
|
-
description?: string;
|
|
298
|
-
keywords?: string;
|
|
299
|
-
noindex?: boolean;
|
|
300
|
-
nofollow?: boolean;
|
|
301
|
-
noarchive?: boolean;
|
|
302
|
-
canonical?: string;
|
|
303
|
-
alternate?: string;
|
|
304
|
-
'og:type'?: string;
|
|
305
|
-
'og:title'?: string;
|
|
306
|
-
'og:site_name'?: string;
|
|
307
|
-
'og:description'?: string;
|
|
308
|
-
'og:url'?: string;
|
|
309
|
-
'og:image'?: string;
|
|
310
|
-
'twitter:card'?: string;
|
|
172
|
+
lang?: string;
|
|
173
|
+
title: string;
|
|
174
|
+
description?: string;
|
|
175
|
+
keywords?: string;
|
|
176
|
+
noindex?: boolean;
|
|
177
|
+
nofollow?: boolean;
|
|
178
|
+
noarchive?: boolean;
|
|
179
|
+
canonical?: string;
|
|
180
|
+
alternate?: string;
|
|
181
|
+
'og:type'?: string;
|
|
182
|
+
'og:title'?: string;
|
|
183
|
+
'og:site_name'?: string;
|
|
184
|
+
'og:description'?: string;
|
|
185
|
+
'og:url'?: string;
|
|
186
|
+
'og:image'?: string;
|
|
187
|
+
'twitter:card'?: string;
|
|
311
188
|
};
|
|
312
189
|
```
|
|
313
190
|
|
|
314
191
|
#### `AnchorData`
|
|
315
192
|
|
|
316
|
-
|
|
193
|
+
アンカー要素(`<a>` / `<area>`)のデータです。
|
|
317
194
|
|
|
318
195
|
```typescript
|
|
319
196
|
type AnchorData = {
|
|
320
|
-
href: ExURL;
|
|
321
|
-
textContent: string;
|
|
197
|
+
href: ExURL;
|
|
198
|
+
textContent: string;
|
|
199
|
+
isExternal?: boolean;
|
|
322
200
|
};
|
|
323
201
|
```
|
|
324
202
|
|
|
325
203
|
#### `ImageElement`
|
|
326
204
|
|
|
327
|
-
|
|
205
|
+
画像要素のデータです。デバイスプリセットごとにキャプチャされます。
|
|
328
206
|
|
|
329
207
|
```typescript
|
|
330
208
|
type ImageElement = {
|
|
331
|
-
src: string;
|
|
332
|
-
currentSrc: string;
|
|
333
|
-
alt: string;
|
|
334
|
-
width: number;
|
|
335
|
-
height: number;
|
|
336
|
-
naturalWidth: number;
|
|
337
|
-
naturalHeight: number;
|
|
338
|
-
isLazy: boolean;
|
|
339
|
-
viewportWidth: number;
|
|
340
|
-
sourceCode: string;
|
|
209
|
+
src: string;
|
|
210
|
+
currentSrc: string;
|
|
211
|
+
alt: string;
|
|
212
|
+
width: number;
|
|
213
|
+
height: number;
|
|
214
|
+
naturalWidth: number;
|
|
215
|
+
naturalHeight: number;
|
|
216
|
+
isLazy: boolean;
|
|
217
|
+
viewportWidth: number;
|
|
218
|
+
sourceCode: string;
|
|
341
219
|
};
|
|
342
220
|
```
|
|
343
221
|
|
|
344
|
-
#### `
|
|
222
|
+
#### `ResourceEntry`
|
|
345
223
|
|
|
346
|
-
|
|
224
|
+
ページ読み込み中にキャプチャされたサブリソースです。
|
|
347
225
|
|
|
348
226
|
```typescript
|
|
349
|
-
type
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
contentType: string; // コンテンツタイプ
|
|
354
|
-
isError: boolean; // エラーかどうか
|
|
355
|
-
request: {
|
|
356
|
-
ts: number; // タイムスタンプ
|
|
357
|
-
headers: Record<string, string>; // リクエストヘッダー
|
|
358
|
-
method: string; // HTTPメソッド
|
|
359
|
-
};
|
|
360
|
-
response?: {
|
|
361
|
-
ts: number; // タイムスタンプ
|
|
362
|
-
status: number; // ステータスコード
|
|
363
|
-
statusText: string; // ステータステキスト
|
|
364
|
-
fromCache: boolean; // キャッシュから取得したかどうか
|
|
365
|
-
headers: Record<string, string>; // レスポンスヘッダー
|
|
366
|
-
};
|
|
227
|
+
type ResourceEntry = {
|
|
228
|
+
log: NetworkLog;
|
|
229
|
+
resource: Omit<Resource, 'uid'>;
|
|
230
|
+
pageUrl: string;
|
|
367
231
|
};
|
|
368
232
|
```
|
|
369
233
|
|
|
370
|
-
#### `
|
|
371
|
-
|
|
372
|
-
リソースの情報です。
|
|
373
|
-
|
|
374
|
-
```typescript
|
|
375
|
-
type Resource = {
|
|
376
|
-
url: ExURL; // リソースURL
|
|
377
|
-
isExternal: boolean; // 外部リソースかどうか
|
|
378
|
-
isError: boolean; // エラーかどうか
|
|
379
|
-
status: number | null; // ステータスコード
|
|
380
|
-
statusText: string | null; // ステータステキスト
|
|
381
|
-
contentType: string | null; // コンテンツタイプ
|
|
382
|
-
contentLength: number | null; // コンテンツ長
|
|
383
|
-
compress: false | CompressType; // 圧縮タイプ
|
|
384
|
-
cdn: false | CDNType; // CDNタイプ
|
|
385
|
-
headers: Record<string, string | string[] | undefined> | null; // ヘッダー
|
|
386
|
-
};
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
#### `SkippedPageData`
|
|
234
|
+
#### `NetworkLog`
|
|
390
235
|
|
|
391
|
-
|
|
236
|
+
ネットワークリクエスト/レスポンスのログエントリです。
|
|
392
237
|
|
|
393
238
|
```typescript
|
|
394
|
-
type
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
239
|
+
type NetworkLog = {
|
|
240
|
+
url: ExURL;
|
|
241
|
+
status: number | null;
|
|
242
|
+
contentLength: number;
|
|
243
|
+
contentType: string;
|
|
244
|
+
isError: boolean;
|
|
245
|
+
request: { ts: number; headers: Record<string, string>; method: string };
|
|
246
|
+
response?: {
|
|
247
|
+
ts: number;
|
|
248
|
+
status: number;
|
|
249
|
+
statusText: string;
|
|
250
|
+
fromCache: boolean;
|
|
251
|
+
headers: Record<string, string>;
|
|
402
252
|
};
|
|
403
253
|
};
|
|
404
254
|
```
|
|
405
255
|
|
|
406
|
-
####
|
|
407
|
-
|
|
408
|
-
##### `ScrapeEventTypes`
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
type ScrapeEventTypes = {
|
|
412
|
-
ignoreAndSkip: {
|
|
413
|
-
pid: number | undefined;
|
|
414
|
-
url: ExURL;
|
|
415
|
-
reason: {
|
|
416
|
-
matchedText: string;
|
|
417
|
-
excludeKeywords: string[];
|
|
418
|
-
};
|
|
419
|
-
};
|
|
420
|
-
resourceResponse: {
|
|
421
|
-
pid: number | undefined;
|
|
422
|
-
url: ExURL;
|
|
423
|
-
log: NetworkLog;
|
|
424
|
-
resource: Omit<Resource, 'uid'>;
|
|
425
|
-
};
|
|
426
|
-
scrapeEnd: {
|
|
427
|
-
pid: number | undefined;
|
|
428
|
-
url: ExURL;
|
|
429
|
-
timestamp: number;
|
|
430
|
-
result: PageData;
|
|
431
|
-
};
|
|
432
|
-
destroyed: {
|
|
433
|
-
pid: number | undefined;
|
|
434
|
-
};
|
|
435
|
-
error: {
|
|
436
|
-
pid: number | undefined;
|
|
437
|
-
url: ExURL;
|
|
438
|
-
shutdown: boolean;
|
|
439
|
-
error: {
|
|
440
|
-
name: string;
|
|
441
|
-
message: string;
|
|
442
|
-
stack?: string;
|
|
443
|
-
};
|
|
444
|
-
};
|
|
445
|
-
changePhase: ChangePhaseEvent;
|
|
446
|
-
};
|
|
447
|
-
```
|
|
256
|
+
#### `Resource`
|
|
448
257
|
|
|
449
|
-
|
|
258
|
+
ネットワークリソースのメタデータです。
|
|
450
259
|
|
|
451
260
|
```typescript
|
|
452
|
-
type
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
name: string;
|
|
464
|
-
message: string;
|
|
465
|
-
stack?: string;
|
|
466
|
-
};
|
|
467
|
-
};
|
|
261
|
+
type Resource = {
|
|
262
|
+
url: ExURL;
|
|
263
|
+
isExternal: boolean;
|
|
264
|
+
isError: boolean;
|
|
265
|
+
status: number | null;
|
|
266
|
+
statusText: string | null;
|
|
267
|
+
contentType: string | null;
|
|
268
|
+
contentLength: number | null;
|
|
269
|
+
compress: false | CompressType;
|
|
270
|
+
cdn: false | CDNType;
|
|
271
|
+
headers: Record<string, string | string[] | undefined> | null;
|
|
468
272
|
};
|
|
469
273
|
```
|
|
470
274
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
#### `scraperEvent`
|
|
275
|
+
#### `ChangePhaseEvent`
|
|
474
276
|
|
|
475
|
-
|
|
277
|
+
スクレイピングライフサイクルのフェーズ遷移イベントです。
|
|
476
278
|
|
|
477
|
-
|
|
478
|
-
import { scraperEvent } from '@d-zero/beholder';
|
|
479
|
-
|
|
480
|
-
// 利用可能なイベント:
|
|
481
|
-
// - scraperEvent.ignoreAndSkip
|
|
482
|
-
// - scraperEvent.resourceResponse
|
|
483
|
-
// - scraperEvent.scrapeEnd
|
|
484
|
-
// - scraperEvent.destroyed
|
|
485
|
-
// - scraperEvent.error
|
|
486
|
-
// - scraperEvent.changePhase
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
#### `subProcessEvent`
|
|
279
|
+
主なフェーズ: `scrapeStart` → `openPage` → `loadDOMContent` → `waitNetworkIdle` → `getHTML` → `getAnchors` → `getMeta` → `extractImages` → `getImages` → `scrapeEnd`
|
|
490
280
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
import { subProcessEvent } from '@d-zero/beholder';
|
|
495
|
-
|
|
496
|
-
// 利用可能なイベント:
|
|
497
|
-
// - subProcessEvent.start
|
|
498
|
-
// - subProcessEvent.destroy
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
## 完全な使用例
|
|
502
|
-
|
|
503
|
-
### 基本的なスクレイピング
|
|
504
|
-
|
|
505
|
-
```typescript
|
|
506
|
-
import Scraper from '@d-zero/beholder';
|
|
507
|
-
import { parseUrl } from '@d-zero/shared/parse-url';
|
|
508
|
-
|
|
509
|
-
const scraper = new Scraper();
|
|
510
|
-
|
|
511
|
-
// イベントハンドラーを設定
|
|
512
|
-
scraper.on('changePhase', async (event) => {
|
|
513
|
-
console.log(`[${event.pid}] ${event.name}: ${event.message}`);
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
scraper.on('scrapeEnd', async (event) => {
|
|
517
|
-
const { result } = event;
|
|
518
|
-
console.log('タイトル:', result.meta.title);
|
|
519
|
-
console.log('ステータス:', result.status);
|
|
520
|
-
console.log('アンカー数:', result.anchorList.length);
|
|
521
|
-
console.log('画像数:', result.imageList.length);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
scraper.on('error', async (event) => {
|
|
525
|
-
console.error('エラー:', event.error.message);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
// スクレイピングを実行
|
|
529
|
-
const url = parseUrl('https://example.com');
|
|
530
|
-
const result = await scraper.scrapeStart(url, {
|
|
531
|
-
isGettingImages: true,
|
|
532
|
-
excludeKeywords: ['広告'],
|
|
533
|
-
isExternal: false,
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// クリーンアップ
|
|
537
|
-
await scraper.destroy(false);
|
|
538
|
-
```
|
|
281
|
+
#### `SkippedPageData`
|
|
539
282
|
|
|
540
|
-
|
|
283
|
+
キーワードまたはパス除外によりスキップされたページのデータです。
|
|
541
284
|
|
|
542
285
|
```typescript
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (action.type === '@@scraper/scrapeEnd') {
|
|
551
|
-
console.log('完了:', action.payload.result.url.href);
|
|
552
|
-
}
|
|
553
|
-
if (action.type === '@@scraper/error') {
|
|
554
|
-
console.error('エラー:', action.payload.error.message);
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
runner.on('changePhase', async (event) => {
|
|
559
|
-
console.log(`[${event.pid}] ${event.name}`);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
// URLリストをスクレイピング
|
|
563
|
-
const urls = [
|
|
564
|
-
'https://example.com',
|
|
565
|
-
'https://example.com/about',
|
|
566
|
-
'https://example.com/contact',
|
|
567
|
-
];
|
|
568
|
-
|
|
569
|
-
for (const urlString of urls) {
|
|
570
|
-
const url = parseUrl(urlString);
|
|
571
|
-
|
|
572
|
-
// サブプロセスが待機状態になるまで待つ
|
|
573
|
-
while (runner.state === 'running') {
|
|
574
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
runner.start(
|
|
578
|
-
url,
|
|
579
|
-
{
|
|
580
|
-
isExternal: false,
|
|
581
|
-
isGettingImages: false,
|
|
582
|
-
excludeKeywords: [],
|
|
583
|
-
executablePath: null,
|
|
584
|
-
isTitleOnly: true,
|
|
585
|
-
screenshot: null,
|
|
586
|
-
},
|
|
587
|
-
false,
|
|
588
|
-
1000,
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// 完了後にクリーンアップ
|
|
593
|
-
runner.destory();
|
|
286
|
+
type SkippedPageData = {
|
|
287
|
+
isSkipped: true;
|
|
288
|
+
url: ExURL;
|
|
289
|
+
matched:
|
|
290
|
+
| { type: 'keyword'; text: string; excludeKeywords: string[] }
|
|
291
|
+
| { type: 'path'; excludes: string[] };
|
|
292
|
+
};
|
|
594
293
|
```
|
|
595
294
|
|
|
596
295
|
## ライセンス
|
|
597
296
|
|
|
598
297
|
MIT
|
|
599
|
-
|
|
600
|
-
## 作者
|
|
601
|
-
|
|
602
|
-
D-ZERO
|