Common LispでGoogleの検索結果を取得

# Common LispでGoogleの検索結果を取得

## はじめに

Webコンテンツのスクレイピングを行う取っ掛かりとして、Googleの検索結果の一覧、タイトルとURLを取得するコードを書いたまとめです(Google APIは使わない系です）。

次の環境で話を進めます。

- Debian 10
- ECL 開発版
- GNU Guix
- Emacs SLIME
- Deepspace

## 使用するライブラリ

- HTTP GETリクエスト
- 取得したHTMLのパース
- HTML要素の値、属性などの取得

上記の処理を行うために、次のCommon Lispライブラリを利用しました。

- [quri](https://github.com/fukamachi/quri) : URI操作のためのユーティリティ
- [dexador](https://github.com/fukamachi/dexador) : HTTP Client
- [plump](https://github.com/Shinmera/plump) : HTML Parser
- [clss](https://github.com/Shinmera/clss/) : CSSライクな記法でHTML要素、属性を取得できるライブラリ
- [cl-ppcre](https://edicl.github.io/cl-ppcre/) : 正規表現ライブラリ

これらのライブラリは、全てGNU Guixのデフォルトのチャネルに登録されているため、次のコマンドで全てインストールできます。

```bash
$ guix package -i cl-quri cl-dexador cl-plump cl-clss cl-ppcre
```

## 必要なファイルの作成

ただファイルを作るだけなんですが自作ツールの[Deepspace](https://gitlab.com/hu.moonstone/deepspace)を使っているので、それで進めます。もし他のツールを使っている場合も、ASDの書き方等変わらないと思うので、大体一緒だと思います。

```bash
$ deepspace -t project -o create -n ok-google
```

で、ok-googleプロジェクトを作成します。以下のような形でディレクトリが作成され、トップに`ok-google.asd`、`src/lisp`以下に`package.lisp`、`ok-google.lisp`ファイルが作成されます。

```bash
$ tree -d
.
├── build
├── doc
├── lib
├── resources
│   ├── data
│   ├── images
│   └── sounds
├── src
│   ├── c
│   └── lisp
└── test
    ├── c
    └── lisp
```

asdファイルは以下のように記述します。depends-onに利用するライブラリの名前を追加します。

```common-lisp:ok-google.asd
(asdf:defsystem :ok-google
  :serial t
  :pathname "src/lisp"
  :components ((:file "package")
               (:file "ok-google"))
  :depends-on (:asdf
               :quri
               :cl-ppcre
               :plump
               :clss
               :dexador))
```

`package.lisp`には外部に公開する関数を記述します。今回は特別手を入れません。

```common-lisp:src/lisp/package.lisp
(in-package :cl-user)
(defpackage :ok-google
  (:use :cl)
  (:export :main))
```

`ok-google.lisp`に実際の処理を記述します。

```common-lisp:src/lisp/ok-google.lisp
(in-package :ok-google)
(defun main () )
```

Deepspaceを使っている場合には標準でmain関数が定義されているため、一旦この中にGoogleにリクエストを投げる処理を記述してみます。

## HTTPリクエストの処理

### リクエストURLの作成

HTTPリクエストを送信するため、まずはURIを構築する必要があります。`quri:make-uri`を使うと、以下のように記述できます。

```common-lisp
(quri:make-uri :scheme "https"
               :host "www.google.com"
               :path "/search"
               :query '(("q" . "Common Lisp")
                        ("ie" . "UTF-8") ;; 検索クエリの文字コード
                        ("oe" . "UTF-8") ;; 検索結果の文字コード
                        ("gbv" . "1") ;; no use JavaScript
                        ("safe" . "medium") ;; セーフサーチ
                        ("pws" . "0") ;; パーソナライズ無効
                        ("hl" . "ja") ;; 表示言語
                        ("lr" . "ja") ;; 検索対象の言語
                        ("num" . "20") ;; 1ページあたりの検索
                        ))
```

静的なHTMLしか取得できないため、`gbv`のオプションに1を指定することで、JavaScriptを検索結果の画面で使わないようにしておきます。作成したURIは、`quri:render-uri`を使うことで文字列に変換できます。上記の式の結果が`request-url`という変数にセットされている場合、

```common-lisp
(quri:render-uri request-url)
```

で、実際のURLの文字列を取得できます。検索キーワードに日本語を指定していても、自動的にエンコードされるため、特別こちらで何か変換を行う必要はありません。

### HTTPリクエストの送信

リクエストは、dexadorの`dex:get`を使用します。引数には先程の`quri:render-uri`を使って文字列に変換したURLを指定します。以下の例では、結果のHTMLソースが`html-source`という変数にセットされます。

```common-lisp
(let ((html-source (dex:get (quri:render-uri request-url))))
  ;; 処理
  )
```

## HTMLのパースと要素の取得

### plumpによるHTMLのパース
plumpの、`plump:parse`というオペレーターでHTMLをパースします。

```common-lisp
(let* ((html-source (dex:get (quri:render-uri request-url)))
      (parsed-html (plump:parse html-source)))
  ;; 処理
  )
```

### clssで要素の取得

JavaScriptをオフにしたブラウザの開発者ツールでGoogleの検索結果画面を確認するのが一番わかりやすいかもしれませんが、タイトルとそのURLを取得するために、要素のクラス名を把握しておきます。すぐ変わってしまうかもしれないのですが、現時点(2020年3月)では、`div.BNeawe.vvjwJb.AP7Wnd`というセレクタで検索結果の各サイトのタイトル、`div.ZINbbc.xpd.O9g5cc.uUPGi > div.kCrYT:first-child > a:first-child`で、各サイトのリンクを取得できます。

clssは、CSSのセレクタがそのまま使えるため、上記の指定をそのまま流用できます。第1引数にセレクタ、第2引数にパース済みのHTMLを渡すと、マッチする要素が配列で取得できます。

```common-lisp
(clss:select "div.BNeawe.vvjwJb.AP7Wnd" parsed-html)
```

セレクタが間違っていると不要なものも取得してしまったり、何も取得できなかったりするので、複雑なセレクタを指定しなければならない場合は、`parsed-html`をダイナミック変数として定義しておき、REPL上で実行しながら確認すると調整しやすいです。

### リンクの取得

先程のセレクタで、サイトタイトルはそのまま取得できますが、リンクは、href属性を取ってくる必要があります。属性は、`plump:attribute`で取得できます。

```common-lisp
(plump:attribute (aref links 0) "href")
```

第1引数の`(aref links 0)`は、配列として返ってきたリンク要素の0番目を指定しています。第2引数で、その要素のどの属性の値を取得するのかを指定します。

が、ここで取得できるhref属性に書かれているURLは、`www.google.com/url?q=https://〜`という感じで始まるアドレスで、サイト本来のURLはqのパラメータとして指定されています。また、後ろには`&sa=`というパラメータが付与されます。サイトのURLを取得したいので、ここは正規表現を使ってマッチさせます。

## 正規表現でサイト本来のURLを取得

`/url?q=https://example.com/aaa/bbb&sa=〜`というような値の中で、ほしいのは`q=`以降、`&sa`より前の部分です。正規表現には、cl-ppcreの`cl-ppcre:scan-to-strings`を使います。

```common-lisp
(cl-ppcre:scan-to-strings "\/url\\?q=(.*)\&sa" url)
```

第1引数にパターン、第2引数にURLの文字列を渡します。このオペレーターは複数の値を返してきます。`(.*)`の部分にマッチする文字列は、2番目に配列として格納されるため、multiple-value-bindを使って次のように書きます。

```common-lisp
(let ((url (plump:attribute link "href")))
  (multiple-value-bind (match strings)
      (cl-ppcre:scan-to-strings "\/url\\?q=(.*)\&sa" url)
    (setf (aref results i) (aref strings 0))))
```

これで、URLの部分だけを取得してくることに成功します。パース後の処理は、以下のように記述しています。

```common-lisp
(let ((results (make-array 20 :element-type 'string))
      (links (clss:select "div.ZINbbc.xpd.O9g5cc.uUPGi > div.kCrYT:first-child > a:first-child" parsed-html)))
  
  (dotimes (i (length links))
    (let ((link (aref links i)))
      (let ((url (plump:attribute link "href")))
        (multiple-value-bind (match strings)
            (cl-ppcre:scan-to-strings "\/url\\?q=(.*)\&sa" url)
          (setf (aref results i) (aref strings 0))))))
  results)
```



printさせているだけですが、無事次のようにサイトタイトルとURLを取得することができました。

```
"Common Lisp - Wikipedia" 
"https://ja.wikipedia.org/wiki/Common_Lisp" 
"いまから始めるCommon Lisp - Qiita" 
"https://qiita.com/t-sin/items/054c2ff315ec3b9d3bdc" 
"Common Lisp" 
"http://www.math.s.chiba-u.ac.jp/~matsu/cl/index.html" 
"A Road to Common Lisp 翻訳 · GitHub" 
"https://gist.github.com/y2q-actionman/49d7587912b2786eb68643afde6ca192" 
"Common Lispとは？" 
"http://wisdom.sakura.ne.jp/programming/lisp/clisp1.html" 
"Common Lispの勉強をするには、あるいは情報を得るには | κeenの ..." 
"https://keens.github.io/blog/2014/10/20/lisp-ja/" 
"Welcome to Common-Lisp.net!" 
"https://common-lisp.net/" 
"逆引きCommon Lisp - LispHub.jp" 
"https://lisphub.jp/common-lisp/cookbook/" 
"実践Common Lisp - アマゾン" 
"https://www.amazon.co.jp/%25E5%25AE%259F%25E8%25B7%25B5Common-Lisp-Peter-Seibel/dp/4274067211" 
"Common Lisp 入門 - NCT" 
"http://www.nct9.ne.jp/m_hiroi/xyzzy_lisp/abclisp11.html" 
"Portacle : Common Lispのオールインワン開発環境 - SlideShare" 
"https://www.slideshare.net/masatoi0/portacle-common-lisp" 
"Lisp/基本/List - Wikibooks" 
"https://ja.wikibooks.org/wiki/Lisp/%25E5%259F%25BA%25E6%259C%25AC/List" 
"Common Lisp広場 - Scrapbox.io" 
"https://scrapbox.io/common-lisp-ja" 
"[PDF] Common LispでSTGを作りますが何か？ - Usamimi.info" 
"http://www.usamimi.info/~ide/programe/stg_doc/stg-commonlisp.pdf" 
"COMMON LISP JP > Archives > 2020/02/02 - Chaton" 
"http://chaton.practical-scheme.net/common-lisp-jp/a/yesterday" 
"GNU Common Lispを使う。" 
"http://www.takeoka.org/~take/ailabo/gcl/gcl.html" 
"Common Lispとは - Weblio辞書" 
"https://www.weblio.jp/content/Common%2BLisp" 
"Common Lisp によるファジィ集合処理システム - J-Stage" 
"https://www.jstage.jst.go.jp/article/fss/34/0/34_110/_pdf/-char/en" 
"common-lisp - common-lispの使い方 | common-lisp Tutorial" 
"https://riptutorial.com/ja/common-lisp" 
"COMMON LISP 第2版 / Guy L.Steele Jr. 著 井田 昌之 翻訳監修 | 共立 ..." 
"https://www.kyoritsu-pub.co.jp/bookdetail/9784320025882" 
```


## おわりに

本格的なスクレイピングのためにはJavaScriptを使う前提で、Seleniumとヘッドレスブラウザを使ったほうが良いと思うので、次はそれをやってみたいと思います。