Crawling & Ranking Platform — Observation & Design

sukebei.nyaa.si
ダウンロードの伸びから人気をランキング化する基盤

公開サイトを定期クローリングして「ダウンロード数の伸び(velocity)」から人気コンテンツをランキング化する仕組みの、サイト構造の網羅観測と最適設計。生データを定期保存して削除せず、その意味づけ(分析)は後からいかようにも変えられる二層アーキテクチャを採用する。

観測日 2026-06-13(実トラフィック検証) インフラ Cloudflare 中心 対象 全カテゴリ 取得間隔 3〜6時間 保存 SQLite / D1

0結論サマリ

先に要点だけ。詳細は各セクションへ。

  • サイトは NyaaV2系(オープンソース nyaa) インスタンス。完全なサーバサイドHTML + RSSで、JS不要・スクレイピング容易
  • 人気指標の核は Completed(= downloads = 完了ダウンロードの累計)。一覧・RSS・詳細すべてに出る。
  • 最重要 サイトは「現在の累計値」しか持たず、履歴・時系列が一切無い。だから「伸び」は自前の定期スナップショットの差分でしか算出できない → これが取得層/分析層の分離が必須となる根拠。
  • データソースは HTML 一覧に一本化(RSSは不採用)。再観測にはカテゴリ別の深いページネーション(最大100p)が必要だが、それはHTMLでしか機能しない(RSSは深掘り不可・実測)。HTMLにRSSと同じ全項目が入るため、RSSを併用する固有メリットは無くパーサが二重になるだけ。
  • 対象は4カテゴリ(Doujinshi / Games / Manga / Real Life)。各カテゴリを新着順 + seeder順の2軸で観測し、巡回ページ数とクロール頻度をカテゴリごとに設定crawl_config)。初期設定で ≈1,840 req/日(§5)。
  • robots.txt は Crawl-delay: 5 / Disallow: /download。一覧・RSS・view は許可、/download は不要(magnetが一覧に含まれる)。マナー遵守は容易。

1設計の中核 — 取得層 / 分析層の分離

生データは不変・追記専用で永久保存。意味づけ(分析)は後からいくらでも作り直す。

取得層 (Acquisition) 不変・追記専用・削除しない / 生の観測事実 T1 id=4624184 downloads=0 s/l=1/7 T2 id=4624184 downloads=120 s/l=9/22 T3 id=4624184 downloads=380 s/l=14/15 T4 id=4624184 downloads=510 s/l=12/8 ↑ 「時刻Tに作品Xの累計DLはNだった」= 不変の事実 分析層 (Analysis) 再計算可能 / 意味づけは後から差し替え自由 velocity = Δdownloads / Δt (510−0) / Δtime → 伸び速度として順位化 急上昇 / 立ち上がり加速 / カテゴリ別 … info_hash で重複排除・タイトル名寄せ 定義を変えても生データは無傷 → 何度でも作り直せる read only (分析層は生データを書き換えない)
生データ(左)は永久不変。分析(右)はそこから派生計算するだけなので、ランキングの定義をいつでも変更・過去遡及できる。

2サイト構造の網羅観測

実トラフィックを取得して検証した、URL設計・一覧・RSS・ソート・ページネーション・詳細。

2.1 URL設計 / クエリパラメータ

ベース 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=rssRSS出力に切替上記と併用可

2.2 データソースの比較 — RSS を主力に

sukebei.nyaa.si サーバサイドHTML + RSS ① RSS フィード ?page=rss — 機械可読・HTMLパース不要 seeders / leechers / downloads / infoHash / categoryId / size / comments / trusted / remake ⚠ ソート無効・深掘り不可(新着のみ) → 本システムでは不採用(HTMLで代替可) ② HTML 一覧 ?s=downloads&o=desc — サーバ側ソートが有効 実測トップ = 363,062 DL(全期間累計) テーブルをパースして数値を取得 ✓ ソートも深掘り(p=100=約8.3日前)も機能 → 採用: 唯一のデータソース(§5の主力)
RSS はソート無効・深掘り不可で新着しか返せない。数日ぶんを遡る再観測には深掘りが必須で、それは HTML のみ可能。HTML に全項目が入るため 本システムは HTML 一本に統一し RSS は不採用(§5)。

