Netflixの日本での全配信タイトル一覧を作る

[アクシス Advent Calendar 2020](https://qiita.com/advent-calendar/2020/mediaxis) 3日目です!
休日は Netflix 三昧のシネフィルの皆さんなら、こんなことが気になったことはありませんか?

- 映画が好きすぎて、Netflix のレコメンドに満足できない。
- Netflixの一覧はレコメンドエンジンに基づいていて[フィルターバブル](https://www.axion.zone/what-filter-bubble-is-like/)を起こしそう。
- 本屋に行き、知らない本と出逢う感覚で新しい映画と出逢いたい。
- 見たことのある作品が多くてタイトルが分かればサムネイルはいらないから、画像のレンダリングを待つのがだるい。

そこで、Netflix Japan で配信しているタイトルテキストの一覧をサムネイル無しでスクリーンスクレイピングにより収集する方法を模索します。

## スクレイピングと法律
スクレイピングで著作権を侵したり、サイトの価値を損なったりしないか調べました。

##### スクレイピングの法律上の問題
①著作権法
②利用規約との抵触
③サーバーへの過度なアクセス

①について
[著作権法
第二章 著作者の権利 第三節 権利の内容
第五款 著作権の制限（電子計算機による情報処理及びその結果の提供に付随する軽微利用等）
第四十七条の五 第一項第二号](https://elaws.e-gov.go.jp/document?lawid=345AC0000000048) 長い(w)
*電子計算機による情報解析を行い、及びその結果を提供すること。*

文脈に沿って解釈すると以下になるそうです。
*コンピュータによって情報を解析することが目的である場合には、例外的に著作権者の同意を得ることなく、スクレイピングによって取得した他社情報などを記録媒体に記録したり翻案することができます。*
出典: [スクレイピングは違法？3つの法律問題と対応策を弁護士が5分で解説](https://topcourt-law.com/internet_security/scraping-illegal#i-6)

今回は、Netflixの情報を解析しパーソナルなコンピュータに保存することが目的なので大丈夫と思われます。
②③については、スクレイプするサイトとその利用規約を調べて判断します。

## スクレイプするサイト
まずは、本家サイトから収集することですが、認証があることと利用規約にもしっかり書いてあります。

**[Netflix 利用規約 4.6  抜粋](https://help.netflix.com/legal/termsofuse)**
*~前略~* Netflixサービスへのアクセスのためにロボット、スパイダー、**<font color="red">スクレイパーその他の自動化手段を使用せず</font>**、Netflixサービスを通じてアクセス可能なソフトウェアまたはその他の製品もしくはプロセスの *~中略~* **<font color="red">いかなるデータマイニング、データ収集もしくは抽出方法を利用しない**ことにも同意します。

本家以外でタイトル一覧を収集する方法を考えます。見つかったのは以下。

**[TMDB (The Movie DB)](https://www.themoviedb.org/movie)**
配信国とプロバイダを絞って検索できますがフィルタを設定したときにURL表示が変わらないので、1ページからスクレイピングすることはできません。

**[TMDB API](https://developers.themoviedb.org/3)**
配信国とプロバイダを絞ってタイトル一覧を取得するのに都合の良いAPIエンドポイントがなく、何万回かのリクエストをすれば欲しいデータをつくれるかもしれませんが、利用制限に引っかかりそうでサーバに負荷をかけそうなので現実的ではありませんでした。

**[JustWatch.com](https://www.justwatch.com)**
配信国はURLで決まり、プロバイダを絞って配信タイトル一覧を表示できます。
https://www.justwatch.com/jp/動画配信サービス/netflix
[利用規約](https://www.justwatch.com/jp/imprint)には特にスクレイピングに関しての記載がありません。
また、[~/robots.txt](https://www.justwatch.com/robots.txt) (スクレイピングのルールを記載するページ)では

```robots.txt
User-agent: *
Disallow:
```
すべてのスクレイパーに対してDisallow がないので、全ページにスクレイピングを許可しています。
JustWatch を使って Netflix をジャストウォッチすることにします。

## 実装
Vue.js を使っているサイトなので、ブラウザインスペクターで見れるHTMLコードをスクレイピングするためにはJavaScriptの実行が必要です。
そこで、[Puppeteer](https://github.com/puppeteer/puppeteer)というヘッドレス(GUIのない) Chromium browser の自動化ライブラリを使います。

##### Puppeteer インストール
```
$ npm i puppeteer
```
ひとまず、Puppeteerを使って対象ページにアクセスし、タイトルテキストの入っているHTMLタグを取得してタイトルテキスト部分を抽出します。

```index.js
const puppeteer = require('puppeteer');

function extractItems() {
  const links = document.querySelectorAll('.title-list-grid__item--link');
  let urls = [];
  for(let link of links) {
    urls.push(link.getAttribute('href'));
  }
  return urls;
}

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://www.justwatch.com/jp/動画配信サービス/netflix', {
        waitUntil: 'networkidle0'
    });

    let items = await page.evaluate(extractItems);
    console.log(items);

    await browser.close();
})();
```
gotoメソッドの`waitUntil`オプションで、ネットワーク接続がなくなるまで待機しています。

出力

```bash
netflix_list_of_titles % node sample1.js 
[
  '/jp/テレビ番組/gui-mie-noren',
  '/jp/テレビ番組/kuinzugiyanbitsuto',
  '/jp/テレビ番組/zhou-shu-hui-zhan',
  .......
  '/jp/映画/chlaadekmsokng'
]
```
20要素ほど抽出できました。
あらためてサイトを見ると、無限スクロールと遅延ロードを使っています。配信国とプロバイダ以外のフィルタを設定しないとタイトル数は5,192です。無限スクロールをしながら描画された要素をスクレイピングするのは難しそうです。このサイトではフィルタに以下のパラメータが使えてURL表示にも反映されます。

- content_type: movie, show(テレビ番組)
- release_year_{from, until}: yyyy
- genres: act, ani, ..., etc.
- ...

フィルタで表示件数を絞ったページをクローリングしつつスクレイピングすることで全てのタイトルを得ることにします。
試行錯誤を経て、以下のコードを書きました。
##### スクレイピングスクリプト

```index.js
const fs = require("fs");
const puppeteer = require("puppeteer");

function extractItems() {
  const links = document.querySelectorAll(".title-list-grid__item--link");
  let urls = [];
  for (let link of links) {
    urls.push(link.getAttribute("href"));
  }
  return urls;
}

async function autoScroll(page) {
  await page.evaluate(async () => {
    await new Promise((resolve) => {
      let totalHeight = 0;
      let distance = 2000;
      let timer = setInterval(() => {
        let scrollHeight = document.body.scrollHeight;
        window.scrollBy(0, distance);
        totalHeight += distance;

        if (totalHeight >= scrollHeight) {
          clearInterval(timer);
          resolve();
        }
      }, 1000);
    });
  });
}

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setViewport({
    width: 1200,
    height: 5000,
  });

  const contentTypes = ["movie", "show"];
  const genres = ['act', 'ani', 'cmy', 'crm', 'doc', 'drm', 'eur', 'fml', 'fnt', 'hrr', 'hst', 'msc', 'rly', 'rma', 'scf', 'spt', 'trl', 'war', 'wsn'];
  const date = new Date();
  const thisYear = date.getFullYear();

  let titles = [];
  for (let year = 1936; year <= thisYear; year++) {
    for (let contentType of contentTypes) {
      for (let genre of genres) {
        await page.goto(
          `https://www.justwatch.com/jp/動画配信サービス/netflix?content_type=${contentType}&genres=${genre}&release_year_from=${year}&release_year_until=${year}`
        );

        await autoScroll(page);
        console.log(year);
        items = await page.evaluate(extractItems);
        console.log(items);

        titles.push(items);
      }
    }
  }

  fs.writeFileSync("./titles.txt", titles.join("\n") + "\n");

  await browser.close();
})();
```
- Viewportのサイズ
- スクロール量
- スクロール後の読み込み待機時間

を調整して、レンダリングし終えた要素を確実に取得できるようにしています。

**出力**

`puppeteer.launch({headless: false});` を指定すると、Chromium browser がGUI有りで記述した操作どおりに動作します。
![titles.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196261/8badfb0f-d96f-a0ae-c413-ad94125f19fa.gif)

```titles.txt
前略
1542 /rodney-king,/jp/映画/catfight-2017,/jp/映画/nogemunoraihu-zero,/jp/映画/sandy-wexler,/jp/映画/naked-2017,/jp/映画/itazuranakiss-the-movie2-kiyanpasubian,/jp/映画/fun-mom-dinner,/jp/映画/the-polka-king,/jp/映画/orushi,/jp/映画/take-me,/jp/映画/ptojk,/jp/映画/handsome,/jp/映画/fe-de-etarras,/jp/映画/jour-j,/jp/映画/o-matador,/jp/映画/emo-the-musical,/jp/映画/jb-hairii-mett-   1542 sejl,/jp/映画/realityhigh,/jp/映画/oh-hello-on-broadway,/jp/映画/cook-up-a-storm,/jp/映画/mr-roosevelt,/jp/映画/small-crimes,/jp/映画/a-storks-journey,/jp/映画/rasutoreshipi-qi-lin-noshe-noji-yi,/jp/映画/dany-de-boon-des-hauts-de-france
1543 /jp/映画/chlaadekmsokng,/jp/映画/sleepless,/jp/映画/the-foreigner-2017,/jp/映画/the-book-of-henry,/jp/映画/uindoriba,/jp/映画/san-du-mu-nosha-ren,/jp/映画/what-happened-to-monday,/jp/映画/geteijia-noshen-dai-jin,/jp/映画/the-hitmans-bodyguard,/jp/映画/good-time,/jp/映画/zigusou-souregashi,/jp/映画/marshall,/jp/映画/1922,/jp/映画/roman-j-israel-esq,/jp/映画/shimmer-lake,/jp   1543 /映画/destiny-the-tale-of-kamakura,/jp/映画/memoir-of-a-murderer,/jp/映画/the-snowman-2017,/jp/映画/iboy,/jp/映画/la-92,/jp/映画/super-dark-times,/jp/映画/el-guardian-invisible,/jp/映画/i-dont-feel-at-home-in-this-world-anymore,/jp/映画/manhunt-2017,/jp/映画/smetto-quando-voglio-masterclass,/jp/映画/bullet-head,/jp/映画/bright,/jp/映画/death-note-2017,/jp/映画/tan-zhen-hab   1543 arniiru3,/jp/映画/po-men-hutarinoyakubiyogami,/jp/映画/bad-day-for-the-cut,/jp/映画/you-get-me,/jp/映画/long-shot,/jp/映画/the-saint,/jp/映画/what-did-jack-do,/jp/映画/el-camino-christmas,/jp/映画/wheelman,/jp/映画/locked-up,/jp/映画/rodney-king,/jp/映画/nieve-negra,/jp/映画/take-me,/jp/映画/coin-heist,/jp/映画/small-crimes,/jp/映画/plan-de-fuga,/jp/映画/bon-cop-bad-cop-2,   1543 /jp/映画/el-otro-hermano,/jp/映画/out-of-thin-air,/jp/映画/riis,/jp/映画/the-swindlers,/jp/映画/deidra-and-laney-rob-a-train,/jp/映画/hamzas-suitcase,/jp/映画/arrest-letter,/jp/映画/a-sort-of-family,/jp/映画/the-bittersweet
1544 /jp/映画/bending-the-arc,/jp/映画/the-circle,/jp/映画/the-dawn-wall,/jp/映画/fireworks-should-we-see-it-from-the-side-or-the-bottom,/jp/映画/chasing-trane,/jp/映画/1922,/jp/映画/diana-in-her-own-words,/jp/映画/la-92,/jp/映画/jim-and-andy-the-great-beyond-featuring-a-very-special-contractually-obligated-mention-of-tony-clifton,/jp/映画/get-me-roger-stone,/jp/映画/icarus,/jp   1544 /映画/ryuichi-sakamoto-coda,/jp/映画/les-affames,/jp/映画/jane,/jp/映画/earth-one-amazing-day,/jp/映画/what-the-health,/jp/映画/one-of-us-2017,/jp/映画/long-shot,/jp/映画/chasing-coral,/jp/映画/heal,/jp/映画/cuba-and-the-cameraman,/jp/映画/power-of-grayskull-the-definitive-history-of-he-man-and-the-masters-of-the-universe,/jp/映画/casting-jonbenet,/jp/映画/mission-control-   1544 the-unsung-heroes-of-apollo,/jp/映画/risk,/jp/映画/a-gray-state,/jp/映画/the-b-side-elsa-dorfmans-portrait-photography,/jp/映画/rodney-king,/jp/映画/gaga-five-foot-two,/jp/映画/the-death-and-life-of-marsha-p-johnson,/jp/映画/strong-island,/jp/映画/unrest,/jp/映画/handsome,/jp/映画/voyeur,/jp/映画/dream-boat,/jp/映画/long-time-running,/jp/映画/bill-nye-science-guy,/jp/映画/   1544 joan-didion-the-center-will-not-hold,/jp/映画/expedition-happiness,/jp/映画/neal-brennan-3-mics,/jp/映画/liberated-the-new-sexual-revolution,/jp/映画/brian-regan-nunchucks-and-flamethrowers,/jp/映画/hasan-minhaj-homecoming-king,/jp/映画/norm-macdonald-hitlers-dog-gossip-and-trickery,/jp/映画/out-of-thin-air,/jp/映画/patton-oswalt-annihilation,/jp/映画/generation-iron-2,/jp   1544 /映画/raising-the-bar,/jp/映画/heroin-e,/jp/映画/dream-big-engineering-our-world,/jp/映画/sarah-silverman-a-speck-of-dust,/jp/映画/the-force,/jp/映画/nobody-speak-trials-of-the-free-press,/jp/映画/the-last-shaman,/jp/映画/counterpunch,/jp/映画/mostly-sunny,/jp/映画/saving-capitalism,/jp/映画/kingdom-of-us,/jp/映画/forbidden-games-the-justin-fashanu-story,/jp/映画/sky-ladde   1544 r-the-art-of-cai-guo-qiang-2016,/jp/映画/follow-me-2017,/jp/映画/the-mars-generation,/jp/映画/barbra-the-music-the-memries-the-magic,/jp/映画/ladies-first,/jp/映画/ram-dass-going-home,/jp/映画/peru-tesoro-escondido,/jp/映画/rory-scovel-tries-stand-up-for-the-first-time,/jp/映画/maddman-the-steve-madden-story,/jp/映画/judah-friedlander-america-is-the-greatest-country-in-the   1544 -united-states,/jp/映画/def-comedy-jam-25,/jp/映画/maz-jobrani-immigrant,/jp/映画/jerry-before-seinfeld,/jp/映画/the-carter-effect,/jp/映画/tiffany-haddish-she-ready-from-the-hood-to-hollywood,/jp/映画/ryan-hamilton-happy-face,/jp/映画/the-legend-of-420,/jp/映画/maria-bamford-old-baby,/jp/映画/the-untold-tales-of-armistead-maupin,/jp/映画/laerte-se,/jp/映画/born-to-be-free   1544 ,/jp/映画/coffee-for-all,/jp/映画/resurface,/jp/映画/behind-the-curtain-todrick-hall,/jp/映画/a-week-in-watts,/jp/映画/crystal-inferno,/jp/映画/we-the-marines,/jp/映画/rodney-carrington-here-comes-the-truth,/jp/映画/chris-delia-man-on-fire,/jp/映画/joshua-teenager-vs-superpower,/jp/映画/bill-burr-walk-your-way-out,/jp/映画/michael-lost-and-found,/jp/映画/la-manzana-de-eva,   1544 /jp/映画/dont-crack-under-pressure-iii,/jp/映画/food-on-the-go,/jp/映画/joe-mandes-award-winning-comedy-special,/jp/映画/lovesick,/jp/映画/pacificum,/jp/映画/jeosuji-geim,/jp/映画/felipe-neto-my-life-makes-no-sense
1545 /jp/映画/chlaadekmsokng,/jp/映画/sleepless,/jp/映画/tulip-fever,/jp/映画/the-foreigner-2017,/jp/映画/saturday-church,/jp/映画/chiyotsutojin-karashi-shi-yametekuru,/jp/映画/chiadan-nu-zi-gao-sheng-gachiadansudequan-mi-zhi-ba-shichiyatsutahontonohua,/jp/映画/3yue-noraion-qian-bian,/jp/映画/the-bad-batch,/jp/映画/the-book-of-henry,/jp/映画/mon-mon-mon-monsters,/jp/映画/huo-hu   1545 a,/jp/映画/3yue-noraion-hou-bian,/jp/映画/uindoriba,/jp/映画/8nian-yue-shinohua-jia-qi-ji-noshi-hua,/jp
後略
```

それなりにリストが作れたように思います。
ただし、

- 英語でもローマ字でも読めないタイトルテキストがある
- 複数ジャンルに登録されている作品が重複している
- HTMLファイル(共有･公開はしない)にして[タイトルテキスト](https://www.netflix.com/search?q=Girl%20with%20the%20Dragon%20Tattoo&jbv=70207663)のようなリンクにしたい

点で、実用のために改善したいです!