Node.js でスクレイピングしてクライアントに返す

映画のレビューを書いている別サイト「そんなには褒めないよ。映画評」に映画レビューサイト「Filmarks」の自分の投稿へ飛ぶ疑似ソーシャルボタン(リンクボタン)をつくってみようと思います。



Filmarks

Filmarks とは、

Filmarks(フィルマークス)は「いい映画と出会おう。」をコンセプトにした国内最大級の映画レビューサービスです。映画レビューをチェック・投稿できる機能をベースに「映画探し」「観たい映画のメモ・備忘録」「鑑賞記録ノート」「映画の感想や情報をシェアして楽しむコミュニケーションツール」としてご利用いただけます。


API が「KDDI IoTクラウド」というところで公開されていますが当然有料です。

で、スクレイピングということになるのですが、なかなかこれが厄介です。


Filmarks 疑似ソーシャルボタン

Filmarks のいいね!やコメント機能はサイト内の投稿へのものですので Filmarks に投稿がないと意味がありません。ですのでやろうとしていることは Filmarks にも書いていますよというリンクボタンです。

であれば自サイトの記事内に Filmarks への投稿 URL を貼っておけば済むんじゃない? という話です(笑)。が、現時点ですでに423件の投稿があり、今さらひとつひとつにリンクを貼るなんてことはできませんので自動化しようということです。


作成の構想

Python でやってみようかと思いましたがまだ理解が進んでいないことや結局サーバーを立てることになるわけですので、まずは Node.js でやってみてうまくいけば同じことを Python でもできるか試してみようと思います。


まず大まかな流れです。

  • 自サイトの記事から映画タイトル取得
  • Node.js サーバーに Filmarks のユーザ名と映画タイトルを投げる
  • Node.js サーバーで Filmarks 内の自分の投稿を検索する
  • Node.js サーバーで該当映画の自分の投稿記事の URL を構成し自サイトに返す
  • 自サイトで受け取った URL を別ウィンドウで開く


Filmarks サイトの構成、検索可能範囲など

Filmarks に用意されている検索機能は、映画タイトル、俳優名、ユーザー名での単純検索のみで結果もあいまい検索結果です。ですので映画タイトルで検索しても結果は複数返されそこからさらに選択する必要があります。


その他、サイト内の構成でわかることは、

  • マイページは自分の投稿の一覧(36投稿固定/1ページ)
  • マイページの URL は https://filmarks.com/users/(ユーザー名)
  • 記事の URL は https://filmarks.com/movies/(映画ID)/reviews/(投稿ID)
  • ユーザーID はあるが投稿ID とは関連性がなさそう


映画ID とユーザーID で投稿URL が検索できれば簡単なんですが外部からはできなさそうです。

ですので、Node.js サーバー内は次のように進めてみます。


  • https://filmarks.com/users/(ユーザー名) から投稿記事数またはページ数をスクレイピングする
  • https://filmarks.com/users/(ユーザー名)?page=(ページ数) 各ページで(映画タイトル)を検索する
  • 一致する投稿があれば投稿ID を取得する
  • https://filmarks.com/movies/(映画ID)/reviews/(投稿ID) を構成して返す


オリジン間リソース共有(CORS)エラーを確認

Node.js サーバーを立てる前にクライアントから Filmarks にリクエストして、オリジン間リソース共有 (CORS)エラーが出ることを確認してみます。


XMLHttpRequest

マイページの URL は https://filmarks.com/users/ausnichts です。

XMLHttpRequest でマイページを取りにいってみます。


var xhr = new XMLHttpRequest();
xhr.open("GET", "https://filmarks.com/users/ausnichts", true);
xhr.onload = function (e) {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      console.log(xhr.responseText);
    } else {
      console.error(xhr.statusText);
    }
  }
};
xhr.onerror = function (e) {
  console.error(xhr.statusText);
};
xhr.send(null); 

MDN のコードです。


Access to XMLHttpRequest at 'https://filmarks.com/users/ausnichts' from origin 'https://ausnichts.hatenadiary.jp' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

https://ausnichts.hatenadiary.jp はテスト用のサイトです。


クライアントサイドから Javascript で他サイトにデータを要求しますとクロスドメインでアクセスを拒否されます。CORS とは何かをいまさら私が説明しなくてもネット上に丁寧な説明がたくさんありますし、そもそもわかりやすく説明できません(笑)。


Fetch API

fetch を使ってやってみます。


fetch("https://filmarks.com/users/ausnichts")
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.blob();
  })
  .then(myBlob => {
    console.log(myBlob);
  })
  .catch(error => {
    console.error('There has been a problem with your fetch operation:', error);
  });

MDN のコードです。


Access to fetch at 'https://filmarks.com/users/ausnichts' from origin 'https://ausnichts.hatenadiary.jp' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.


オプションに mode: 'no-cors' を設定してみます。


fetch("https://filmarks.com/users/ausnichts", { mode: 'no-cors' })
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.blob();
  })
  .then(myBlob => {
    console.log(myBlob);
  })
  .catch(error => {
    console.error('There has been a problem with your fetch operation:', error);
  });


CORS エラーは出ませんが、レスポンスがありません。

There has been a problem with your fetch operation: Error: Network response was not ok


サーバーを立てるしかない

ということでサーバーを立てるしかないということです。

ローカルの Node.js でやってみます。


const client = require('cheerio-httpcli');

var p = client.fetch('https://filmarks.com/users/ausnichts');
p.then(function (result) {
  // レスポンスヘッダを参照
  console.log(result.response.headers);
 
  // HTMLタイトルを表示
  console.log(result.$('title').text());
 
})
 
p.catch(function (err) {
  console.log(err);
});
 
p.finally(function () {
  console.log('done');
});

cheerio-httpcli - npm のコードです。


{
  date: 'Mon, 14 Sep 2020 03:53:19 GMT',
  'content-type': 'text/html; charset=utf-8',
  'transfer-encoding': 'chunked',
  connection: 'close',
  server: 'nginx',
  vary: 'Accept-Encoding, User-Agent',
  'x-frame-options': 'SAMEORIGIN',
  'x-xss-protection': '1; mode=block',
  'x-content-type-options': 'nosniff',
  'x-download-options': 'noopen',
  'x-permitted-cross-domain-policies': 'none',
  'referrer-policy': 'strict-origin-when-cross-origin',
  etag: 'W/"54ed2c46eafc24a4412177e8cfe61296"',
  'cache-control': 'max-age=0, private, must-revalidate',
  'set-cookie': [
    'uuid=9df80dc0-e793-4133-857b-902f0e3f508a; path=/; expires=Fri, 14 Sep 2040 03:53:19 GMT; secure',
    '_filmarks_v2_session=052889d4fa84fb679859e77209472f3a; path=/; expires=Sun, 13 Dec 2020 03:53:19 GMT; secure; HttpOnly'
  ],
  'x-request-id': '70178ae2-3ece-42a2-9a66-8b3c548c3d8f',
  'x-runtime': '0.187742',
  'strict-transport-security': 'max-age=31536000',
  'content-encoding': 'gzip'
}
ausnichtsさんの映画レビュー・感想・評価 | Filmarks映画
done

スクレイピングできました。


あとはスクレイピングしたデータから自分の投稿 URL を探し出すだけということになります。

続きは次回(下記リンクです)。