yu-tarrrrの日記

完全に個人の趣味でその時々書きたいことを書く

Elixirでさくっとスクレイピングする

はじめに

  • 今回はAPIが提供されていないWebサイトに対して、スクレイピングをして何かしら加工して出力したいと思います
  • また、あくまでも私的利用であり数回しか叩かない予定であり、スクレイピングを推奨している訳ではありません

用意したもの

実装していく

まず、依存ライブラリを追加していく

defmodule Scrape.MixProject do
  use Mix.Project

  def project do
    [
      app: :scrape,
      version: "0.1.0",
      elixir: "~> 1.10",
      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
    [
      {:floki, "~> 0.29.0"},
      {:httpoison, "~> 1.2.0"},
      {:poison, "~> 3.1"}
    ]
  end
end
  • 今回追加したものは下記の通り

  • HTTPリクエストを行うので、そのためのライブラリとしてHTTPoison hexdocs.pm

  • 一旦、DBなどは使わずJsonからmaster情報を取得して、Jsonへ出力するということをしたいのでPoisonを使う hexdocs.pm

  • API提供されてないサイトへのスクレイピングを行うので、そこら辺はFlokiが頑張ってくれる hexdocs.pm

Jsonをパースしていく

  • マスター情報はこんな感じで、とあるショップのショップ名とショップURLを持たせておく
[
    {
        "shopName": "website1",
        "shopUrl": "https://website1.jp/"
    },
    {
        "shopName": "website2",
        "shopUrl": "https://website2.jp/"
    }
]
defmodule ScrapeSample do
  # main関数からjsonを取得する関数を呼び出す
  def main do
    read_master()
  end

  def read_master do
    "master.json"
    |> File.read!()
    |> Poison.decode!()
  end
end

スクレイピングする

defmodule ScrapeSample do
  def main do
    read_master()
    |> Enum.map(& scrape(&1["shopUrl"]))
  end

  def read_master do
    "master.json"
    |> File.read!()
    |> Poison.decode!()
    end

  def scrape(url) do
    # 受け取ったURLに対してリクエストして、Floki.find()で取得したい要素を指定する
  HTTPoison.get!(url).body
    |> Floki.find("section")
    |> Floki.find("h2")
  end
end
  • ちなみにこの時点で、scrapeした結果を出力すると、下記のような感じになる。
[
  {"h2", [], ["aaaaa"]},
  {"h2", [], ["bbbbb "]},
  {"h2", [], ["ccccc"]}
]
[
  {"h2", [], ["ddddd"]},
  {"h2", [], ["eeeee"]},
]
  • この場合欲しい要素はTupleの3つ目の要素のみなので、修正する
 def scrape(url) do
    HTTPoison.get!(url).body
      |> Floki.find("section")
      |> Floki.find("h2")
      |> Enum.flat_map(&(Tuple.to_list(&1) |> Enum.at(2)))
  end
  • このあと、必要に応じてフィルタの処理を入れてもいいが、今回は割愛する

あとは、jsonに出力する

  • 出力するオブジェクトを作成して、それをFile.writeに渡したら終了
   def main do
    read_master()
    |> Enum.map(& %{shopName: &1["shop_name"], items: scrape(&1["shop_url"]), url: &1["shop_url"]})
    |> write_items()
  end

  def write_items(items) do
    File.write!("items.json", Poison.encode!(items))
  end

最終的なコード

defmodule ScrapeSample do

  def main do
    read_master()
    |> Enum.map(& %{shopName: &1["shopName"], items: scrape(&1["shopUrl"]), url: &1["shopUrl"]})
    |> write_items()
  end

  def read_master do
    "master.json"
    |> File.read!()
    |> Poison.decode!()
  end

  def scrape(url) do
    HTTPoison.get!(url).body
        |> Floki.find("section")
        |> Floki.find("h2")
        |> Enum.flat_map(&(Tuple.to_list(&1) |> Enum.at(2)))
  end

  def write_items(items) do
    File.write!("items.json", Poison.encode!(items))
  end
end

感想

  • スクレイピングするだけならめちゃくちゃ簡単でした
  • 特に、Flokiのハマりポイントもないので、Elixirの入門として書くコードとしてはまあまあ良さそうという印象です。