◆ ブラウザでやると簡単なのでエクステンションを使ってやってみます
◆ JavaScript を実行後の状態からデータ取得もできます 

Node.js で ウェブスクレイピングをするという記事を見かけて JavaScript でやるなら最初からブラウザでいいんじゃないのと思いました
もちろん結果を DB などローカルに保存するなら ブラウザでスクレイピングして結果を POST 送信して サーバサイドで DB へ保存 なんてするより Node.js で集めてそのまま DB 登録のほうがいいですし 用途次第です

ですが http のリクエストがあったときに サーバサイドで別ページをスクレイピングして結果を返すなんてするくらいなら最初から ブラウザでやればいいとなりますよね

ブラウザでやる時の問題点が CORS です
別のオリジンのサイトを fetch や xhr で取得できないという面倒な仕組みです
ただ 通常の JavaScript でできなくてもブラウザエクステンションだとそういう制限を受けずに好きなサイトからデータを取得できます
ウェブスクレイピングする目的って基本的に個人的な用途でこういうデータ集めたい とか アプリの一部で外部の情報が必要という場合ですので エクステンションで困ることはあまりないと思います

エクステンション作るのはハードル高いとか それならサーバ通したほうが早いとか言う人もいると思います
ですが Chrome のエクステンションは簡単に作れますし サーバ用意するより遥かに簡単です

と言ってはみたものの ユーザが入力した URL のページタイトルを取得したい など公開サービスのシンプルなものだとわざわざエクステンション入れるのも手間なだけな場合もあるので エクステンションをインストールせずページ開くだけで使えるようにしたい場合は仕方なくサーバにしてください

ウェブスクレイピングって

HTML ソースを直接取ってきて そこから必要な部分を取り出すものです

自分でソースをパースしたり正規表現を頑張ったり
最近はそんなことしなくても使いそうなデータは扱いやすい形式で用意されてることも多く ウェブスクレイピングする機会はほとんどないです

エクステンション

とりあえず manifest.json ファイルを作ります
今回はいつでもサッと使える browser action とします
右上のボタン押すやつです

[manifest.json]
{
"name": "tes",
"version": "1.0",
"browser_action": {
"default_popup": "popup.html"
},
"permissions": ["http://*/*", "https://*/*", "activeTab", "tabs"],
"manifest_version": 2,
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}


次にボタンを押した時出てくるポップアップ部分です
今回は css なしで html と JavaScript だけで作ります

[popup.html]
<!doctype html>
<meta charset="utf-8">
<script src="popup.js" defer></script>

<input type="url" id="url">
<textarea id="function"></textarea>
<input type="button" id="btn-wojs" value="without-js">
<input type="button" id="btn-wjs" value="with-js">

HTML は URL と 関数をいれる input とボタンだけです

[popup.js]
$ = document.querySelector.bind(document)

$("#btn-wojs").onclick = function(){
withoutJS(common())
}
$("#btn-wjs").onclick = function(){
withJS(common())
}

function common(){
var url = $("#url").value
var fn_str = $("#function").value

try{
var func = null
eval(`func = ${fn_str}`)
}catch(e){
alert(e.message)
return
}
return {url, func}
}


function withoutJS(opt){
fetch(opt.url).then(e => e.text()).then(getValue)

function getValue(text){
var doc = new DOMParser().parseFromString(text, "text/html")
alert(opt.func(doc))
}
}

function withJS(opt){
var code = `
var func = ${opt.func}
chrome.runtime.sendMessage(func(document))
`

var newtab_id = null
chrome.tabs.create({url:opt.url, active:false}, e => {
newtab_id = e.id
chrome.tabs.executeScript(e.id, {code, runAt: "document_end"})
})

chrome.runtime.onMessage.addListener(function listener(val) {
chrome.tabs.remove(newtab_id)
chrome.runtime.onMessage.removeListener(listener)
alert(val)
})
}

JavaScript はこんなのです

この 3 つのファイルを一つのフォルダに入れたら
chrome://extensions/
を開いて 右上の「デベロッパーモード」にチェックして「パッケージされていない拡張機能を読み込む...」を選びます

そこでさっきのフォルダを選ぶと 拡張機能がインストールされます
右上の [t] ボタンを押すとこんなふうになると思います

extws01


t なのは拡張機能名が tes になってるからです

試しにこのブログトップページにある記事へのリンクを取得してみます

上の input には
http://var.blog.jp

textarea には
function(doc){
var arr = Array.from(doc.querySelectorAll(".article-title a"), e => e.textContent)
return JSON.stringify(arr)
}

と書きます
ページ内の全部の article-title クラス以下の a タグの textContent を配列で取得して JSON 形式のテキストを返すという処理の関数です

これで without-js ボタンを押すと

extws02

こんな alert が出てくるはずです
配列のテキスト形式で記事タイトルが取れていますね


HTML 自体から取れる情報なら簡単ですが 最近では JavaScript が実行されること前提で JavaScript を動かさないとほしい情報がとれないことも増えていますよね

さっきの without-js ボタンを押したときの処理はこうなっています
function withoutJS(opt){
fetch(opt.url).then(e => e.text()).then(getValue)

function getValue(text){
var doc = new DOMParser().parseFromString(text, "text/html")
alert(opt.func(doc))
}
}

URL の HTML を fetch してきて DOMParser を使って DOMTree を作ってから 関数にそのツリーのルートを渡しています
なので 2 つめの input の関数では引数に document を受け取って何か処理をして値を取り出して その結果を返すという処理を書くだけでした

HTML をパースしたものからデータを取り出しているだけなので JavaScript の処理は動いていません


もうひとつの with-js ボタンでは JavaScript を実行した結果を取得できます
with-js を押したときの処理は
function withJS(opt){
var code = `
var func = ${opt.func}
chrome.runtime.sendMessage(func(document))
`

var newtab_id = null
chrome.tabs.create({url:opt.url, active:false}, e => {
newtab_id = e.id
chrome.tabs.executeScript(e.id, {code, runAt: "document_end"})
})

chrome.runtime.onMessage.addListener(function listener(val) {
chrome.tabs.remove(newtab_id)
chrome.runtime.onMessage.removeListener(listener)
alert(val)
})
}
となっています

新しいタブで URL を開いて ロードが終わったら 2 番目の input に書いた関数を 開いたタブ内に作って実行しています
返り値は sendMessage されて popup.html に戻ってきます
メッセージを受け取ったら タブを閉じて 結果を表示します

↓のページは JavaScript で title や body を生成しています
http://var.blog.jp/s/html-by-js.html

このページに
function (doc){
return doc.title
}
この処理を実行します

without-js のときは

extws03

空文字になっています

では with-js のときは

extws04

タイトルが取得できています
js を動かした場合の取得も簡単にできますね


with-js のほうは新しくタブを開いてます
これをできれば iframe にしたかったのですが 拡張機能を使っても iframe との直接通信はできないようでした
iframe に対して executeScript をしたくても ポップアップはタブじゃないですし タブを持ったページにしても chrome-extension: スキームになるので コードの埋め込みができません

タブはバックグラウンドで開くようにしているので 大量に同時に実行しなければ特に問題でないと思います

まとめ

ブラウザ(エクステンション)ですると JavaScript を実行した後の DOM からデータの取り出しも簡単にできます!