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

# 初めに

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

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

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

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

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

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

## アプローチ

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

例えば以下の様なソースのサイトが有るとします。 (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>
```

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

#### 目標物
```js
[
  {
    name: "Tokyo",
    weather: "晴れ"
  },
  {
    name: "Osaka",
    weather: "雨"
  },
    name: "Nagoya",
    weather: "曇り"
  },
]
```

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

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

```html
<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` です。

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

![](https://storage.googleapis.com/zenn-user-upload/7161b1213745-20231025.png)!

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

ではセレクターが `body > ul > li`だと分かったので Deno を動かしていきます。
(Denoのインストール方法や基礎知識は[こちら](https://yoshixmk.github.io/deno-manual-ja/getting_started/installation.html)のサイトが分かりやすいです。)

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

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

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

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

コンソールには

```bash
<!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)** で実装されたライブラリが有ります。

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

とすることで使えます。

では続きを書きます。

```js
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` が分からない人は[こちら](https://www.sejuku.net/blog/70581))

今回は複数取得したいので  `querySelectorAll` を使います。 
```js
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` という変数を用意します。

```js
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の全ての要素に対し都市の名前と天気を取得し、`Results` に`push`する処理を書きます。

## 完成

```js
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)
})
```

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

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

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

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