Crawling & Ranking Platform — Observation & Design
公開サイトを定期クローリングして「ダウンロード数の伸び(velocity)」から人気コンテンツをランキング化する仕組みの、サイト構造の網羅観測と最適設計。生データを定期保存して削除せず、その意味づけ(分析)は後からいかようにも変えられる二層アーキテクチャを採用する。
先に要点だけ。詳細は各セクションへ。
downloads = 完了ダウンロードの累計)。一覧・RSS・詳細すべてに出る。crawl_config)。初期設定で ≈1,840 req/日(§5)。Crawl-delay: 5 / Disallow: /download。一覧・RSS・view は許可、/download は不要(magnetが一覧に含まれる)。マナー遵守は容易。生データは不変・追記専用で永久保存。意味づけ(分析)は後からいくらでも作り直す。
実トラフィックを取得して検証した、URL設計・一覧・RSS・ソート・ページネーション・詳細。
ベース https://sukebei.nyaa.si/。すべて GET パラメータで制御でき、RSSも同じパラメータを共有する。
| param | 役割 | 値 |
|---|---|---|
q | 検索キーワード | 任意文字列 |
c | カテゴリ | 0_0All / 1_0Art(1_1Anime,1_2Doujinshi,1_3Games,1_4Manga,1_5Pictures) / 2_0Real Life(2_1Photobooks,2_2Videos) |
f | フィルタ | 0なし / 1No remakes / 2Trusted only |
s | ソートキー | id(日付) / seeders / leechers / downloads / size / comments |
o | ソート順 | asc / desc |
p | ページ | 1〜(75件/ページ) |
page=rss | RSS出力に切替 | 上記と併用可 |
/?c=2_2)/view/{ID} + title属性).torrent + magnet:?xt=urn:btih:{infohash})4.6 GiB)data-timestamp = UTC epoch)<item> の nyaa: 名前空間<item> <guid>…/view/4624184</guid> // view ID <pubDate>… -0000</pubDate> <nyaa:downloads>0</…> // 人気指標 <nyaa:seeders>1</…> <nyaa:leechers>7</…> <nyaa:infoHash>9bb4…</…> // 安定ID <nyaa:categoryId>2_2</…> <nyaa:size>4.6 GiB</…> </item>
| 項目 | 観測結果 |
|---|---|
| HTMLソート | ?s=downloads&o=desc 等が完全に機能(トップ=363,062DL を実測) |
| RSSソート | 無効(常に新着順を返す。実測確認) |
| HTML深掘り | 機能する(p=100 で約8.3日前まで遡れる実測)。再観測の主力はこちら(§5) |
| RSS深掘り | 不可(p=40/80 が同一の最新内容を返し、深いページに遡れない。実測確認)→ RSSはごく新着の発見にのみ使う |
| ページネーション | ?p=N、75件/ページ。カテゴリ別に最大100pまで深掘り可能(サイト上限)。本システムは各カテゴリ p1..100 を巡回(§5) |
| 「昨日の一覧」 | 専用の日付レンジフィルタは無い。新着順で UTC タイムスタンプを辿り、対象日を過ぎたら停止。日付の意味づけ(JST/UTC)は分析層で決める |
詳細 /view/ID | Submitter / 説明文 / ファイル一覧 / コメント等の静的メタを取得可。1リクエスト/件とコスト高、数値スナップショット目的では不要 |
サイトは累計値の現在地しか教えてくれない。傾き(velocity)は自前のスナップショットから導く。
各作品を最低2回観測しないと差分が出ない。観測事実を欠損なく追記保存し(取得層)、傾き・順位・しきい値などの「意味」は後から計算する(分析層)。
SQLite = D1、生原本 = R2、定期実行 = Workers Cron Triggers。言語は TypeScript。
| コンポーネント | 役割 | Cloudflare機能 |
|---|---|---|
| collector 取得層 | 定期クロール→生データ保存 | Worker + Cron Triggers |
| analyzer 分析層 | 生データ→velocity/ランキング | Worker + Cron Triggers |
| API + UI | ランキング配信・閲覧 | Worker (JSON) + Pages |
| 観測DB | observations / torrents / rankings | D1(= SQLite) |
| 生原本 | RSS/HTML をそのまま保管 | R2 |
対象は Doujinshi / Games / Manga / Real Life の4カテゴリ。新着順とseeder順の2軸で観測し、巡回ページ数とクロール頻度をカテゴリごとに設定する。
カテゴリ別なら新着で 最大100ページ(≈7,500件)まで深掘り可能。100pが遡る時間幅はカテゴリで桁違い(実測: Real Life ≈8日 / Doujinshi ≈207日 / Manga ≈2.5年 / Games ≈4.5年)。よってカテゴリごとに最適な深さ・頻度が異なる=個別設定が必須。Real Life親(c=2_0)は子(Videos/Photobooks)を束ねることも確認。seeder順ソートも正常動作(実測トップ 728 seed)。
pages ページずつ巡回し observations に追記。深さ・頻度は crawl_config でカテゴリ別に可変。crawl_config)頻度(interval)と巡回ページ数(pages)をカテゴリごとに設定。下表は実測アップロード速度から導いた初期値(編集可能)。新着の巡回深さは「直近の活動量 + 余裕」を満たすように設定している。
| カテゴリ | c | 速度(実測) | pages | interval | 軸 |
|---|---|---|---|---|---|
| Real Life | 2_0 | ≈940/日 | 100 | 3時間 | 新着 + seeder |
| Doujinshi | 1_2 | ≈36/日 | 20 | 6時間 | 新着 + seeder |
| Manga | 1_4 | ≈8/日 | 15 | 12時間 | 新着 + seeder |
| Games | 1_3 | ≈4.6/日 | 10 | 24時間 | 新着 + seeder |
※ pages はサイト上限の100まで設定可。Real Lifeは100p≈8日でちょうど velocity 窓に一致。低頻度カテゴリは浅め+低頻度に抑え、無駄な再取得を避ける。値はすべて運用中に変更可能(再デプロイ不要)。
単一の Cron Trigger を最小粒度(例: 1時間ごと)で起動。collector は crawl_config を読み、各カテゴリについて now ≥ 前回実行 + interval なら「実行対象」と判定し、対象カテゴリのみを各軸 p1..pages で巡回する。前回実行時刻は crawl_runs で管理。これにより1つのCronでカテゴリごとに異なる頻度を実現する。
| カテゴリ | 1回のreq | 回/日 | req/日 |
|---|---|---|---|
| Real Life | 200 | 8 | 1,600 |
| Doujinshi | 40 | 4 | 160 |
| Manga | 30 | 2 | 60 |
| Games | 20 | 1 | 20 |
| 合計 | ≈1,840/日 |
最大の単一サイクルは Real Life の 200 req × 5s ≈ 約17分。Crawl-delay 5s 厳守でも全カテゴリ余裕。/download は叩かない。
初期設定での観測量 ≈ 9万件/日(Real Lifeが大半)→ 年間 約3,000万行(数GB)。D1単独でも数年もつが、原則どおり D1 = ホット窓(直近14〜30日)/ R2 = 全履歴アーカイブ(不削除)に階層化し、分析層は両方を読む。pages/intervalを上げた場合に備えた標準構成。
Durable Object + Alarm によるカーソル型クローラを推奨。DOが「現在の (カテゴリ, 軸, ページ)」を状態として持ち、1ページ取得 → 5秒後に自分を再起動 → 次へを繰り返す。crawl-delay 5sが自然に厳守され、単一Worker実行の時間上限に縛られず、中断しても途中再開できる。(代替: Cloudflare Queues に (cat,sort,page) ジョブを投入し consumer を max_concurrency:1 で消化。)
observations は UPDATE/DELETE 禁止の追記専用。これが「生データ・不削除」の核。
-- 観測事実(追記専用・不変) CREATE TABLE observations ( id INTEGER PRIMARY KEY AUTOINCREMENT, observed_at INTEGER NOT NULL, -- クロール時刻 (UTC epoch) nyaa_id INTEGER NOT NULL, -- /view/{ID} info_hash TEXT NOT NULL, -- コンテンツ安定ID downloads INTEGER NOT NULL, -- Completed(人気指標の母数) seeders INTEGER NOT NULL, leechers INTEGER NOT NULL, comments INTEGER NOT NULL, size_bytes INTEGER, category_id TEXT, trusted INTEGER, -- 0/1 remake INTEGER, source TEXT NOT NULL -- 'newest' | 'seeders'(取得した軸) ); CREATE INDEX idx_obs_nyaa_time ON observations(nyaa_id, observed_at); CREATE INDEX idx_obs_time ON observations(observed_at); -- 寸法表(変化しにくいメタの最新値・UPSERT) CREATE TABLE torrents ( nyaa_id INTEGER PRIMARY KEY, info_hash TEXT, title TEXT, category_id TEXT, pub_date INTEGER, submitter TEXT, first_seen_at INTEGER, last_seen_at INTEGER ); -- カテゴリ別クロール設定(運用中に編集可能・§5.2) CREATE TABLE crawl_config ( category_id TEXT PRIMARY KEY, -- '2_0','1_2','1_4','1_3' label TEXT NOT NULL, pages INTEGER NOT NULL, -- 各軸で巡回する最大ページ数(1..100) interval_min INTEGER NOT NULL, -- クロール間隔(分) sorts TEXT NOT NULL DEFAULT 'newest,seeders', enabled INTEGER NOT NULL DEFAULT 1 ); INSERT INTO crawl_config VALUES ('2_0','Real Life',100, 180,'newest,seeders',1), ('1_2','Doujinshi', 20, 360,'newest,seeders',1), ('1_4','Manga', 15, 720,'newest,seeders',1), ('1_3','Games', 10,1440,'newest,seeders',1); -- 分析層の出力(いつでも DROP→再構築) CREATE TABLE rankings ( ranking_key TEXT, rank INTEGER, nyaa_id INTEGER, score REAL, computed_at INTEGER, window_from INTEGER, window_to INTEGER, PRIMARY KEY (ranking_key, computed_at, rank) );
容量試算: 初期設定で ≈9万件/日 → 年間 約3,000万行(数GB)。D1単独でも数年もつが、原則どおり D1=ホット窓(直近14〜30日)/ R2=全履歴アーカイブ(Parquet・不削除)に階層化し、分析層は両方を読む。生HTML原本も raw/{cat}/{sort}/{YYYY}/{MM}/{DD}/{run_id}-p{page}.html で R2 保管。
取得層には触れず observations から派生計算。定義変更=クエリ差し替えのみ、過去遡及も可能。
| ランキング | 定義例 |
|---|---|
| 急上昇(直近24h) | 直近24hの velocity 降順 |
| 立ち上がり加速 | acceleration(velocityの増加)降順 = 新作の伸び初速 |
| 需要先行 | leechers 瞬間値 / seeders比(今まさにDL中) |
| カテゴリ別 | 上記を category_id で層別 |
info_hash で torrent単位。タイトル正規化で content単位(後付け可)。observed_at / pub_date に UTC/JST境界を当てて抽出(生データに日付があるので自由に切れる)。確定事項: ストレージ=SQLite/D1 ・ インフラ=Cloudflare中心 ・ 対象=全カテゴリ ・ 取得間隔=3〜6時間。