🦕

Deno で始めるスクレイピング講座

2023/10/25に公開

初めに

皆様スクレイピングは知っていますか?

スクレイピングの定義はこうです。

ウェブスクレイピングとは、ウェブサイトから情報を抽出するコンピュータソフトウェア技術のこと。
通常このようなソフトウェアプログラムは低レベルのHTTPを実装することで、もしくはウェブブラウザを埋め込むことによって、WWWのコンテンツを取得する。

要するにブラウザからFetch等で取得するのではなく、
PythonCpp でHTTPリクエストを送信し、レスポンスを解析することでサイトの情報を取得する事です。

Python では BeautifulSoupRequestsSelenium等が有ります。
レスポンスのHTMLソースをDOM解析して情報を取得することが出来ます。

Deno は言わずと知れた JavaScriptランタイムの大御所です。
PythonよりもDOM解析に優れています。

アプローチ

一般的なスクレイピングのアプローチを例に挙げます。

例えば以下の様なソースのサイトが有るとします。 (https://zenn-scraping.deno.dev)

<!DOCTYPE html>
<html>
  <head>
    <!-- ... -->
  </head>
  <body>
    <h1>今日の天気</h1>
    <ul>
      <li><span id="city">Tokyo</span><span id="weather">晴れ</span></li>
      <li><span id="city">Osaka</span><span id="weather"></span></li>
      <li><span id="city">Nagoya</span><span id="weather">曇り</span></li>
    </ul>
  </body>
</html>

ここから 都市天気 を含んだ配列を生成したいと思います。

目標物

[
  {
    name: "Tokyo",
    weather: "晴れ"
  },
  {
    name: "Osaka",
    weather: "雨"
  },
    name: "Nagoya",
    weather: "曇り"
  },
]

まず 都市の名前と天気が書いてある liタグ までのセレクターを考える必要が有ります。

bodyタグ から順に辿りましょう。

<body>
    <h1>今日の天気</h1>
    <ul>
      <li><span id="city">Tokyo</span><span id="weather">晴れ</span></li>
      <li><span id="city">Osaka</span><span id="weather"></span></li>
      <li><span id="city">Nagoya</span><span id="weather">曇り</span></li>
    </ul>
</body>

body の下に ul が有るので body > ul
その中に li が有るので body > ul > li です。

実はこの作業は開発者ツールをうまく使う事で短縮できます。

!

取得したい要素の上にフォーカスを置き、右クリック。
コピーから selector をコピーを押すことでその要素までの一意のセレクターを生成できます。

ではセレクターが body > ul > liだと分かったので Deno を動かしていきます。
(Denoのインストール方法や基礎知識はこちらのサイトが分かりやすいです。)

Denoではfetchが組み込みで動きます。

まずは https://zenn-scraping.deno.dev にリクエストを送り、HTMLのソースを取得してみます。

fetch("https://zenn-scraping.deno.dev").then(resp => resp.text()).then(source => {
    console.log(source);
})

これで https://zenn-scraping.deno.dev のHTMLソースを取得することが出来ました。

コンソールには

<!DOCTYPE html>
<html>
  <head>
    <!-- ... -->
  </head>
  <body>
    <h1>今日の天気</h1>
    <ul>
      <li><span id="city">Tokyo</span><span id="weather">晴れ</span></li>
      <li><span id="city">Osaka</span><span id="weather"></span></li>
      <li><span id="city">Nagoya</span><span id="weather">曇り</span></li>
    </ul>
  </body>
</html>

と流れているはずです。
ではここからいつも通り document.querySelector() を使って取得します。

...

使えません。

なぜなら documentはWebAPIの組み込みであり、ブラウザで動いているDOMを指すオブジェクトです。

DOMParser

今回セレクターで探したいのは source の部分です。

しかしこれはただのテキストです。
これを DOM解析して DOM オブジェクト に直す必要が有ります。
そこでどうするか。
DOMParserという便利な WebAPI組み込み関数が有ります。

new DOMParser().parseFromString("<body><p>Hi! </p></body>", "text/html")
という感じでDOMオブジェクトにすることが出来ます。
しかし DOMParser()WebAPI の組み込み関数であり、Deno等のランタイムには有りません。

ではどうするか。

Rust (wasm) で実装されたライブラリが有ります。

import {
  DOMParser
} from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";

とすることで使えます。

では続きを書きます。

import {
  DOMParser
} from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";

fetch("https://zenn-scraping.deno.dev").then(resp => resp.text()).then(source => {
    const DOM = new DOMParser().parseFromString(source, "text/html")
})

querySelector

これで DOMには https://zenn-scraping.deno.dev の HTMLソースを DOMオブジェクトに変換した物が入りました。
ここからは通常通り querySelector を使っていきます。
( querySelector が分からない人はこちら)

今回は複数取得したいので querySelectorAll を使います。

import {
  DOMParser
} from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";

fetch("https://zenn-scraping.deno.dev").then(resp => resp.text()).then(source => {
    const DOM = new DOMParser().parseFromString(source, "text/html")
    const Targets = DOM.querySelectorAll("body > ul > li")
})

これで Targetsには li 内の三つのオブジェクトが入りました。

結果を入れる Results という変数を用意します。

import {
  DOMParser
} from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";

fetch("https://zenn-scraping.deno.dev").then(resp => resp.text()).then(source => {
    const DOM = new DOMParser().parseFromString(source, "text/html")
    const Targets = DOM.querySelectorAll("body > ul > li")
    
    const Results = []
})

そして Targetsの全ての要素に対し都市の名前と天気を取得し、Resultspushする処理を書きます。

完成

import {
  DOMParser
} from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";

fetch("https://zenn-scraping.deno.dev").then(resp => resp.text()).then(source => {
    const DOM = new DOMParser().parseFromString(source, "text/html")
    const Targets = DOM.querySelectorAll("body > ul > li")
    
    const Results = []
    
    Targets.forEach(el => {
        const name = el.querySelector("#city").innerText
        const weather = el.querySelector("#weather").innerText
	
	Results.push({
	   name, weather
	})
    })
    
    console.log(Results)
})

これで完成です。
コンソールには以下のように出てるはずです。

[
  {
    name: "Tokyo",
    weather: "晴れ"
  },
  {
    name: "Osaka",
    weather: "雨"
  },
    name: "Nagoya",
    weather: "曇り"
  },
]

最後に

スクレイピングはサイトのよっては負荷防止等の目的で禁止されています。
良く規約を呼んで実行してください。

良いスクレイピングライフを

Discussion