アニメ名探偵コナンの放送話情報を得るために誤りと表記ゆれの海なYTVのjsonと格闘する

# 注意

微粒子レベルですが名探偵コナンとまじっく快斗のネタバレを含んでいます。苦手な方は注意して読み進めてください。

# はじめに

皆さん、アニメ名探偵コナンはご存知でしょうか？「真実はいつも一つ！」とかのたまってる見た目は子ども、頭脳は(自称)大人なバーローが主人公のあのアニメです。

さて、アニメ名探偵コナンはytv(日本テレビ系列)が制作しているので、そこに公式Webページがあります。
そしてこれまでの放送話(事件ファイル)を公開しています。
https://www.ytv.co.jp/conan/archive/index.html

# 公式の事件ファイルのページはどのように作られているか

最近仕様変更が入ったのですがそれに基づいて話をしていきます。

通信をブラウザで調査すると主に2つのjsonファイルが登場します。

- 古いほう: https://www.ytv.co.jp/conan/data/story.json
- 新しいほう: https://www.ytv.co.jp/conan/data/case.json

これをもとに自作のツールを作ろうとしていたので具体的に公式でこのjsonをどうパースしてDOMを組み立ててるかは意図的に見ないことにしました。著作権侵害とか言われても困りますしね。

これらのjsonには以下の4つに分類される情報が時系列に混ぜられて格納されています。

- 通常の放送
- 再放送(デジタルリマスター版)
- まじっく快斗の放送(名探偵コナンと同じ原作者青山 剛昌先生の作品、怪盗)
- なぜか放送話の連番から外れた「謹賀新年 毛利小五郎」(2015年1月3日放送)
- 劇場版のTV放送情報(むかしのほうだけ)

~~(おい、5つあるぞ)~~

今回は劇場版のTV放送情報は無視することにしました。

# `story.json`の誤りと表記ゆれの数々

YTVが提供しているこの`story.json`には誤りと表記ゆれが多数存在しています。見ていきましょう。

## 「名探偵コナンスペシャル　『風林火山　迷宮の鎧武者』」

当該部分のjsonは次のとおりです。

```json
			{
				"oaDateId"	: "20081103",
				"title"			: "名探偵コナンスペシャル　『風林火山　迷宮の鎧武者』",
				"story_num"	: "",
				"url"				:	"k116921.html"
			},
```

`story_num`が抜けています。`516`が正しい値です。

## 「怪盗キッドの瞬間移動魔術」

当該部分のjsonは次のとおりです。

```json
			{
				"oaDateId"	: "20081020",
				"title"			: "怪盗キッドの瞬間移動魔術",
				"story_num"	: "",
				"url"				:	"k116923.html"
			},
```

`story_num`が抜けています。`515`が正しい値です。

## `()`と`（）`の不統一

jsonを示すのはやめますが統一されていません。どっちかにしてくれ・・・。

## `story_num`が`R`から始まるけどオリジナルの放送時とタイトルが一致しない例

### `title`内のデジタルリマスターの表現が自由すぎる

まあそもそも`story_num`が`R`から始まるとデジタルリマスター版であるということになっています。ところが、`title`にデジタルリマスターであることを示す文言が入っていなかったり、入り方が多岐に渡ります。

1. そもそもそんなものは含まれていない  
    デジタルリマスター版放映が始まった初期に多いです。
2. `デジタルリマスター`で終端する  
    わかりやすくていいですね。最近のに多めな気がします。
3. 正規表現でどうにか引っ掛けられるケース

    ```typescript
    const degitalReMasterSearchRegex = /(.+)[[（(［]デジタル *リマスター.*[）)\]］]$/;
    ```
    のような正規表現で引っかかります。括弧が同種類で対応しないことすらあります。あと`デジタル リマスター`/`デジタルリマスター`の2パターンあります。揺れ過ぎな？ちなみにこの正規表現は`黒の組織との接触（デジタルリマスター特別編集版）`みたいなやつにも対応しています。

    しかもここからさらに

    ````typescript
    const pureTitle = executed[1].replace(/[ 〔（]+(.{1,2})編[〕） ]*$/, '（$1編）');
    ```

    のような置換が必要です。前後編以外にも`仏滅に出る悪霊（解決編）`みたいなパターンがあります。
4. 正規表現で「」の中を抜き出すことでなんとかなるケース
   `秋の本格的ミステリースペシャル　「呪いの仮面は冷たく笑う」（デジタルリマスター版）`(2004/9/20)が実例ですね。

   ```typescript
   const bracket = /「(.+)」/.exec(title);
   ```

   ただし`秋のミステリースペシャル名探偵コナン「鳥取クモ屋敷の怪（デジタルリマスター）」`(2007/10/29)のように抜いたあとにさらにデジタルリマスターのような何かを含んでいることもあるので

   ```typescript
   const degitalReMasterSearchRegex = /(.+)[[（(［]デジタル *リマスター.*[）)\]］]$/;
   //略
   const maybePureTitle = bracket[1];
   const executed = degitalReMasterSearchRegex.exec(maybePureTitle);
   ```

   のようにさらなる抽出が必要です。そして`鳥取クモ屋敷の怪`は初回は3話に分けて放送されていますからそれも想定して

   ```typescript
   const matched = this.pure.filter(c => c.title.includes(pureTitle));
   ```

   のように全体検索で絞り込む必要が出てきます。

### あらかじめ`story.json`を置換しておかないときついケース

こういう努力にも関わらずうまく行かないものがたくさんあるので予め`story.json`を置換するプログラムを書くことにしました。

[storyConverter.ts](https://github.com/yumetodo/detective_connan_story_info_getter/blob/main/tools/storyConverter.ts)

- `集められた名探偵～`: 再放送は分割されたので`集められた名探偵！工藤新一ＶＳ怪盗キッド`に
- `容疑者毛利小五郎～`: もとの話は`容疑者・毛利小五郎～
- `ブラックインパクト～`: 再放送は分割されたので`名探偵コナン放送10周年記念超拡大スペシャル「ブラックインパクト！組織の手が届く瞬間」`に
- `闇の男爵殺人事件～`: `s/闇の男爵殺人事件・(.+)編\(デジタルリマスター\)/闇の男爵（ﾅｲﾄﾊﾞﾛﾝ）殺人事件（$1篇）/`
- `園子のアブナイ夏物語`: `s/園子のアブナイ夏物語（(.+)編）\(デジタル・*リマスター\)/園子のアブない夏物語（$1編）`

