Skip to content

Latest commit

 

History

History
757 lines (589 loc) · 41 KB

File metadata and controls

757 lines (589 loc) · 41 KB

Nitpicker アーキテクチャ


1. プロジェクト構成

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.jsondependencies にも追加すること。

Note: @d-zero/dealer は上図では crawler と report-google-sheets への接続のみ表示しているが、cli と core も Lanes 型のインポートのために依存している。


2. 全体データフロー

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 圧縮"]
Loading

3. パッケージ詳細

@d-zero/beholder

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

@nitpicker/crawler

オーケストレーター + 型定義 + ユーティリティ + アーカイブストレージ。

主要クラス:

  • 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/query

.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/crawlerArchive, ArchiveAccessor を使用)

@nitpicker/mcp-server

Model Context Protocol サーバー。AI アシスタント(Claude 等)から .nitpicker アーカイブを直接クエリするための 14 ツールを提供。

構成:

  • mcp-server.ts: createServer() で MCP Server インスタンスを構築。低レベル Server API を使用(McpServer + Zod スキーマの深い型インスタンス化問題を回避)
  • tool-definitions.ts: 14 ツールの JSON Schema 定義

バイナリ: nitpicker-mcp(stdio トランスポート)

依存: @modelcontextprotocol/sdk, @nitpicker/query

@nitpicker/cli

@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)を提供

4. Crawler の詳細

LinkList のライフサイクル

URL 発見 → add(url)     → pending セット
deal() で選択           → progress(url) → progress セット
スクレイプ完了          → done(url)     → done セット + Link オブジェクト生成

LinkList.done() の処理:

  1. isExternal 判定: !scope.has(url.hostname)
  2. isLowerLayer 判定: スコープ URL とのパス配列先頭一致
  3. isPage 判定: !isExternal && isLowerLayer && isHTTP && hasResponse && isHTML && !isError
  4. isPage = truecompletePages カウント増加

終了判定: 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 のみ

deal() コールバック内の処理順序

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 設定済み)→ スクレイプ

dealer 統合

  • @d-zero/dealerdeal() がスケジューリングと並列制御を担当
  • 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 を保持し、signal getter で AbortSignal を公開
  • CrawlerOrchestrator のコンストラクタで archiveerror イベントを監視し、アーカイブエラー発生時にも Crawler.abort() を呼び出す
  • CLI の killed() ハンドラでは abort() 後に garbageCollect()(ゾンビ Chromium プロセスの終了)→ process.exit() を実行

主要定数

定数 説明
MAX_PROCESS_LENGTH 10 最大並列プロセス数

5. Scraper の詳細

HEAD リクエスト(fetch-destination.ts)

fetchDestination({ url, isExternal, userAgent?, method?, options? })
  ├── キャッシュ確認(cacheMap)
  ├── 10秒タイムアウト
  └── follow-redirects で HTTP リクエスト
      ├── hostname + port を分離して指定
      ├── User-Agent ヘッダー付与(設定時のみ)
      ├── 405/501/503 → GET にフォールバック
      └── redirectPaths を記録

ブラウザスクレイプ(scraper.ts)

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(): 除外キーワードチェック

メタ情報抽出(dom-evaluation.ts:getMeta)

フィールド セレクタ プロパティ
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

キーワード除外(keyword-check.ts)

excludeKeywords の各文字列を strToRegex() で正規表現に変換し、HTML 全体に対して test() する。マッチしたら呼び出し元(scraper.ts)が ScrapeResulttype: 'ignoreAndSkip' で返却し、changePhasename: 'ignoreAndSkip')を emit する。


6. Archive DB スキーマ

pages テーブル

カラム 説明
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 順序

anchors テーブル

カラム 説明
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() vs getPagesWithRefs()

メソッド リダイレクト アンカー リファラー
getPages(filter?) ロードする ロードしない ロードしない
getPagesWithRefs() ロードする ロードする ロードする

getPages()getRedirectsForPages()redirectFrom を一括ロードする。getAnchors() は DB に都度クエリする(遅い)。

PageFilter

フィルタ 条件
'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
なし 全件

7. Analyze の詳細

データフロー

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 圧縮)
Loading

並列処理の設計

  • 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)。


8. Report の詳細

データフロー

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()
Loading

生成可能なシート

シート名 内容
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)。


9. Predictive Pagination

連番 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 に保存"]
Loading

アルゴリズム

  1. パターン検出: URL をトークン(パスセグメント + クエリ値)に分解し、前回 URL と比較。差分が単一トークンかつ整数の場合のみ検出
  2. URL 生成: 検出したステップ(差分値)を元に、並列数分の未来ページ URL を生成
  3. 結果フィルタ: 予測 URL のスクレイプ結果が 4xx/5xx/error/skip なら破棄
  4. cascade 防止: paginationCtx で予測 URL から更なる予測生成を抑制

実装詳細は crawler/detect-pagination-pattern.ts, crawler/generate-predicted-urls.ts, crawler/should-discard-predicted.ts の JSDoc を参照。


10. URL 処理の重要な仕様

実装詳細は crawler/utils/url/ 配下の各関数の JSDoc を参照。

isLowerLayer(スコープ判定)

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 となりスクレイプされない。

parseUrl の特殊処理

  • disableQueries=true → クエリ文字列を完全削除
  • PHPSESSID パラメータは自動削除
  • 複数スラッシュ(//)は単一に正規化
  • withoutHashAndAuth: DB 保存用(認証情報・ハッシュなし)
  • withoutHash: クローラー内部用(認証情報あり、ハッシュなし)

excludeUrls(URL プレフィックス除外)

excludeUrls は URL プレフィックスのリストで、url.href.startsWith(prefix) による先頭マッチで判定する。 デフォルトでソーシャルメディアの共有エンドポイント等が含まれ、--exclude-url で追加可能。 パスの glob パターンを使う excludes とは異なり、スキーム・ホスト名を含むフル URL に対してマッチする。

pathMatch(除外パターン)

micromatch による glob マッチ。URL の pathname に対して適用。

pathMatch('/blog/2020/01', '/blog/*')    → true
pathMatch('/blog/2020/01', '/blog/**/*') → true
pathMatch('/about', '/blog/*')           → false

normalizeToArray(カンマ区切り正規化)

--exclude 等の CLI フラグはカンマ区切りで複数パターンを指定可能。 normalizeToArray() がブレース展開({html,php})内のカンマを保持しつつ、トップレベルのカンマで分割する。

normalizeToArray('/blog/**/*,/facility/**/*')
  → ['/blog/**/*', '/facility/**/*']

normalizeToArray('/blog/*.{html,php},/admin/*')
  → ['/blog/*.{html,php}', '/admin/*']

11. エラーハンドリング

フェーズ エラー 処理
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 でフォールバック値

CLI 終了コード

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 を省略する。


12. E2E テスト構成

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.tsmaxWorkers: 1

テスト用 crawl ヘルパーのデフォルトオプション:

interval: 0             # 待機なし
parallels: 1            # 直列実行
image: false            # 画像取得なし

13. 外部依存パッケージ(@d-zero/*

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.jsontoken.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 形式でインポートすること