Lerna + Yarn Workspaces によるモノレポ。
packages/
├── @nitpicker/
│ ├── cli # 統合 CLI(crawl / analyze / report コマンド)
│ ├── crawler # オーケストレーター + 型定義 + ユーティリティ + アーカイブ
│ ├── core # Nitpicker プラグインシステム
│ ├── types # 共有型定義
│ ├── query # アーカイブクエリ API(SQL レベルのフィルタ・集計)
│ ├── mcp-server # MCP サーバー(AI アシスタントからのアーカイブクエリ)
│ ├── analyze-* # 各種 analyze プラグイン
│ └── report-google-sheets # Google Sheets レポーター
└── test-server/ # E2Eテスト用 Hono サーバー
@d-zero/beholder(外部)
↑
└── crawler ── @nitpicker/cli ← @d-zero/roar(外部)
↑ ↑ ↑ ↑ ↑
│ │ core │ report-google-sheets ← @d-zero/google-sheets(外部)
│ │ ↑ │ ↑
│ │ analyze-* プラグイン │
│ └── query │
│ ↑ │
│ mcp-server ← @modelcontextprotocol/sdk
└── @d-zero/dealer(外部)──┘
Note: CLI は analyze プラグインに直接依存する(
npx実行時のモジュール解決のため)。新規 analyze プラグイン追加時は@nitpicker/cli/package.jsonのdependenciesにも追加すること。Note:
@d-zero/dealerは上図では crawler と report-google-sheets への接続のみ表示しているが、cli と core もLanes型のインポートのために依存している。
flowchart TD
User["ユーザー(CLI / API)"] --> Crawling["CrawlerOrchestrator.crawling(urls, options)"]
Crawling --> Archive["Archive.create()<br/>SQLite DB を tmpDir に作成"]
Crawling --> Crawler["Crawler(options)"]
Crawler --> Scope["scope 解析(Map<hostname, URL[]>)"]
Crawler --> LinkList["LinkList に開始 URL を追加"]
Crawler --> Deal["deal()(@d-zero/dealer)"]
Deal --> RobotsCheck["robots.txt チェック(RobotsChecker)"]
RobotsCheck --> Checks["除外チェック / fetchExternal チェック"]
Deal --> Push["push() で発見した URL を動的にキューに追加"]
Deal --> Beholder["Scraper(@d-zero/beholder)<br/>インプロセス実行"]
Beholder --> Head["HEAD リクエスト(User-Agent 付き)"]
Beholder --> Puppeteer["Puppeteer でページ取得<br/>(ブラウザは Crawler が管理)"]
Beholder --> DOM["DOM からアンカー・メタ・画像を抽出"]
Beholder --> Keyword["キーワード除外チェック"]
Beholder --> Result["ScrapeResult を返却(戻り値)"]
Result --> Done["LinkList.done() でリンク完了処理"]
Result --> Save["Archive にページデータ保存"]
Crawling --> Write["CrawlerOrchestrator.write()"]
Write --> ArchiveWrite["Archive.write()<br/>snapshot を zip 圧縮 → tmpDir を .nitpicker ファイルに tar 圧縮"]
Puppeteer ベースのスクレイパー。インプロセスで実行され、戻り値ベースの API を提供。
自己完結型で、型定義・ユーティリティ関数を内部に持ち、@d-zero/shared に直接依存。
主要クラス:
Scraper: スクレイピングロジック(scrapeStart()がScrapeResultを返す)
API の特徴:
scrapeStart()はScrapeResultを直接返す(イベント経由ではない)- ストリーミングイベント(
changePhase,resourceResponse)のみ emit - Page オブジェクトは外部から注入(ブラウザ管理は呼び出し元が担当)
スクレイピングフェーズ:
scrapeStart → openPage → loadDOMContent → getHTML → waitNetworkIdle
→ getAnchors → getMeta
→ extractImages → [setViewport → waitImageLoad → getImages](デバイスプリセットごとにループ)
→ scrapeEnd
オーケストレーター + 型定義 + ユーティリティ + アーカイブストレージ。
主要クラス:
CrawlerOrchestrator: エントリポイント。CrawlerOrchestrator.crawling(),CrawlerOrchestrator.resume()Crawler: リンク管理・スクレイプスケジューリングLinkList: URL キュー管理(pending → progress → done)Archive: アーカイブの作成・再開・書き出しArchiveAccessor: 読み取り専用アクセサ(getPages,getPagesWithRefsなど)Page: ページデータラッパー
内部モジュール構造:
crawler/src/
├── utils/ # 型定義 + ユーティリティ
│ ├── types/ # ExURL, PageData, Link, CrawlerError 等
│ ├── array/ # eachSplitted
│ ├── object/ # cleanObject
│ └── error/ # DOMEvaluationError, ErrorEmitter
├── archive/ # SQLite アーカイブストレージ
│ ├── filesystem/ # 1関数1ファイル(16ファイル)+ tar, untar
│ └── ... # archive, archive-accessor, database, init-schema, limited-page-ids, redirect-table, get-json, page, resource, safe-path, types
├── crawler/ # Crawler エンジン
│ ├── crawler.ts # Crawler クラス
│ ├── link-list.ts # URL キュー管理
│ ├── types.ts # CrawlerOptions, CrawlerEventTypes, PaginationPattern
│ ├── should-skip-url.ts # URL 除外判定
│ ├── is-external-url.ts # 外部 URL 判定
│ ├── inject-scope-auth.ts # スコープ認証注入
│ ├── find-best-matching-scope.ts # スコープマッチング
│ ├── is-in-any-lower-layer.ts # 下位レイヤー判定
│ ├── handle-scrape-end.ts # スクレイプ成功ハンドラ
│ ├── handle-ignore-and-skip.ts # スキップハンドラ
│ ├── handle-resource-response.ts # リソースレスポンスハンドラ
│ ├── handle-scrape-error.ts # スクレイプエラーハンドラ
│ ├── detect-pagination-pattern.ts # ページネーション検出
│ ├── generate-predicted-urls.ts # 予測 URL 生成
│ ├── should-discard-predicted.ts # 予測結果破棄判定
│ ├── decompose-url.ts # URL トークン分解
│ ├── reconstruct-url.ts # URL 再構築
│ ├── fetch-destination.ts # HTTP HEAD/GET リクエスト
│ ├── clear-destination-cache.ts # キャッシュクリア
│ ├── destination-cache.ts # リクエストキャッシュ
│ ├── fetch-robots-txt.ts # robots.txt 取得・パース
│ ├── robots-checker.ts # robots.txt 準拠チェッカー(origin 別キャッシュ)
│ ├── format-crawl-progress.ts # deal() 進捗表示のフォーマッタ
│ └── ... # link-to-page-data, protocol-agnostic-key, net-timeout-error
├── crawler.ts # バレルエクスポート(パッケージ公開 API)
├── crawler-orchestrator.ts # CrawlerOrchestrator
├── debug.ts # デバッグログユーティリティ
├── resolve-output-path.ts # 出力パス解決・検証
├── types.ts # CrawlEvent インターフェース
└── write-queue.ts # Archive 書き込み直列化キュー
.nitpicker アーカイブファイルに対する SQL レベルのクエリ API。大規模データセット(10,000+ ページ、500,000+ レコード)向けに最適化。
主要クラス・関数:
ArchiveManager: アーカイブのライフサイクル管理(open / get / close / closeAll)。同一ファイルの重複オープンは参照カウントで管理し、untar を再実行しないlistPages: ページ一覧取得(ステータス・メタデータ欠損・URL パターンなどでフィルタ)getSummary: サイト全体の統計(ページ数、ステータス分布、メタデータ充足率)getPageDetail: 単一ページの詳細情報(メタデータ、アウトバウンド/インバウンドリンク、リダイレクト元)getPageHtml: HTML スナップショット取得(truncation サポート)listLinks: リンク分析(broken / external / orphaned)listResources: サブリソース一覧(CSS, JS, 画像、フォント)listImages: 画像一覧(alt 欠損、寸法欠損、オーバーサイズ検出)getViolations: 分析プラグインの違反データ取得findDuplicates: 重複タイトル・説明の検出findMismatches: メタデータ不一致の検出(canonical, og:title, og:description)getResourceReferrers: リソースを参照しているページの特定checkHeaders: セキュリティヘッダーチェック(CSP, X-Frame-Options, X-Content-Type-Options, HSTS)
依存: @nitpicker/crawler(Archive, ArchiveAccessor を使用)
Model Context Protocol サーバー。AI アシスタント(Claude 等)から .nitpicker アーカイブを直接クエリするための 14 ツールを提供。
構成:
mcp-server.ts:createServer()で MCP Server インスタンスを構築。低レベルServerAPI を使用(McpServer+ Zod スキーマの深い型インスタンス化問題を回避)tool-definitions.ts: 14 ツールの JSON Schema 定義
バイナリ: nitpicker-mcp(stdio トランスポート)
依存: @modelcontextprotocol/sdk, @nitpicker/query
@d-zero/roar ベースの統合 CLI。5つのサブコマンドを提供。全 analyze プラグインを dependencies に含んでおり、npx 実行時に @nitpicker/core の動的 import() がプラグインモジュールを解決できるようにしている。
npx @nitpicker/cli crawl <URL>: Webサイトをクロールして.nitpickerファイルを生成npx @nitpicker/cli analyze <file>:.nitpickerファイルに対して analyze プラグインを実行。--search-keywords,--axe-lang等のフラグで設定ファイルのプラグイン設定を上書き可能(buildPluginOverrides()→Nitpicker.setPluginOverrides()経由)npx @nitpicker/cli report <file>:.nitpickerファイルから Google Sheets レポートを生成npx @nitpicker/cli pipeline <URL>: crawl → analyze → report を直列実行。startCrawl()でアーカイブパスを取得し、そのパスをanalyze()とreport()に引き渡す。--sheet指定時のみ report ステップを実行npx @nitpicker/cli query <file> <sub-command>:.nitpickerファイルに対してクエリを実行し、結果を JSON で出力。@nitpicker/queryの全関数を CLI から利用可能。12 のサブコマンド(summary,pages,page-detail,html,links,resources,images,violations,duplicates,mismatches,headers,resource-referrers)を提供
URL 発見 → add(url) → pending セット
deal() で選択 → progress(url) → progress セット
スクレイプ完了 → done(url) → done セット + Link オブジェクト生成
LinkList.done() の処理:
isExternal判定:!scope.has(url.hostname)isLowerLayer判定: スコープ URL とのパス配列先頭一致isPage判定:!isExternal && isLowerLayer && isHTTP && hasResponse && isHTML && !isErrorisPage = true→completePagesカウント増加
終了判定: deal() が全アイテムの処理完了で resolve → crawlEnd イベント emit
Archive 書き込みの直列化: CrawlerOrchestrator は複数のイベントハンドラ(page, externalPage, skip, response, responseReferrers, error)から Archive に非同期書き込みを行う。高並列度で SQLite の書き込みロック競合を防ぐため、すべての書き込みは WriteQueue(Promise チェーンベースの FIFO キュー)で直列化される。crawlEnd 時には WriteQueue.drain() で未完了の書き込みを全て待機してからクロール完了とする。
発見したアンカーについて:
├── recursive=true の場合:
│ ├── isLowerLayer → LinkList.add(url) # フルスクレイプ
│ └── isExternal && fetchExternal → add(url, { metadataOnly: true })
│
└── recursive=false の場合:
└── add(url, { metadataOnly: true }) # HEAD のみ
URL を deal() で受け取り:
1. robots.txt チェック → 拒否なら skip イベント emit + return
2. shouldSkipUrl(excludes / excludeUrls)→ マッチなら skip
3. fetchExternal チェック → 外部 URL で無効なら externalPage emit + return
4. HEAD プリフライト → 到達不能なら error
5. metadataOnly / 非 HTML → ブラウザなしで結果返却
6. HTML → Puppeteer 起動(User-Agent 設定済み)→ スクレイプ
@d-zero/dealerのdeal()がスケジューリングと並列制御を担当intervalオプションでリクエスト間の待機時間を設定可能- スクレイピングはインプロセス(
@d-zero/beholder)で実行。各 URL ごとにブラウザを起動・終了 push()で発見した新 URL を動的にキューに追加onPushコールバックでwithoutHashAndAuthによる重複排除signalオプションでAbortSignalを渡し、中断時に新規ワーカーの起動を停止
CLI シグナルハンドラ(SIGINT / SIGHUP 等)
→ CrawlerOrchestrator.abort()
→ Crawler.abort()
→ AbortController.abort()
→ deal() の signal オプション経由で新規ワーカー起動を停止
→ 実行中のワーカーは正常完了まで継続
→ 全ワーカー完了後 deal() が resolve → crawlEnd イベント emit
Crawlerは内部にAbortControllerを保持し、signalgetter でAbortSignalを公開CrawlerOrchestratorのコンストラクタでarchiveのerrorイベントを監視し、アーカイブエラー発生時にもCrawler.abort()を呼び出す- CLI の
killed()ハンドラではabort()後にgarbageCollect()(ゾンビ Chromium プロセスの終了)→process.exit()を実行
| 定数 | 値 | 説明 |
|---|---|---|
MAX_PROCESS_LENGTH |
10 | 最大並列プロセス数 |
fetchDestination({ url, isExternal, userAgent?, method?, options? })
├── キャッシュ確認(cacheMap)
├── 10秒タイムアウト
└── follow-redirects で HTTP リクエスト
├── hostname + port を分離して指定
├── User-Agent ヘッダー付与(設定時のみ)
├── 405/501/503 → GET にフォールバック
└── redirectPaths を記録
scrapeStart(url, page, options)
├── #fetchData(url, page):
│ ├── page.goto(url)
│ ├── リダイレクトチェーン追跡(Puppeteer redirectChain)
│ ├── contentType チェック → 非HTML なら早期リターン
│ ├── waitForNavigation('domcontentloaded', 5s)
│ ├── HTML + title 取得
│ ├── metadataOnly=true → ここでリターン(アンカー・画像なし)
│ ├── waitForNavigation('networkidle0', 5s)
│ ├── getAnchorList(): <a>, <area> から href 抽出
│ ├── getMeta(): メタ情報抽出
│ └── #fetchImages()(オプション、@retryable fallback:[]):
│ └── デバイスプリセットごとにループ(desktop-compact, mobile-small):
│ ├── try-catch で各プリセットを独立実行(部分結果を許容)
│ ├── beforePageScan(): viewport 変更 + リロード + スクロール
│ ├── waitForFunction(): lazy 画像ロード完了待ち
│ └── getImageList(): 画像データ取得
└── keywordCheck(): 除外キーワードチェック
| フィールド | セレクタ | プロパティ |
|---|---|---|
| title | title |
textContent |
| lang | html |
lang |
| description | meta[name="description"] |
content |
| keywords | meta[name="keywords"] |
content |
| noindex/nofollow/noarchive | meta[name="robots"] |
content をパース |
| canonical | link[rel="canonical"] |
href |
| alternate | link[rel="alternate"] |
href |
| og:type, og:title, etc. | meta[property="og:*"] |
content |
| twitter:card | meta[name="twitter:card"] |
content |
excludeKeywords の各文字列を strToRegex() で正規表現に変換し、HTML 全体に対して test() する。マッチしたら呼び出し元(scraper.ts)が ScrapeResult を type: 'ignoreAndSkip' で返却し、changePhase(name: 'ignoreAndSkip')を emit する。
| カラム | 型 | 説明 |
|---|---|---|
| id | INTEGER PK | 自動採番 |
| url | VARCHAR(8190) UNIQUE | URL 文字列 |
| redirectDestId | INTEGER FK → pages.id | リダイレクト先ページID |
| scraped | BOOLEAN | スクレイプ済みか |
| isTarget | BOOLEAN | ターゲットページか |
| isExternal | BOOLEAN | 外部ページか |
| status | INTEGER | HTTP ステータスコード |
| statusText | TEXT | |
| contentType | TEXT | |
| contentLength | INTEGER | |
| responseHeaders | TEXT (JSON) | |
| lang | TEXT | <html lang> |
| title | TEXT | <title> |
| description | TEXT | meta description |
| keywords | TEXT | meta keywords |
| noindex | BOOLEAN | robots noindex |
| nofollow | BOOLEAN | robots nofollow |
| noarchive | BOOLEAN | robots noarchive |
| canonical | TEXT | link canonical |
| alternate | TEXT | link alternate |
| og_type, og_title, og_site_name, og_description, og_url, og_image | TEXT | Open Graph |
| twitter_card | TEXT | Twitter Card |
| html | TEXT | HTML スナップショットの相対パス |
| isSkipped | BOOLEAN | スキップされたか |
| skipReason | TEXT | スキップ理由 |
| order | INTEGER | Natural URL Sort 順序 |
| カラム | 型 | 説明 |
|---|---|---|
| id | INTEGER PK | |
| pageId | FK → pages.id | アンカーが存在するページ |
| hrefId | FK → pages.id | リンク先ページ |
| hash | TEXT | フラグメント |
| textContent | TEXT | アンカーテキスト |
- images: pageId, src, currentSrc, alt, width/height, naturalWidth/naturalHeight, isLazy, viewportWidth, sourceCode
- resources: url, isExternal, status, statusText, contentType, contentLength, compress, cdn, responseHeaders
- resources-referrers: resourceId → resources.id, pageId → pages.id
- info: 設定情報(単一レコード、
Config型のフィールドを JSON で保存)
リダイレクトは独立テーブルではなく、pages.redirectDestId で表現:
updatePage(pageData) の処理:
redirectPaths = [...pageData.redirectPaths]
destUrl = redirectPaths.pop() # 最後の要素 = 最終宛先
redirectPaths.unshift(pageData.url) # 元URL を先頭に追加
# destUrl のページをINSERT/UPDATE(スクレイプ結果を保存)
# redirectPaths の各URL に redirectDestId = destPageId を設定
# ただし redirect === destUrl(自己リダイレクト)はスキップ
# → Basic認証チャレンジ等で同一URLへ302される場合の対策
| メソッド | リダイレクト | アンカー | リファラー |
|---|---|---|---|
getPages(filter?) |
ロードする | ロードしない | ロードしない |
getPagesWithRefs() |
ロードする | ロードする | ロードする |
getPages() は getRedirectsForPages() で redirectFrom を一括ロードする。getAnchors() は DB に都度クエリする(遅い)。
| フィルタ | 条件 |
|---|---|
'page' |
contentType='text/html' AND isTarget=1 |
'page-included-no-target' |
contentType='text/html' |
'internal-page' |
contentType='text/html' AND isExternal=0 |
'external-page' |
contentType='text/html' AND isExternal=1 |
'no-page' |
contentType IS NULL OR contentType != 'text/html' |
'internal-no-page' |
(contentType IS NULL OR != 'text/html') AND isExternal=0 |
'external-no-page' |
(contentType IS NULL OR != 'text/html') AND isExternal=1 |
| なし | 全件 |
sequenceDiagram
participant CLI as npx @nitpicker/cli analyze
participant NP as Nitpicker(@nitpicker/core)
participant Archive as Archive
participant Pool as Bounded Promise Pool(limit: 50)
participant Worker as Worker Thread
CLI->>NP: Nitpicker.open(filePath)
NP->>Archive: Archive.open({ openPluginData: true })
Archive-->>NP: Archive インスタンス
CLI->>NP: setPluginOverrides(overrides)
CLI->>CLI: selectPlugins()(--all / --plugin / TTY プロンプト / 全選択)
CLI->>NP: analyze(filter?)
NP->>NP: loadPluginSettings({}, pluginOverrides)(cosmiconfig)
NP->>NP: importModules(plugins)
NP->>Archive: getPagesWithRefs(100_000, callback)
loop ページバッチごと
par eachPage トラック(Worker スレッド)
NP->>Pool: bounded Promise pool(limit: 50)
loop 各ページ
Pool->>Worker: runInWorker(html, url, plugins)
Note over Worker: JSDOM パース + プラグイン実行
Worker-->>Pool: ReportPages(テーブルデータ + violations)
end
and eachUrl トラック(メインスレッド)
loop 各ページ × 各プラグイン
NP->>NP: mod.eachUrl({ url, isExternal })
end
end
end
NP->>Archive: setData("analysis/report", report)
NP->>Archive: setData("analysis/table", table)
NP->>Archive: setData("analysis/violations", violations)
CLI->>NP: write()
NP->>Archive: Archive.write()(tar 圧縮)
- Worker スレッド: DOM 重い解析(JSDOM + axe-core, markuplint 等)はワーカースレッドで隔離実行。プラグインのクラッシュがメインプロセスに波及しない
- Bounded Promise Pool (limit: 50): メモリ枯渇防止 + リアルタイム進捗表示のため
Promise.allではなくPromise.raceベースの bounded concurrency - Cache: URL 単位で結果をキャッシュ。部分失敗後の再実行時にスキップ可能
実装詳細は
@nitpicker/coreの JSDoc を参照(Nitpicker.analyze(),runInWorker(),page-analysis-worker.ts)。
sequenceDiagram
participant CLI as npx @nitpicker/cli report
participant GS as @nitpicker/report-google-sheets
participant Archive as Archive
participant Sheets as Google Sheets API
CLI->>GS: report(filePath, sheetUrl, credentials, config, limit, all?, silent?)
GS->>GS: authentication(credentials)(OAuth2)
GS->>Archive: getArchive(filePath) → { archive, removeSignalHandlers }
Note over GS: try/finally で cleanup を保証
GS->>GS: loadConfig(configPath)
GS->>Archive: getPluginReports(archive)
alt all=true(--all 指定 or 非TTY環境)
GS->>GS: 全シートを自動選択
else all=false
GS->>GS: enquirer プロンプト(シート選択)
end
loop 選択されたシートごと
GS->>Archive: getPagesWithRefs(limit, callback)
GS->>GS: createSheetData(Page List, Links, Resources 等)
GS->>Sheets: シートデータをアップロード
Note over GS,Sheets: silent=false 時: Lanes で進捗表示 + レート制限カウントダウン
end
GS->>GS: removeSignalHandlers()
GS->>Archive: archive.close()
| シート名 | 内容 |
|---|---|
| Page List | 全ページのメタデータ一覧 |
| Links | 全ページの HTTP ステータス・リンク情報・備考一覧 |
| Resources | ネットワークリソース一覧 |
| Images | 画像一覧(サイズ・alt・lazy 等) |
| Violations | analyze プラグインが検出した違反一覧 |
| Discrepancies | analyze プラグインの比較データ |
| Summary | サマリー |
| Referrers Relational Table | ページ → リファラーの関係テーブル |
| Resources Relational Table | ページ → リソースの関係テーブル |
実装詳細は
@nitpicker/report-google-sheetsの JSDoc を参照(report(),createSheets(), 各create-*.ts)。
連番 URL(例: /page/1, /page/2, ...)を検出し、先読みで予測的にキューへ追加する仕組み。
flowchart TD
A["新 URL を push()"] --> B{"前回 push した URL と比較"}
B -->|パターン検出| C["detectPaginationPattern()"]
B -->|パターンなし| D["通常のキュー追加"]
C --> E{"単一トークンの数値差分?"}
E -->|Yes| F["PaginationPattern を返却"]
E -->|No| D
F --> G["generatePredictedUrls(pattern, url, count)"]
G --> H["予測 URL をキューに追加"]
H --> I["deal() でスクレイプ実行"]
I --> J{"shouldDiscardPredicted(result)"}
J -->|4xx/5xx/error| K["結果を破棄"]
J -->|2xx/3xx| L["Archive に保存"]
- パターン検出: URL をトークン(パスセグメント + クエリ値)に分解し、前回 URL と比較。差分が単一トークンかつ整数の場合のみ検出
- URL 生成: 検出したステップ(差分値)を元に、並列数分の未来ページ URL を生成
- 結果フィルタ: 予測 URL のスクレイプ結果が 4xx/5xx/error/skip なら破棄
- cascade 防止:
paginationCtxで予測 URL から更なる予測生成を抑制
実装詳細は
crawler/detect-pagination-pattern.ts,crawler/generate-predicted-urls.ts,crawler/should-discard-predicted.tsの JSDoc を参照。
実装詳細は
crawler/utils/url/配下の各関数の JSDoc を参照。
isLowerLayer(target, base) はパス配列の先頭一致で判定する。
paths = URL の pathname を "/" で split した文字列配列
例:
/meta/ → paths: ['meta', ''] (末尾スラッシュ)
/meta/full → paths: ['meta', 'full']
isLowerLayer('/meta/full', '/meta/') → true (meta が一致, full は追加)
isLowerLayer('/meta/robots-noindex', '/meta/full') → false (full ≠ robots-noindex)
isLowerLayer('/meta/robots-noindex', '/meta/') → true (meta が一致)
重要: 再帰クロールで子ページを発見するには、開始 URL をディレクトリパス(末尾
/)にする必要がある。ファイルパス(例:/meta/full)を開始 URL にすると、同階層の他ページはisLowerLayer=falseとなりスクレイプされない。
disableQueries=true→ クエリ文字列を完全削除PHPSESSIDパラメータは自動削除- 複数スラッシュ(
//)は単一に正規化 withoutHashAndAuth: DB 保存用(認証情報・ハッシュなし)withoutHash: クローラー内部用(認証情報あり、ハッシュなし)
excludeUrls は URL プレフィックスのリストで、url.href.startsWith(prefix) による先頭マッチで判定する。
デフォルトでソーシャルメディアの共有エンドポイント等が含まれ、--exclude-url で追加可能。
パスの glob パターンを使う excludes とは異なり、スキーム・ホスト名を含むフル URL に対してマッチする。
micromatch による glob マッチ。URL の pathname に対して適用。
pathMatch('/blog/2020/01', '/blog/*') → true
pathMatch('/blog/2020/01', '/blog/**/*') → true
pathMatch('/about', '/blog/*') → false
--exclude 等の CLI フラグはカンマ区切りで複数パターンを指定可能。
normalizeToArray() がブレース展開({html,php})内のカンマを保持しつつ、トップレベルのカンマで分割する。
normalizeToArray('/blog/**/*,/facility/**/*')
→ ['/blog/**/*', '/facility/**/*']
normalizeToArray('/blog/*.{html,php},/admin/*')
→ ['/blog/*.{html,php}', '/admin/*']
| フェーズ | エラー | 処理 |
|---|---|---|
| HEAD リクエスト | タイムアウト(10s), ECONNREFUSED 等 | ScrapeResult.type='error'(shutdown=false) |
| ブラウザ起動 | Puppeteer 起動失敗 | ScrapeResult.type='error'(shutdown=true) |
| page.goto() | タイムアウト, ERR_NAME_NOT_RESOLVED | @retryable でリトライ後 type='error' で返却 |
| 画像抽出 | context 破壊, タイムアウト | デバイスプリセット単位で try-catch、部分結果を返却。全失敗時は fallback:[] |
| DOM 解析 | evaluate 失敗 | catch でフォールバック値 |
crawl コマンドと pipeline コマンドはエラーの種類に応じて異なる終了コードを返す:
| コード | 定数 (exit-code.ts) |
意味 |
|---|---|---|
0 |
ExitCode.Success |
成功 |
1 |
ExitCode.Fatal |
致命的エラー(引数不足、内部エラー、スコープ内ページのエラー等) |
2 |
ExitCode.Warning |
警告 — 外部リンクエラーのみ発生(クロール自体は成功) |
CrawlerError.isExternal
├── true → 外部エラー(DNS 失敗、証明書エラー等)
└── false → 内部エラー(スコープ内ページの失敗)
CrawlAggregateError
├── hasOnlyExternalErrors = true → exit 2(--strict 時は exit 1)
└── hasOnlyExternalErrors = false → exit 1
--strict フラグを指定すると、外部リンクエラーのみの場合でも exit 1(致命的)として扱う。CI/CD パイプラインで外部リンクの一時的な障害を許容したい場合は --strict を省略する。
packages/test-server/
├── src/
│ ├── server.ts # createApp(), startServer()
│ ├── routes/
│ │ ├── basic.ts # /, /about
│ │ ├── recursive.ts # /recursive/**
│ │ ├── redirect.ts # /redirect/**(301→302→200チェーン)
│ │ ├── meta.ts # /meta/**(16メタフィールド)
│ │ ├── exclude.ts # /exclude/**(パス・キーワード・URLプレフィックス除外)
│ │ ├── options.ts # /options/**(fetchExternal, disableQueries)
│ │ ├── error-status.ts # /error-status/**(4xx/5xxステータス)
│ │ ├── scope.ts # /scope/**(スコープ判定)
│ │ ├── pagination.ts # /pagination/**(ページネーション検出)
│ │ └── scroll-jack.ts # /scroll-jack/**(viewport依存リダイレクト)
│ └── __tests__/e2e/
│ ├── global-setup.ts # Hono サーバー起動/停止(port 8010)
│ ├── helpers.ts # crawl(), cleanup() ヘルパー
│ ├── await-event-emitter-shim.ts # CJS/ESM interop shim
│ ├── single-page.e2e.ts
│ ├── recursive.e2e.ts
│ ├── redirect.e2e.ts
│ ├── meta.e2e.ts
│ ├── exclude.e2e.ts
│ ├── options.e2e.ts
│ ├── archive-pipeline.e2e.ts
│ ├── config-persistence.e2e.ts
│ ├── error-status.e2e.ts
│ ├── scope.e2e.ts
│ ├── parallel-and-interval.e2e.ts
│ ├── snapshot.e2e.ts
│ ├── output-path.e2e.ts
│ ├── pagination.e2e.ts
│ └── scroll-jack.e2e.ts
テスト実行: yarn vitest run --config vitest.e2e.config.ts(maxWorkers: 1)
テスト用 crawl ヘルパーのデフォルトオプション:
interval: 0 # 待機なし
parallels: 1 # 直列実行
image: false # 画像取得なし
Nitpicker は D-ZERO が公開する以下の外部パッケージに依存している。
仕様変更やバグ調査時はこれらのパッケージを参照すること。バージョンは各パッケージの package.json を参照。
| パッケージ | 用途 | 検索キーワード |
|---|---|---|
@d-zero/beholder |
Puppeteer ベースのスクレイパーエンジン。ScrapeResult を返す |
"@d-zero/beholder" changelog |
@d-zero/dealer |
並列処理・スケジューリング。deal() 関数と Lanes 進捗表示を提供 |
"@d-zero/dealer" deal concurrent |
@d-zero/shared |
共有ユーティリティ(サブパスエクスポート形式: @d-zero/shared/parse-url 等) |
"@d-zero/shared" subpath exports |
@d-zero/roar |
CLI フレームワーク | "@d-zero/roar" command |
@d-zero/google-auth |
OAuth2 認証(credentials.json → token.json) |
"@d-zero/google-auth" oauth2 |
@d-zero/google-sheets |
Google Sheets API クライアント | "@d-zero/google-sheets" spreadsheet |
@d-zero/fs |
ファイルシステムユーティリティ | "@d-zero/fs" |
@d-zero/readtext |
テキスト読み取りユーティリティ | "@d-zero/readtext" |
@d-zero/beholder → crawler(Scraper, ScrapeResult)
@d-zero/dealer → crawler(deal() 並列制御), core・cli・report-google-sheets(Lanes 進捗表示)
@d-zero/shared → 全パッケージ(parseUrl, delay, isError, detectCompress, detectCDN)
@d-zero/roar → cli(CLI コマンド定義)
@d-zero/google-auth → report-google-sheets(OAuth2 認証)
@d-zero/google-sheets → report-google-sheets(Sheets API)
@d-zero/fs → crawler(ファイルシステムユーティリティ)
@d-zero/readtext → cli(リストファイル読み込み)
@d-zero/beholder:ScrapeResultの型が変わると crawler 全体に影響@d-zero/dealer:deal()の API が変わると crawler の並列処理に影響。Lanesの型が変わると core・cli・report-google-sheets の進捗表示に影響@d-zero/shared: サブパスエクスポートの追加・削除に注意。@d-zero/shared/parse-url形式でインポートすること