これは 高知工科大 Advent Calendar 2018 5日目の記事です
みなさんこんにちは.最近1番コード書いてるのに何故かGitHubには草が生えないnnksです.
この記事ではみなさんおなじみ顔本ページの投稿をWebスクレイピングしたことを書いていきたいと思います.
書いてたらなんか無駄に長くて技術記事と言うよりかはブログみたいな感じになったけど許して
ことの発端は顔本のGraphAPI仕様変更です.
ITmedia: Facebook、サードパーティアプリがアクセスできる個人情報を大幅制限へ
oh...まじか:weary:
これによって顔本ページの投稿を取得するPage APIを使用するにはクッソめんどくさい認証を突破しなければなりません.
自分はただひなビタ♪のページの投稿を取得するだけのスクリプトが動かせればいいのに...どうしてこうなった.
しかしくよくよしててもPage APIは帰ってきません.
だから仕方なくスクレイピングしました.
- 顔本の投稿取得 <- この記事のメイン
- 投稿のcsv化
この記事の主な内容はこれです.なんとかして顔本の投稿を取得しなければなりません.
取得の方法としては,
htmlを取得->そのhtmlから欲しい情報(投稿された文章)を抽出->csv化
みたいな感じで取得しようと思います.そのためにまずはhtmlを取得するところから始めようと思います.
顔本では無限スクロールを採用しています.twitterでも採用されていますね.
過去の投稿が自動的に更新され,貴重な時間を多大に消費させられる非常に悪意のあるUIです.
無限スクロールってことは全ての投稿があるhtmlを取得するには,1番最初の投稿までスクロールすればおっけーってことですね.
最初は愚直なやり方で取得するのが基本です.
なので頑張って自動スクロールで1番下に行くまで放置しようとしたのですが,謎の力によって自動スクロールでは記事が更新されなかったので頑張って手動でやりました.
やってみた所感としては
- とりあえず全ての投稿があるhtmlを取得できたので良かった:relaxed:
- 重い.Chrome落ちるかと思った:tired_face:
- 長い.とにかく1番下まで行くのに時間がかかる:innocent:
数百件とかだとすぐ終わるんですが,自分が取得しようとしているページは2000件以上の投稿があるのでとにかく重くて長かったです.htmlの大きさが37MBあった.一つだけ確実に分かったのは人間のすることじゃないってことですね.
なので自動化していきたいと思います.
まずはそれっぽいリクエストを見つけてクエリ解析して,疑似的に無限スクロールができるようにリクエストを投げようと思ったんですが解析で断念.それっぽいリクエストのクエリ見ても何してるか全然分からんかった.
そこでブラウザを使わずにブラウザ操作が出る奴(?)ないんかな〜って思い探してたらヘッドレスブラウザとかいうそれっぽいものにたどり着きました.案外あるもんですね.
ヘッドレスブラウザとはGUIを使わずにAPIを使って操作するブラウザのことです.自分の要望にはドンピシャでした.
そしてPuppetterとかいうnode.jsでヘッドレスブラウザを操作できるライブラリがあったのでこれで自動化に挑戦していきたいと思います.
実行環境
- node 11.2.0
- npm 6.4.1
- puppeteer 1.10.0
とりあえずサンプルを動かしてみる
公式のやつ
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});
await browser.close();
})();
すごい.ちゃんとスクショ撮れてる
htmlはこれ使ったら取得できた.やったね.
次は1番大事なスクロール.
PuppetterのAPIドキュメントを探してもそれっぽいものが見つからなかったけどIssuesを覗いてみると同じ思いをしている人は過去にいたっぽい
page.scroll()?
evaluateを使うといい感じにスクロールできて,かつ記事も更新されてた.
最初のhtml取得時もJavaScriptのスクリプト動かせば良かったのでは?🤔と記事を書いてる途中に思った.愚直すぎた:innocent:
とりあえずスクロール->2秒待つを2000回繰り返してみるスクリプトを書いた
const puppeteer = require('puppeteer');
function wait (ms) {
return new Promise(resolve => setTimeout(() => resolve(), ms));
}
(async () => {
var loopflg = true;
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.facebook.com/hinabitter');
for (var i = 0; i < 2000; i++) {
await page.evaluate(_ => {
window.scrollBy(0, window.innerHeight);
});
await wait(2000);
}
let html = await page.content();
console.log(html);
await browser.close();
})();
time測りつつ実行
$ time node index.js > index.html
→やってみたら取れてた.やったね.
timeコマンドで時間計ってたけど1時間30分ぐらいかかってた.まぁそんなもんか.
これで晴れて自動的にhtmlの取得をできるプログラムができました.
ぶっちゃけ上まででちゃんと取れるんですが,ちょっとだけ攻めて取得時間の短縮もしました.2000回繰り返すとか明らかに無駄じゃん...
PuppeteerにはsetRequestInterceptionと言うメソッドがありこれを使えばリクエストの監視ができるようになるんで,リクエストの中に特定のクエリがあった時にループを停止するとうまく行くと思うんでやって見た.
きっと最後の投稿をリクエストする時になんらかのクエリを投げるはず.と思い調べてみたらそれっぽいものはなかった:innocent:
しかし運のいいことに最後の投稿かを見分けれる†謎の数列†があったので,それが出るまでループを行うようなスクリプトを少ない知識でゴリゴリ書きました.
イベントドリブン型のプログラミングを書き慣れてないからうまく書けん:sweat:
const puppeteer = require('puppeteer');
function wait (ms) {
return new Promise(resolve => setTimeout(() => resolve(), ms));
};
function getHostname (path) {
return path.split('/')[2];
};
function isNextPage(url) {
var getQueryToJson = function (url) {
return JSON.parse(unescape(url.split("&")[1]).slice(7));
}
// is query request
if (url.split('/')[3] !== 'pages_reaction_units')
return true;
json = getQueryToJson(url);
if (json['timeline_section_cursor'] != null &&
json['timeline_section_cursor']['end'] == 1357027199 /* 謎の数列 */)
return false;
else
return true;
}
(async () => {
var loopflg = true;
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);
await page.on('request', interceptedRequest => {
const url = interceptedRequest.url();
if (getHostname(url) === 'www.facebook.com') {
if (!isNextPage(url))
loopflg = false;
}
interceptedRequest.continue();
});
await page.goto('https://www.facebook.com/hinabitter');
while (loopflg) {
await page.evaluate(_ => {
window.scrollBy(0, window.innerHeight);
});
await wait(500);
}
let html = await page.content();
console.log(html);
await browser.close();
})();
もっかいtime測りつつ実行
$ time node index.js > index.html
→46分ぐらいで終わってた.まぁアニメ2本分と考えればよし.
これで実行時間をできるだけ短くできた.
投稿のcsv化は夏あたりに実装していました.
rubyのnokogiriを使ってギコギコやってましたが,これはその内Puppeteerで書き直すかも.
nokogiriってオプションつけると30MBのhtmlでも読み込めるのすごい.しかも結構早い
今回やってきて1番詰まったのはnode.jsというかJavaScriptの知識の無さでした.研究で使ってるのに:innocent:
返り値としてPromiseを返すって言ってるけどPromiseってなんなん?って感じでした.今もわからんけど.
そこらへんはサンプルプログラムや記事を見よう見まねで頑張って書いて,JavaScript力あげなきゃな〜と感じました.
ちなみにPuppeteerの使い勝手はとても良かったです.スクレイピングの他にもテストなどにも持ってこいですし,node.js使える人ならばweb上でのいろんな作業を自動化できそうですね.