用意するもの
- 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
HTTPoisonとFlokiを追加
インストールするバージョンは上記リンク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件取得してみます
プログラムの流れはこんな感じになりそうです
- 対象のページにアクセスしページ情報を取得
- 抽出したいdomを指定する
- domのテキストを抽出する
- 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
まとめ
ここまでて最低限のスクレイピングが完了しました
ただ、これだけだと先駆者がいるので次回の記事では
- ページネーションの対応
- マルチプロセスでの実行
について触れたいと思います
凄い長くなってしまったので反省