@d-zero/page-cluster 0.3.0 → 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.
- package/README.md +92 -54
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# `@d-zero/page-cluster`
|
|
2
2
|
|
|
3
|
-
大量クロール HTML の重複・類似ページを構造トークンで検出するパッケージ。CLI が主、ライブラリ関数群がオマケ。
|
|
3
|
+
大量クロール HTML の重複・類似ページを構造トークンで検出するパッケージ。HTML ページ集合を受け取って、**同一テンプレートと判定できるページ**に同じクラスタキーを振る。テキストは無視して DOM 構造だけを見るので、本文が違っても同じテンプレートを使うページ群は 1 つのクラスタにまとまる。単一サイトで数万〜十数万ページ規模のクロール成果物を、テンプレート単位に畳んで概観したいときに使う。CLI が主、ライブラリ関数群がオマケ。
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
`page-cluster` は HTML ページ集合を受け取って、**同一テンプレートと判定できるページ**に同じキーを振る。テキストは無視して DOM 構造だけを見るので、記事本文が違うが同じテンプレートを使うページ群は 1 つのクラスタにまとまる。単一サイトで数万〜十数万ページ規模のクロール成果物を、テンプレート単位に畳んで概観したいときに使う。
|
|
8
|
-
|
|
9
|
-
## Install
|
|
5
|
+
## Installation
|
|
10
6
|
|
|
11
7
|
```sh
|
|
12
8
|
yarn add @d-zero/page-cluster
|
|
@@ -14,7 +10,13 @@ yarn add @d-zero/page-cluster
|
|
|
14
10
|
|
|
15
11
|
インストールすると `page-cluster` コマンドが `node_modules/.bin/` 配下に入る。
|
|
16
12
|
|
|
17
|
-
##
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### CLI
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
page-cluster [--content-block-attribute <name>] < pages.jsonl > clusters.jsonl
|
|
19
|
+
```
|
|
18
20
|
|
|
19
21
|
**入力**: JSONL 1 行 1 ページ。フィールドは以下。`html` 以外はすべて任意(`paths` / `stylesheetHrefs` がないと粗い分類になる)。
|
|
20
22
|
|
|
@@ -34,33 +36,29 @@ yarn add @d-zero/page-cluster
|
|
|
34
36
|
{ "id": "任意の識別子", "clusterKey": "..." }
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
`jq` のワンライナーで配列を line-delimited にする典型例:
|
|
39
|
+
クローラ出力が JSON 配列の場合は `jq` で line-delimited に変換して食わせる:
|
|
40
40
|
|
|
41
41
|
```sh
|
|
42
42
|
jq -c '.[]' crawl-output.json | page-cluster > clusters.jsonl
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
#### オプション
|
|
46
46
|
|
|
47
|
-
CMS が自由編集コンテンツブロックに付与している属性名(例: `data-bgb
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
page-cluster --content-block-attribute data-bgb < pages.jsonl > clusters.jsonl
|
|
51
|
-
```
|
|
47
|
+
- `--content-block-attribute <name>` — CMS が自由編集コンテンツブロックに付与している属性名(例: `data-bgb`)が分かっている場合に指定する。指定すると比較前にその属性を持つ要素配下を無視するので、同じテンプレートで本文構成だけ違うページを混同しなくなる。唯一の site-specific なオプションで、未指定でも `<main>` / `role="main"` を起点にした自動深さキャップが常時働く(詳細は `resolve-page-cluster-keys.ts` の JSDoc を参照)
|
|
48
|
+
- `--help` / `-h` — ヘルプを表示する
|
|
49
|
+
- `--version` / `-v` — バージョンを表示する
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
#### 進捗表示
|
|
54
52
|
|
|
55
53
|
処理中は stderr に進捗を出す。stdout の JSONL 出力は影響を受けない。
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
**対話端末(TTY)**: アニメーション付きの単一ヘッダー行が in-place に書き換わり、現在のフェーズ・進捗・経過時間を表示する。
|
|
58
56
|
|
|
59
57
|
```
|
|
60
58
|
🌏 page-cluster — clustering 12/47 blocks (elapsed 23s)
|
|
61
59
|
```
|
|
62
60
|
|
|
63
|
-
**非TTY
|
|
61
|
+
**非 TTY(パイプ・ファイルリダイレクト・CI)**: `[page-cluster] ...` 形式の行を追記する。`pass0:` / `pass1:` / `pass1b:` / `stage-b:` の phase トークンを含むので `grep` / `awk` 互換。
|
|
64
62
|
|
|
65
63
|
```
|
|
66
64
|
[page-cluster] reading input pages...
|
|
@@ -74,49 +72,89 @@ page-cluster --content-block-attribute data-bgb < pages.jsonl > clusters.jsonl
|
|
|
74
72
|
|
|
75
73
|
silence したい場合は `2>/dev/null`。ログに残したい場合は `2> progress.log`。
|
|
76
74
|
|
|
77
|
-
|
|
75
|
+
### Library
|
|
76
|
+
|
|
77
|
+
サブパスエクスポート構成。import パスと提供関数の対応は以下。
|
|
78
|
+
|
|
79
|
+
| import パス | 提供関数 |
|
|
80
|
+
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `@d-zero/page-cluster` | `tokenize` — `<body>` 配下を構造トークン列に変換する低レベルプリミティブ |
|
|
82
|
+
| `@d-zero/page-cluster/resolve-page-cluster-keys` | `resolvePageClusterKeys`(非同期・ファクトリ入力・メモリ有界のメインエントリー)、`resolvePageClusterKeysFromArray`(array 入力ラッパー)、`resolvePageClusterKeysInMemory`(同期・array 入力) |
|
|
83
|
+
| `@d-zero/page-cluster/extract-landmarks` | `extractLandmarks` — 6 種の HTML5 ランドマーク(header / footer / nav / aside / form / search)を抽出 |
|
|
84
|
+
| `@d-zero/page-cluster/resolve-landmark-variant-keys` | `resolveLandmarkVariantKeys` — 特定ランドマークのデザインバリアントでページを分類 |
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { resolvePageClusterKeysFromArray } from '@d-zero/page-cluster/resolve-page-cluster-keys';
|
|
88
|
+
|
|
89
|
+
const keys = await resolvePageClusterKeysFromArray([
|
|
90
|
+
{
|
|
91
|
+
paths: ['news', '1'],
|
|
92
|
+
stylesheetHrefs: ['/a.css'],
|
|
93
|
+
html: '<body><article>one</article></body>',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
paths: ['news', '2'],
|
|
97
|
+
stylesheetHrefs: ['/a.css'],
|
|
98
|
+
html: '<body><article>two</article></body>',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
paths: ['about'],
|
|
102
|
+
stylesheetHrefs: ['/a.css'],
|
|
103
|
+
html: '<body><section>about</section></body>',
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
// keys[0] === keys[1](同一テンプレート)、keys[2] は別クラスタ
|
|
107
|
+
```
|
|
78
108
|
|
|
79
|
-
|
|
109
|
+
オプション・型・設計判断の WHY はすべて各関数の JSDoc に記載している。CLI 経由で十分な場合は読み飛ばして OK。
|
|
80
110
|
|
|
81
|
-
|
|
82
|
-
- **`resolvePageClusterKeys(pagesFactory, options?)`** — ページ集合からクラスタキーを返すメインエントリー。ファクトリ関数入力で大規模コーパスに対応
|
|
83
|
-
- **`resolvePageClusterKeysFromArray(pages, options?)`** — メモリに全ページ載る前提の array 入力ラッパー
|
|
84
|
-
- **`resolveLandmarkVariantKeys(htmlList, landmarkType, options?)`** — `header` / `footer` / `nav` / `aside` などのランドマークバリアント分類
|
|
85
|
-
- **`extractLandmarks(html)`** — 1 ページから 6 種の HTML5 ランドマーク(header / footer / nav / aside / form / search)を抽出
|
|
111
|
+
## アルゴリズム概観
|
|
86
112
|
|
|
87
|
-
|
|
113
|
+
`clusterKey` がどう決まるかを知っておくと、出力の解釈(なぜこの 2 ページが同じキーなのか)とオプションの選択がしやすくなる。実装詳細の WHY は各ソースファイルの JSDoc が正。
|
|
88
114
|
|
|
89
|
-
|
|
90
|
-
┌────────────────────────────────────────┐
|
|
91
|
-
│ Blocking (paths / stylesheet 集合) │
|
|
92
|
-
└──────────────────┬─────────────────────┘
|
|
93
|
-
│ 同じテンプレートを共有する候補群
|
|
94
|
-
▼
|
|
95
|
-
┌────────────────────────────────────────┐
|
|
96
|
-
│ Stage A: complete-linkage クラスタリング │
|
|
97
|
-
│ (ブロック内、Jaccard 距離) │
|
|
98
|
-
└──────────────────┬─────────────────────┘
|
|
99
|
-
│ 各ブロックのクラスタ代表
|
|
100
|
-
▼
|
|
101
|
-
┌────────────────────────────────────────┐
|
|
102
|
-
│ Stage B: quorum-core cross-block merge │
|
|
103
|
-
│ (ブロック境界を越えた再統合) │
|
|
104
|
-
└────────────────────────────────────────┘
|
|
105
|
-
```
|
|
115
|
+
### 全体パイプライン
|
|
106
116
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
```mermaid
|
|
118
|
+
flowchart TD
|
|
119
|
+
IN[入力ページ集合] --> P0["Pass 0: ブロッキング<br>URL パス + first-party CSS 集合 → blockKey<br>(orphan ページの再割当を含む)"]
|
|
120
|
+
P0 --> GATE{"ページ数 ≤ 20,000?"}
|
|
111
121
|
|
|
112
|
-
|
|
122
|
+
GATE -- "yes(in-memory)" --> CHROME_ALL["chrome discovery(コーパス全体)<br>ランドマーク署名の度数分布に auto-cut<br>→ グローバル chrome 除外 / ローカル chrome 再注入"]
|
|
123
|
+
CHROME_ALL --> SA_ALL["Stage A × 全ブロック<br>深さキャップ → tokenize →<br>complete-linkage + auto-cut → 包含割当"]
|
|
113
124
|
|
|
114
|
-
|
|
125
|
+
GATE -- "no(ストリーミング)" --> RES["ブロックごとにリザーバサンプリング<br>(各ブロック最大 100 ページ、決定的シード)"]
|
|
126
|
+
RES --> SA_SAMPLE["chrome discovery + Stage A<br>(サンプルのみ、ブロック単位で逐次 flush)"]
|
|
127
|
+
SA_SAMPLE --> P1B["Pass 1b: 非サンプルページを<br>max-Jaccard で最寄りクラスタへ割当"]
|
|
115
128
|
|
|
116
|
-
|
|
129
|
+
SA_ALL --> SB["Stage B: ブロック越えマージ<br>(不動点ループ、下図)"]
|
|
130
|
+
P1B --> SB
|
|
131
|
+
SB --> OUT["clusterKey を入力順に出力"]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- **Pass 0(ブロッキング)** — HTML を読まず、URL パスと first-party stylesheet 集合だけで粗く分割する。高価な構造比較を同一ブロック内に閉じ込め、コーパス全体の比較コストを O(n²) から劇的に減らす。stylesheet を持たない orphan ページは同一セクションの CSS ブロックへ再割当される
|
|
135
|
+
- **chrome discovery** — 全ページのランドマーク署名の度数分布に auto-cut を当て、閾値以上を「グローバル chrome」(サイト共通のヘッダー等)として比較から除外し、閾値未満かつ 2 ページ以上に出現するものを「ローカル chrome」(セクション固有のナビ等)としてトークン再注入する
|
|
136
|
+
- **Stage A(ブロック内クラスタリング)** — ブロックごとに直線的な処理。`<main>` の深さキャップ(候補深度を全走査して knee を探す自動選択)→ tokenize → complete-linkage 階層クラスタリング → max-gap auto-cut でカット高を決定 → 最後に包含関係にあるクラスタを吸収する包含割当(割当チェーンを辿り、循環はメンバー最大のクラスタをルートに選んで解決)
|
|
137
|
+
- **Pass 1b(ストリーミング時のみ)** — 20,000 ページ超では各ブロックをリザーバサンプリング(最大 100 ページ、ブロックキーをシードにした決定的乱数)で代表させ、サンプル外のページは Stage A 完了後に max-Jaccard で最寄りクラスタへ一括割当する。メモリ使用量はコーパス全体ではなくサンプルサイズに比例する
|
|
138
|
+
- **Stage B(ブロック越えマージ)** — ブロック分割はあくまで比較コスト削減のためなので、最後に同一テンプレートがブロックを跨いで分かれていないか再統合する。これが唯一の反復処理(次節)
|
|
139
|
+
|
|
140
|
+
### Stage B: ブロック越え統合の不動点ループ
|
|
141
|
+
|
|
142
|
+
```mermaid
|
|
143
|
+
flowchart TD
|
|
144
|
+
START["ラウンド開始(最大 10 ラウンド)"] --> CORE["現在のプール済みメンバーから再計算:<br>文書頻度 → distinctive tokens → quorum core(80%)"]
|
|
145
|
+
CORE --> FINE["fine stage(単一 union-find 上で 3 経路):<br>① complete-linkage(固定 0.8)<br>② 包含割当(0.9、チェーン走査 + サイクル解決)<br>③ shape-Jaccard(0.9、複数ページユニットのみ)"]
|
|
146
|
+
FINE --> Q1{"fine でマージ発生?"}
|
|
147
|
+
Q1 -- yes --> APPLY1["マージ適用(メンバー統合)"]
|
|
148
|
+
APPLY1 --> START
|
|
149
|
+
Q1 -- no --> L2["L2 stage:<br>L2 signature 包含 + shell 相互裏付け<br>(shell は auto-cut で自己発見)"]
|
|
150
|
+
L2 --> Q2{"L2 でマージ発生?"}
|
|
151
|
+
Q2 -- yes --> APPLY2["マージ適用"]
|
|
152
|
+
APPLY2 --> START
|
|
153
|
+
Q2 -- no --> DONE["収束 — 全ユニットのキーが不動点に到達"]
|
|
154
|
+
```
|
|
117
155
|
|
|
118
|
-
|
|
156
|
+
マージが起きるとユニットのメンバー構成が変わり、文書頻度も quorum core も変わる。そのため毎ラウンド、統合後のプールから全指標を**再計算**してマージを再試行する。fine stage・L2 stage の両方でマージが 1 件も出なくなった時点で不動点に到達したとみなして収束する(安全弁として最大 10 ラウンド。実データでは 7 ラウンド以内に収束)。L2 stage は fine stage が空振りしたラウンドでしか実行されない最後の粗い経路で、誤マージ防止のために shell(ランドマーク由来トークン)の相互裏付けを要求する。
|
|
119
157
|
|
|
120
|
-
|
|
158
|
+
### Self-tuning
|
|
121
159
|
|
|
122
|
-
|
|
160
|
+
閾値の多くは **max-gap auto-cut**(度数分布の隣接ギャップ最大の中点を境界とする)でデータから自己発見される。① Stage A のカット高、② Stage B の shell 判定、③ chrome discovery のグローバル/ローカル判定、④ Pass 0 の URL パス深さ選択、の 4 箇所で同一プリミティブを再利用しているので、サイトごとにハイパーパラメータをチューニングする必要はない。詳細は `autoCutThreshold` の JSDoc を参照。例外的に Stage B fine stage の complete-linkage だけは固定閾値 0.8 を使う(理由は `merge-cross-block-clusters.ts` の JSDoc を参照)。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-zero/page-cluster",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Clusters crawled HTML pages by DOM-structure similarity — assigns the same key to pages sharing a template, ignoring text content. CLI-first, with library APIs.",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"clean": "tsc --build --clean"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@d-zero/dealer": "1.10.
|
|
39
|
+
"@d-zero/dealer": "1.10.1",
|
|
40
40
|
"@d-zero/shared": "0.22.2",
|
|
41
41
|
"htmlparser2": "12.0.0"
|
|
42
42
|
},
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"url": "https://github.com/d-zero-dev/tools.git",
|
|
46
46
|
"directory": "packages/@d-zero/page-cluster"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "9c1e3da176d39972eae9903a979e942a59515229"
|
|
49
49
|
}
|