田舎で並行処理の夢を見る

試していることなど...需要がないかもしれないけど細々とアウトプットしてます

elixirでクソ簡単にAmazonレビューをスクレイピングする

用意するもの

  • elixir(1.8.1でやってます)
  • mix(elixirのビルドツール。クソ便利)
  • HTTPoison(HTTPクライアント)
  • Floki(HTMLパーサー)

事前準備について

elixirのインストールについては割愛します
公式のドキュメントみた方が圧倒的ッ!に早いです こちらに各環境でのインストール方法が記載されています

mixについてはelixirをインストールした際に共にインストールされています
nodeインストールするとnpm入ってるのと同じですね
試しに以下コマンドを打つ

mix help
mix                   # Runs the default task (current: "mix run")
mix app.start         # Starts all registered apps
mix app.tree          # Prints the application tree
mix archive           # Lists installed archives
mix archive.build     # Archives this project into a .ez file
mix archive.install   # Installs an archive locally
mix archive.uninstall # Uninstalls archives
mix clean             # Deletes generated application files
mix cmd               # Executes the given command
mix compile           # Compiles source files
mix deps              # Lists dependencies and their status
:
:

大丈夫そうですね

新規のプロジェクトを立ち上げる

mixのコマンドを使うことでプロジェクトを生成することが可能です
今回は「amazon_scraping」という名前のプロジェクトを立ち上げるとしましょう

mix new amazon_scraping

mix newコマンドによって各ファイルが生成されました

* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/amazon_scraping.ex
* creating test
* creating test/test_helper.exs
* creating test/amazon_scraping_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd amazon_scraping
    mix test

Run "mix help" for more commands.

とりあえずcdコマンドで場所だけ移動しておきましょう

cd amazon_scraping