2.3 一覧テーブルの列 / RSS フィールド

HTML 一覧テーブルの列(75件/ページ)

  • Category(アイコン+リンク /?c=2_2
  • Name(/view/{ID} + title属性)
  • Comments 数
  • Link(.torrent + magnet:?xt=urn:btih:{infohash}
  • Size(例 4.6 GiB
  • Date(data-timestamp = UTC epoch)
  • Seeders / Leechers
  • Completed(= downloads)= 人気指標の核

RSS <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>

2.4 ソート実測 / ページネーション / 詳細ページ

項目観測結果
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/IDSubmitter / 説明文 / ファイル一覧 / コメント等の静的メタを取得可。1リクエスト/件とコスト高、数値スナップショット目的では不要

3「伸び」をどう算出するか

サイトは累計値の現在地しか教えてくれない。傾き(velocity)は自前のスナップショットから導く。

累計DL 時間 T1 T2 (+4h) T3 (+8h) T4 (+12h) 0 120 380 510 +30/h +65/h ← 加速 +32/h 青点線 = 定期スナップショット(取得層が記録) velocity = Δdownloads / Δt サイトは各点の「現在値」しか出さない。 傾き=伸びは差分でしか得られない。
定期スナップショット(青)を取り、隣接観測の差分で velocity を算出。加速(T2→T3)が「急上昇」シグナルになる。

だから二層分離が必須になる

各作品を最低2回観測しないと差分が出ない。観測事実を欠損なく追記保存し(取得層)、傾き・順位・しきい値などの「意味」は後から計算する(分析層)。

4Cloudflare 構成

SQLite = D1、生原本 = R2、定期実行 = Workers Cron Triggers。言語は TypeScript。

sukebei.nyaa.si HTML 一覧 Cloudflare Cron 1h due判定→対象だけ Worker A collector(取得層) 5s間隔で巡回・生保存 D1 (SQLite) observations / torrents 追記専用・不削除 R2 生RSS/HTML原本 再解釈用の保険 Cron 1h ⏱ Trigger Worker B analyzer(分析層) velocity・順位を再計算 D1 rankings DROP→再構築 自由 Worker C API + Pages UI read only ブラウザ ランキング閲覧
collector が生データを D1/R2 に蓄積(取得層)。analyzer は生データを読むだけで rankings を再計算(分析層)。API/Pages が閲覧を提供。
取得層 / Worker D1 (SQLite) R2 分析層 Cron
コンポーネント役割Cloudflare機能
collector 取得層定期クロール→生データ保存Worker + Cron Triggers
analyzer 分析層生データ→velocity/ランキングWorker + Cron Triggers
API + UIランキング配信・閲覧Worker (JSON) + Pages
観測DBobservations / torrents / rankingsD1(= SQLite)
生原本RSS/HTML をそのまま保管R2

5クロール戦略 — 4カテゴリ × 2軸 × カテゴリ別設定

対象は 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)。

5.1 2軸クロール(新着順 × seeder順)

① 新着順 (date desc) ?c={cat}&p=1..{pages} 最近性軸 — 発見 + 時系列の再観測 100p の到達時間幅(カテゴリ依存・実測) Real Life ≈ 8日(高頻度=velocity窓) Doujinshi ≈ 207日 / Manga ≈ 2.5年 Games ≈ 4.5年(低頻度=ほぼ全件) → カテゴリ別に巡回ページ数を設定 ② seeder順 (seeders desc) ?c={cat}&s=seeders&o=desc&p=1..{pages} 現在人気軸 — 今よく配信されている作品 年齢に関係なく上位を毎回観測 古い作品でも現在人気が再燃すれば捕捉 需要中(seeders)=DLの伸びと別軸の人気指標 実測 p1 トップ: 728 seed / 2,770 DL × 4カテゴリ × 4カテゴリ 頻度・ページ数は crawl_config でカテゴリ別に設定 observations へ追記(サイクル毎に重複排除) 同時に生HTML原本を R2 へ
新着順=最近性軸、seeder順=現在人気軸。両軸を各カテゴリ pages ページずつ巡回し observations に追記。深さ・頻度は crawl_config でカテゴリ別に可変。

5.2 カテゴリ別クロール設定(crawl_config

頻度(interval)と巡回ページ数(pages)をカテゴリごとに設定。下表は実測アップロード速度から導いた初期値(編集可能)。新着の巡回深さは「直近の活動量 + 余裕」を満たすように設定している。

カテゴリc速度(実測)pagesinterval
Real Life2_0≈940/日1003時間新着 + seeder
Doujinshi1_2≈36/日206時間新着 + seeder
Manga1_4≈8/日1512時間新着 + seeder
Games1_3≈4.6/日1024時間新着 + seeder

※ pages はサイト上限の100まで設定可。Real Lifeは100p≈8日でちょうど velocity 窓に一致。低頻度カテゴリは浅め+低頻度に抑え、無駄な再取得を避ける。値はすべて運用中に変更可能(再デプロイ不要)。

5.3 スケジューラ(カテゴリ別頻度の実現)

単一の Cron Trigger を最小粒度(例: 1時間ごと)で起動。collector は crawl_config を読み、各カテゴリについて now ≥ 前回実行 + interval なら「実行対象」と判定し、対象カテゴリのみを各軸 p1..pages で巡回する。前回実行時刻は crawl_runs で管理。これにより1つのCronでカテゴリごとに異なる頻度を実現する。

性能 — 初期設定での負荷

カテゴリ1回のreq回/日req/日
Real Life20081,600
Doujinshi404160
Manga30260
Games20120
合計≈1,840/日

最大の単一サイクルは Real Life の 200 req × 5s ≈ 約17分Crawl-delay 5s 厳守でも全カテゴリ余裕。/download は叩かない。

ストレージ → D1ホット + R2コールド(推奨)

初期設定での観測量 ≈ 9万件/日(Real Lifeが大半)→ 年間 約3,000万行(数GB)。D1単独でも数年もつが、原則どおり D1 = ホット窓(直近14〜30日)/ R2 = 全履歴アーカイブ(不削除)に階層化し、分析層は両方を読む。pages/intervalを上げた場合に備えた標準構成。

5.4 Cloudflare上の実行方式

Durable Object + Alarm によるカーソル型クローラを推奨。DOが「現在の (カテゴリ, 軸, ページ)」を状態として持ち、1ページ取得 → 5秒後に自分を再起動 → 次へを繰り返す。crawl-delay 5sが自然に厳守され、単一Worker実行の時間上限に縛られず、中断しても途中再開できる。(代替: Cloudflare Queues に (cat,sort,page) ジョブを投入し consumer を max_concurrency:1 で消化。)

6D1 スキーマ(取得層)

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 保管。

7分析層 — 差し替え自由な「意味づけ」

取得層には触れず observations から派生計算。定義変更=クエリ差し替えのみ、過去遡及も可能。

ランキング定義例
急上昇(直近24h)直近24hの velocity 降順
立ち上がり加速acceleration(velocityの増加)降順 = 新作の伸び初速
需要先行leechers 瞬間値 / seeders比(今まさにDL中)
カテゴリ別上記を category_id で層別

8未決事項 / 次アクション

  • 確認 ランキングUIの要否・形(最小の静的表示から始めるか)
  • 確認 分析ウィンドウの既定値(急上昇=24h / 中期=7d など)
  • 確認 R2 生原本保管を最初から有効化するか(推奨: 有効
  • Wrangler プロジェクト雛形 + D1マイグレーション + collector 最小実装に着手

確定事項: ストレージ=SQLite/D1 ・ インフラ=Cloudflare中心 ・ 対象=全カテゴリ ・ 取得間隔=3〜6時間。