アクシス Advent Calendar 2020 3日目です!
休日は Netflix 三昧のシネフィルの皆さんなら、こんなことが気になったことはありませんか?
- 映画が好きすぎて、Netflix のレコメンドに満足できない。
- Netflixの一覧はレコメンドエンジンに基づいていてフィルターバブルを起こしそう。
- 本屋に行き、知らない本と出逢う感覚で新しい映画と出逢いたい。
- 見たことのある作品が多くてタイトルが分かればサムネイルはいらないから、画像のレンダリングを待つのがだるい。
そこで、Netflix Japan で配信しているタイトルテキストの一覧をサムネイル無しでスクリーンスクレイピングにより収集する方法を模索します。
スクレイピングと法律
スクレイピングで著作権を侵したり、サイトの価値を損なったりしないか調べました。
スクレイピングの法律上の問題
①著作権法
②利用規約との抵触
③サーバーへの過度なアクセス
①について
著作権法
第二章 著作者の権利 第三節 権利の内容
第五款 著作権の制限(電子計算機による情報処理及びその結果の提供に付随する軽微利用等)
第四十七条の五 第一項第二号 長い(w)
電子計算機による情報解析を行い、及びその結果を提供すること。
文脈に沿って解釈すると以下になるそうです。
コンピュータによって情報を解析することが目的である場合には、例外的に著作権者の同意を得ることなく、スクレイピングによって取得した他社情報などを記録媒体に記録したり翻案することができます。
出典: スクレイピングは違法?3つの法律問題と対応策を弁護士が5分で解説
今回は、Netflixの情報を解析しパーソナルなコンピュータに保存することが目的なので大丈夫と思われます。
②③については、スクレイプするサイトとその利用規約を調べて判断します。
スクレイプするサイト
まずは、本家サイトから収集することですが、認証があることと利用規約にもしっかり書いてあります。
Netflix 利用規約 4.6 抜粋
~前略~ Netflixサービスへのアクセスのためにロボット、スパイダー、スクレイパーその他の自動化手段を使用せず、Netflixサービスを通じてアクセス可能なソフトウェアまたはその他の製品もしくはプロセスの ~中略~ いかなるデータマイニング、データ収集もしくは抽出方法を利用しないことにも同意します。
本家以外でタイトル一覧を収集する方法を考えます。見つかったのは以下。
TMDB (The Movie DB)
配信国とプロバイダを絞って検索できますがフィルタを設定したときにURL表示が変わらないので、1ページからスクレイピングすることはできません。
TMDB API
配信国とプロバイダを絞ってタイトル一覧を取得するのに都合の良いAPIエンドポイントがなく、何万回かのリクエストをすれば欲しいデータをつくれるかもしれませんが、利用制限に引っかかりそうでサーバに負荷をかけそうなので現実的ではありませんでした。
JustWatch.com
配信国はURLで決まり、プロバイダを絞って配信タイトル一覧を表示できます。
https://www.justwatch.com/jp/動画配信サービス/netflix
利用規約には特にスクレイピングに関しての記載がありません。
また、~/robots.txt (スクレイピングのルールを記載するページ)では
User-agent: *
Disallow:
すべてのスクレイパーに対してDisallow がないので、全ページにスクレイピングを許可しています。
JustWatch を使って Netflix をジャストウォッチすることにします。
実装
Vue.js を使っているサイトなので、ブラウザインスペクターで見れるHTMLコードをスクレイピングするためにはJavaScriptの実行が必要です。
そこで、Puppeteerというヘッドレス(GUIのない) Chromium browser の自動化ライブラリを使います。
Puppeteer インストール
$ npm i puppeteer
ひとまず、Puppeteerを使って対象ページにアクセスし、タイトルテキストの入っているHTMLタグを取得してタイトルテキスト部分を抽出します。
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
オプションで、ネットワーク接続がなくなるまで待機しています。
出力
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.
- ...
フィルタで表示件数を絞ったページをクローリングしつつスクレイピングすることで全てのタイトルを得ることにします。
試行錯誤を経て、以下のコードを書きました。
スクレイピングスクリプト
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有りで記述した操作どおりに動作します。
前略
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ファイル(共有・公開はしない)にしてタイトルテキストのようなリンクにしたい
点で、実用のために改善したいです!