現状ではtestはどちらでもいいかな(後にちゃんとやる

ライブラリのインストール

生成されたプロジェクトディレクトリの直下にmix.exsというファイルがあります
このファイルを開きます ./amazon_scraping/mix.exs

defmodule AmazonScraping.MixProject do
  use Mix.Project

  def project do
    [
      app: :amazon_scraping,
      version: "0.1.0",
      elixir: "~> 1.8",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end
end

これ何なん?という質問に直球で答えると設定ファイル(プロジェクトの情報群)だと思ってください
今回の目的に合わせて答えると、このファイルに使用したい外部ライブラリを記述することでinstallがすることが可能となります
なので、この関数内に

# Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end

HTTPoisonFlokiを追加
インストールするバージョンは上記リンクgitのREAD.MEを見れば大体分かります
こちらは2019年4月での最新バージョンをインストールしています

# Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
      {:httpoison, "~> 1.4"},
      {:floki, "~> 0.20.0"}
    ]
  end

ファイルを保存して、下記コマンドを叩きます

mix deps.get

このコマンドを打つことでmix.exsファイルに記述されている外部ファイルをダウンロードしてくれます
当然ながら、通信環境下でないと失敗します

Resolving Hex dependencies...
Dependency resolution completed:
Unchanged:
  certifi 2.5.1
  floki 0.20.4
  hackney 1.15.1
  html_entities 0.4.0
  httpoison 1.5.0
  idna 6.0.0
  metrics 1.0.1
  mimerl 1.2.0
  mochiweb 2.18.0
  parse_trans 3.3.0
  ssl_verify_fun 1.1.4
  unicode_util_compat 0.4.1
All dependencies are up to date

ついでに依存関係のダウンロードもしてくれるので色々とゴチャゴチャしてますが
無事にダウンロードしてくれたようですね
ただ、まだコンパイルされていないので

mix deps

と叩くと

:
:
* httpoison (Hex package) (mix)
  locked at 1.5.0 (httpoison) 71ae9f30
  the dependency build is outdated, please run "mix deps.compile"
:
:
* floki (Hex package) (mix)
  locked at 0.20.4 (floki) be42ac91
  the dependency build is outdated, please run "mix deps.compile"

コンパイルオナシャスと返ってきますが自動的にコンパイルされるのでどちらでも大丈夫です
いますぐコンパイルしたいという方は

mix deps.cpmpile

でmixを満足させてあげてください

いよいよコードを書いていく

生成されたこちらのファイルに記述をしていきます
./amazon_scraping/lib/amazon_scraping.ex

defmodule AmazonScraping do
  @moduledoc """
  Documentation for AmazonScraping.
  """

  @doc """
  Hello world.

  ## Examples

      iex> AmazonScraping.hello()
      :world

  """
  def hello do
    :world
  end
end

手始めにAmazonScrapingモジュールのhelloコマンドを呼び出してみます

iex -S mix

と叩くとプロジェクトを立ち上げてiexプロンプトを起動することが出来ます
つまりはインストールした外部ファイル使えまっせということ(怪しい
先ほどmix deps.compileコマンドを叩いていない場合にコンパイルが自動的に走ります

:
:
==> httpoison
Compiling 3 files (.ex)
Generated httpoison app
===> Compiling mochiweb
==> floki
Compiling 1 file (.erl)
Compiling 21 files (.ex)
Generated floki app
==> amazon_scraping

ではさっそく呼び出してみます

iex(1)> AmazonScraping.hello()
:world

うまく呼び出すことが出来ました
ではさっくとスクレイピングの処理を記述します
今回は宣伝も兼ねて「プログラミングelixir」のレビューを取得します

2019年4月4日で3件のレビューがあります(増えろ増えろ~
このレビューのテキストを3件取得してみます
プログラムの流れはこんな感じになりそうです

  1. 対象のページにアクセスしページ情報を取得
  2. 抽出したいdomを指定する
  3. domのテキストを抽出する
  4. etc

とりあえずhelloという名前はふさわしくないのでfetchと改名しつつ上記のフローを形にしてみます

def fetch do
    #取得先のURL
    target_url = "https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0Elixir-Dave-Thomas/dp/4274219151/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&keywords=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0elixir&qid=1554332915&s=gateway&sr=8-1"
    fetch_res =
      HTTPoison.get!(target_url).body #ページにアクセスし戻り値のbodyを取得する
      |> Floki.find("div.a-expander-content") #レビュー部分のdomを抽出
      |> Enum.map(&(Floki.text(&1))) #domのテキストを抽出
      |> Enum.map(&(String.replace(&1, "\n", ""))) #改行記号がうざいので除去
    fetch_res
  end

といった形に落ち着きました まず、HTTPoison.get!メゾットを使用して対象ページにアクセスします
このメゾットは例外を発生させる(elixirでは例外を発生させるメゾットには「!」をつける)んですが
HTTPoison.getメゾットを使うよりもメリットがあって
get!メゾットの方がgetメゾットよりも階層が1つ浅いのでbodyの取得が楽です

HTTPoison.get!の場合

AmazonScraping.get()
%HTTPoison.Response{
  body: "information for body....",
  headers: [
    {"Content-Type", "text/html;charset=UTF-8"},
    {"Transfer-Encoding", "chunked"},
    :
    :
  ],
  request: %HTTPoison.Request{
    body: "",
    headers: [],
    method: :get,
    options: [],
    params: %{},
    url: "https://sample.com"
  },
  request_url: "https://sample.com",
  status_code: 200
}

HTTPoison.getの場合

{:ok,
  %HTTPoison.Response{
    body: "information for body....",
    headers: [
      {"Content-Type", "text/html;charset=UTF-8"},
      {"Transfer-Encoding", "chunked"},
      :
      :
    ],
    request: %HTTPoison.Request{
      body: "",
      headers: [],
      method: :get,
      options: [],
      params: %{},
      url: "https://sample.com"
    },
    request_url: "https://sample.com",
    status_code: 200
  }
}

getメゾットは例外を発生させない代わりに:ok, :errorのどちらかのアトムが返ってきています
このreturnのbodyをFlokiを使って解析します
今回の場合はレビューのテキストを取得したいので「div.a-expander-content」 にアクセスします
domについては深いところには触れません。気になる人はブラウザで右クリックして検証画面を開いてみてください
Floki.find()を使用するとこんな感じでわけのわからんデータが返ってきます

[
  {"div",
   [
     {"data-hook", "review-collapsed"},
     {"aria-expanded", "false"},
     {"class",
      "a-expander-content reviewText review-text-content a-expander-partial-collapse-content"}
   ],
   [
     {"span", [{"class", ""}],
      [
        "Ruby On Railsで作ったTodoリストアプリに比べ、最大2,000倍の処理速度で動くなど、Elixirは次に来るWeb言語と言われることがよくわかった。",
        {"br", [], []},
        "昔から処理スピードを気にしてるおっさんなので、最初から速い設計な言語を探していたけど、Elixirは是非習得したいと思わせる魅力がある。",
        {"br", [], []},
        {"br", [], []},
        "残念ながらerlangは全く知らないので活用出来るかどうかわからないが、焦ってもしょうがない。時間をかけてじっくり理解していこうと思う。",
        {"br", [], []},
        {"br", [], []},
        "他にElixirの本が無いのでこの本がしばらくバイブルになりそう。"
      ]}
   ]},
  :
  :
]

この中からテキストのみを抜き出しするにはFloki.text()を使えばOK
ただ、今回は複数データが返ってきてるので普通に戻り値にFloki.text()使うとerrorになります
なのでEnum.map(&(Floki.text(&1))でよしなにやりましょう

["Ruby On Railsで作った...",
 "Elixirの文法部分は前半125ページくらいまでを...",
 "erlangもelixirもろくに触ったことはないですが\n"]

最後に改行記号が邪魔なので「\n」を除去しましょう
あと関数内にurlを定数置きしてますけど、関数の引数に渡す方がcoolですね

def fetch(url) do
  :
  :
end

まとめ

ここまでて最低限のスクレイピングが完了しました
ただ、これだけだと先駆者がいるので次回の記事では

  • ページネーションの対応
  • マルチプロセスでの実行

について触れたいと思います
凄い長くなってしまったので反省