LoginSignup
2

More than 5 years have passed since last update.

[Elixir] Flokiを使ってスクレイピングを行う

Last updated at Posted at 2017-06-25

ふとHTTPステータスコードの一覧と日本語の説明のデータが欲しくなったので
ElixirのFlokiモジュールを使って WikipediaのHTTPステータスコードからデータを取得してみました。

ほしいもの

以下のような構造を持ったリストを取得したいと思います

[
%{
  status_code: 509,
  summary: "509 Bandwidth Limit Exceeded",
  description: "帯域幅制限超過。そのサーバに設定されている帯域幅(転送量)を使い切った場合に返される。"
},
%{
  status_code: 508,
  summary: "508 Loop Detected",
  description: "ループを検出。WebDAVの拡張ステータスコード。"
},
...
]

コード

scraping_wiki_httpstatus.exs
defmodule Parser do
  def parse(nodes), do: parse(nodes, [], %{})
  def parse([], result, _), do: result
  def parse([head | tail], result, tmp) do
    {tag, _, children_nodes} = head
    text = Floki.text(children_nodes)

    {result, tmp} = case tag do
      "dt" ->
        status_code = text |> String.replace(~r/ .*/, "") |> String.to_integer
        {[tmp | result], %{summary: text, status_code: status_code}}
      "dd" ->
        description =
          case tmp[:description] do
            nil -> text
            desc -> "#{desc}\n#{text}"
          end
       {result, tmp |> Map.put(:description, description)}
    end

    parse(tail, result, tmp)
  end

end

%{body: html} = HTTPoison.get! "https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89"

result = html
|> Floki.find("dt, dd")
|> Parser.parse

IO.puts inspect result

取得対象のHtmlは以下のようになっているので

<dt>200 OK</dt>
<dd><b>OK</b>。リクエストは成功し、レスポンスとともに要求に応じた情報が返される。</dd>
<dd><a href="/wiki/%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6" title="ブラウザ">ブラウザ</a>でページが正しく表示された場合は、ほとんどがこのステータスコードを返している。</dd>
<dt>201 Created</dt>
<dd><b>作成</b>。リクエストは完了し、新たに作成されたリソースのURIが返される。</dd>
<dd>例: PUTメソッドでリソースを作成するリクエストを行ったとき、そのリクエストが完了した場合に返される。</dd>

Floki.find/2 を用いて dt dd タグの一覧を取得しています

body
|> Foki.find("dt, dd")
#{"dt", [], ["201 Created"]},
# {"dd", [],
#  [{"b", [], ["作成"]},
#   "。リクエストは完了し、新たに作成されたリソースのURIが返される。"]},
# {"dd", [],
#  ["例: PUTメソッドでリソースを作成するリクエストを行ったとき、そのリクエストが完了した場合に返される。"]},
# {"dt", [], ["202 Accepted"]},
# {"dd", [],
#  [{"b", [], ["受理"]},
#   "。リクエストは受理されたが、処理は完了していない。"]},
# {"dd", [],
#  ["例: PUTメソッドでリソースを作成するリクエストを行ったとき、サーバがリクエストを受理したものの、リソースの作成が完了していない場合に返される。",
#   {"a",
#    [{"href", "/wiki/%E3%83%90%E3%83%83%E3%83%81%E5%87%A6%E7%90%86"},
#     {"title", "バッチ処理"}], ["バッチ処理"]}, "向け。"]},

このとき dtdd は親子関係になっていません。
なので再帰の中で値を tmp に入れておき dt タグが来たら
tmpの中身をresultに追加しています。

    {result, tmp} = case tag do
      "dt" ->
        status_code = text |> String.replace(~r/ .*/, "") |> String.to_integer
        {[tmp | result], %{summary: text, status_code: status_code}}
      "dd" ->
        description =
          case tmp[:description] do
            nil -> text
            desc -> "{desc}\n#{text}"
          end
       {result, tmp |> Map.put(:description, description)}
    end

ソースコードは gist にあげています

実行可能な形式であげていますので以下のように試せます

% git clone https://gist.github.com/tamanugi/6ccececefde54226f5f0b261037dedac
% cd 6ccececefde54226f5f0b261037dedac
% mix deps.get
% mix run scraping_wiki_httpstatus.exs
[%{description: "帯域幅制限超過。そのサーバに設定されている帯域幅(転送量)を使 い切った場合に返される。", status_code: 509, summary: "509 Bandwidth Limit Exceeded"}, %{description: "ループを検出。WebDAVの拡張ステータスコード。", status_code: 508, summary: "508 Loop Detected"}, ...

以下の記事を参考にさせていただきました

GistへElixirのプロジェクトを置き,最小限の自己完結した確認可能なコードとして利用する

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2