```typescript
const replaceList = [
  ['TVドラマロケ殺人事件（デジタルリマスター）', 'ＴＶドラマロケ殺人事件'],
  ['標的（ターゲット）は毛利小五郎（デジタルリマスター）', '標的は毛利小五郎'],
  ['帝丹小7不思議事件（デジタルリマスター）', '帝丹小７不思議事件'],
  ['１年B組大作戦！(デジタルリマスター版)', '1年B組大作戦！'],
  ['黒の組織との再会', '黒の組織との再会（灰原編）'],
  ['名探偵コナンスペシャル「工藤新一NYの事件」', '工藤新一ＮＹの事件（事件編）'],
  ['黒の組織との接触（デジタルリマスター特別編集版）', '黒の組織との接触（交渉編）'],
  ['そして人魚はいなくなった（デジタルリマスター特別編集版）', 'そして人魚はいなくなった（事件編）'],
  ['名探偵コナン10周年記念スペシャル 「コナンＶＳ怪盗キッド」（デジタルリマスター版）', 'コナンVS怪盗キッド'],
  ['名探偵コナン１時間スペシャル「空飛ぶ密室　工藤新一最初の事件」', '空飛ぶ密室 工藤新一最初の事件'],
  [
    '名探偵コナン秋の本格ミステリー2時間スペシャル　揺れる警視庁1200万人の人質」（デジタルリマスター版） ',
    '揺れる警視庁　1200万人の人質',
  ],
  ['1時間スペシャル（デジタルリマスター版）「迷宮への入り口 巨大神像の怒り」', '迷宮への入口　巨大神像の怒り'],
  ['怪盗キッドの驚異空中歩行', '怪盗キッドの驚異空中歩行」1時間スペシャル】'],
];
```

加えてこれらの手動置換が必要です。

# `case.json`に対する補正

フォーマットが変わった`case.json`、比較的揺れが少ないので「`title`内のデジタルリマスターの表現が自由すぎる」の項目の対策だけである程度通るので助かるのですが、やっぱり補正がいらないわけではないようです。

## `大捜索 ９つのドア`

`大捜索 ９つのドア`という放送話ですが、じつはデジタルリマスター版で、もとの放送話は`大捜索九つのドア`というタイトルです。

## 緋色の帰還

上記のような対策をしてもう完璧やろと安心しきっていました。しかし2021年4月、毎週回すようにしていたCIが落ちた知らせが入ります。何かと思ったら「緋色の帰還」系でやらかしてくれました。こいつもまたタイトルだけではデジタルリマスター版と見抜けないシリーズです。しかも元の放送話とタイトルの規則が違うので置換が必要です。

```typescript
const replaceScarletCase = (title: string) => {
  if (!title.startsWith('緋色の帰還')) return undefined;
  const k = '（デジタルリマスター）';
  // 2021-04-10放送のタイトルは、緋色の帰還（真相）、なんと（デジタルリマスター）って書いてない！
  const t = title.endsWith(k) ? title.substring(0, title.length - k.length) : title;
  return t.replace(/緋色の帰還（([^）]+)）/, '緋色の$1');
};
```

# 名探偵コナン放送話検索ツールを作ったが、結局どういうフローでこれらの誤りに立ち向かっているのか

https://github.com/yumetodo/detective_connan_story_info_getter

これが作ったものです。

typescriptで書いています。ビルドにはWebpackを使っていましたがesbuildに乗り換えました。テストコードの実行が律速するのでCIの実行時間的にはあまり意味はありません。

ビルドは2回行われます

1. `story.json`の事前変換ツールのビルド
2. `story.json`の事前変換: 実行時に持っていけない辛さを予めなんとかする
    1. 初回放送っぽいのを抜き出す
    2. 手動置換や正規表現を駆使して再放送と初回放送をさらに分離
    3. `まじっく快斗`/`怪盗キッド`を含むor`聖夜（イブ）は恋するゲレンデで`はまじっく快斗に分離
    4. 残りは特別話として扱う
3. 変換された`story.json`をbundleして実行時に動かすツールをビルド

実行時には`case.json`を取得して分類していきます。このとき`PureDatabase#find`関数が上記のような変換をして揺れを吸収します。ただし`case.json`に対する補正は分類の前に当てます。

# 教訓

世に送り出すデータがきちんと正規化されていないと、利用者はこういう膨大な苦労が必要になります。
YTVさんはべつにデータを提供しようとはさほど思っていないだろうと予想できますのでいいのですが、
データを提供しよう、という人は使う人がパースしやすい形式で正規化して提供するように心がけるといいと思います。

# 注意

npm packageにすると`story.json`を梱包するような形になるので著作権に控えめに言って喧嘩を売ってしまうものと思います。ましてや再頒布とかするのはアウトです。やめましょう。