sironekotoroの日記

Perl で楽をしたい

PerlでWebから情報を持ってくるときのツール

まず注意

昨今のWebではhttps化が進んでいます。https の s はSecureの s です。安全な通信をするためにデータを暗号化/復号するのですが、そのためのモジュールが別途必要です。

運が良ければ、すでにインストールされているかも・・・

Perl入学式で構築した手元のMsys32(Windows7上に構築)だと

  • のちに出てくる HTTP::Tiny のサンプルスクリプトを試したところ、httpのサイト(例:http://www.hatena.ne.jp/)はアクセスできるが、httpsのサイト(例:https://www.yahoo.co.jp)にPerlスクリプトからアクセスできず。

  • エラーメッセージによると、 Net::SSLeay が無いとのこと。

  • cpanm Net::SSLeay

    • インストール失敗
    • エラーメッセージによると、MSYS側にSSL環境がなさそう
  • pacman -Syu

    • MSYS側の更新がなくなるまで複数回実行&再起動
    • エラー変わらず
  • pacman -S openssl

    • f:id:sironekotoro:20190728112219p:plain
    • MSYSのopensslのインストールはできたが、 cpanm Net::SSLeayは失敗
  • pacman -S perl-Net-SSLeay

    • cpanm じゃなくて、MSYSのパッケージマネージャ(pacman)で入れてみることにする
    • f:id:sironekotoro:20190728114636p:plain
    • インストールはされたが、httpsのサイトへのアクセスは失敗する・・・が、エラーメッセージが変わった!一歩前進!
    • こんどは IO::Socket::SSLがないだとー
  • cpanm IO::Socket::SSL

    • 大文字小文字を厳密に見てくるので注意
    • こっちはなんか時間切れとか出てくる。きー!
  • pacman -S perl-IO-Socket-SSL

    • インストールできた
  • うごいた!

こんな感じでした。ここまでが長い。でも、動いてしまえば「許そう、全てを・・・」って気分になりませんか。うちはなります。

HTTPクライアント編

HTTPクライアントというのは、Chromeとか、Firefoxとかそういったブラウザです。HTTPサーバーにアクセスし、情報を得るものです。

プログラムからもHTTPサーバにアクセスして情報を得ることができます。

以下はPerlでのHTTPクライアントの紹介です。

HTML::Tiny

perldoc.jp

Perl 5.13.9以降であればコアモジュールとしてPerl本体と一緒にインストールされるモジュール。なので、普通にPerlをインストールすれば大概の環境では使えるはず。

$ corelist HTTP::Tiny

Data for 2018-11-29
HTTP::Tiny was first released with perl v5.13.9

HTTP::Tiny のページにあるサンプルをPerl入学式にある範囲で書き直したものです。あと、対象を日本のYahoo Japanに変えています。

$response->{content} とか、リファレンス回をやった人だと見覚えがあるのではないでしょうか。ハッシュリファレンスですね。

use strict;
use warnings;
use HTTP::Tiny;

# Yahooのurlを変数に入れる
my $yahoo_url = 'http://www.yahoo.co.jp';

# HTTP::TinyでYahooにアクセスして、getでデータを持ってくる
my $response = HTTP::Tiny->new->get($yahoo_url);

# データを持ってくるのに成功したかどうかを判別
if ( $response->{success} ) {
    # 成功してたら、HTTPのステータスコードを表示。200ならok
    print "$response->{status} $response->{reason}\n";
}
# 失敗してたら、プログラム終了
else {
    print "Failed!\n";
    exit();
}

print $response->{content}; # ヤフーのHTMLが表示される

LWP::Simple

perldoc.jp

ここからは cpanm でのインストールが必要なモジュール。LWP というのは ibwww-perl の略称で、PerlをつかってWebにアクセスするときの基本的なモジュールです。その機能最小版です。

use strict;
use warnings;
use LWP::Simple;

# Yahooのurlを変数に入れる
my $yahoo_url = 'http://www.yahoo.co.jp';

# getでデータを持ってくる
my $content = get($yahoo_url);

# データ持ってくるのに成功したら表示する
if ($content) {
    print $content;    # ヤフーのHTMLが表示される
}

HTTP::Tinyと大体同じですね。うちのコメントも早くも簡素になってきました。

LWP

perldoc.jp

上記 LWP::Simple の大元である、HTTPアクセスに必要なことの全てが詰まったフルセット版です。 ユーザーエージェントの変更やPOSTでのデータ送信など、HTTP通信を使った機能が詰まっています。

use strict;
use warnings;
use LWP::UserAgent;

# Yahooのurlを変数に入れる
my $yahoo_url = 'http://www.yahoo.co.jp';

# LWP::UserAgentのインスタンスを生成する
my $ua  = LWP::UserAgent->new();
# HTTP::Requestのインスタンスを生成するついでに、GETでアクセスしに行くページも設定する
my $req = HTTP::Request->new( GET => $yahoo_url );

# uaがGETでyahooにデータを取りに行く
my $res = $ua->request($req);

# ページの取得に成功してたら内容を表示する
if ( $res->is_success ) {
    print $res->content;  # ヤフーのHTMLが表示される
}

Furl

perldoc.jp

tokuhiromさん作成の「早い」ライブラリ。

Furlはもう一つのHTTPクライアントライブラリです。LWPはPerl5のデファクトスタンダードな HTTPクライアントですが、クリティカルなジョブでは遅すぎますし、週末のハッキングには 複雑過ぎます。Furlはこれらの問題を解決します。楽しんで下さい!

use strict;
use warnings;
use Furl;

# Yahooのurlを変数に入れる
my $yahoo_url = 'http://www.yahoo.co.jp';

# Furlのインスタンスを生成するついでに、GETでアクセスしに行くページも設定する
my $res = Furl->new->get($yahoo_url);

# ページの取得に成功してたら内容を表示する
if ( $res->is_success ) {
    print $res->content;
}

Webスクレイピング

ここまで紹介したものは、Webサーバーにアクセスして、そこから情報を取得するものでした。しかし、ほとんどの用途では、Webの情報が全て必要なわけではなく、ごく一部のみ必要だったりします。

そういった情報を都度、正規表現などを使って取り出してもいいですが、もっと楽はできないものでしょうか?そんなプログラマの三大美徳「怠惰」を実現させるツール、ありそうですよね。

Webサーバから返ってきたデータのうち、必要な部分だけ取り出す、こそげとる、それがスクレイピングと言われるツールです。

ここからはPerlスクレイピングモジュールを使ってYahooニュースからトピック一覧だけ取り出してみます。

Web::Scraper

metacpan.org

うちはこのツールをPerl CPANモジュールガイド という本で知って、ほぼこれをメインに使っています。要素の指定にXPathCSSセレクタを利用することができます。

この本には他にもPerlのモジュールがたくさん載っているので、やや古いとはいえオススメです。

use strict;
use warnings;

# 日本語をターミナルで表示(出力)するときに警告が出ないよう書いておく
binmode STDOUT, ':encoding(UTF-8)';

use Web::Scraper;
use URI;

my $yahoo_news_url = 'https://news.yahoo.co.jp/topics';

my $uri = URI->new($yahoo_news_url);

# どの要素を取得するかを設定する変数
my $scraper = scraper {

    # タイトルを取得するスクレイパー
    # Xpath形式で指定
    process '/html/head/title', 'page_title' => 'TEXT';

    # ニュースタイトル一覧を取得するスクレイパー
    # CSSセレクタ形式で指定
    process 'li.topicsListItem > a ', 'news_titles[]' => 'TEXT';
};

# ここでスクレイピングが実行される
my $res = $scraper->scrape($uri);

# ここから結果表示
print $res->{page_title} . "\n";    # タイトルの表示

for my $title ( @{ $res->{news_titles} } ) {
    print $title . "\n";    # ニュースのタイトルの表示
}

Web::Query

metacpan.org

Web::Query is a yet another scraping framework, have a jQuery like interface.

Javascriptのライブラリ、JQueryに似た方法で要素を指定してスクレイピンすることができるモジュールです。実はJQuery使ったことないので、わからない・・・

でも、直感的に要素を指定できる感じで良いですね。

use strict;
use warnings;

# 日本語をターミナルで表示(出力)するときに警告が出ないよう書いておく
binmode STDOUT, ':encoding(UTF-8)';

use Web::Query;

my $yahoo_news_url = 'https://news.yahoo.co.jp/topics';

# topicsListItem 属性をもつ li タグの下にある a タグを持ってきて、
# a タグのテキスト要素を表示する
wq($yahoo_news_url)->find('li.topicsListItem > a')->each(
    sub {
        print $_->text . "\n";
    }
);

HTML::TreeBuilder

metacpan.org

スクレイピングツールのくくりに入れましたが、HTML::TreeBuilderはHTMLをパース(解析)するためのツールです。

正確なHTMLは<html>の中に<head><body>タグがあり、その中にさらに複数のタグが配置されています。

これを、<html>という太い幹から<head><body>という枝が生え、さらにその枝から別の枝が生え・・・という木構造で解析していきます。

本来は「特定の枝の先にだけ、何らかの処理を行う」ということをしたかったのですが、ちょっと今の自分ではわかりませんでしたわ・・・

use strict;
use warnings;

# 日本語をターミナルで表示(出力)するときに警告が出ないよう書いておく
binmode STDOUT, ':encoding(UTF-8)';

use HTML::TreeBuilder;

my $yahoo_url = 'https://news.yahoo.co.jp/topics';

# new_from_urlメソッドは裏でLWP::UserAgent使っているので、
# LWP入れてないと動かない
my $tree = HTML::TreeBuilder->new_from_url($yahoo_url);
$tree->eof();

foreach $a ( $tree->find("a") ) {

    # news の a タグには data-ual-gotocontent って属性が付与されてた
    # ところで、ual って何だろう・・・urlならわかるんだけど
    my $data_ual_gotocontent = $a->attr('data-ual-gotocontent') || next;

    if ( $data_ual_gotocontent eq 'true' ) {
        print $a->as_text . "\n";
    }
}

閑話休題

WWW::Mechanize

perldoc.jp

一部の人には刺さるかもしれません。私も刺さった人です。

今まで紹介したものは、Webサーバから情報を持ってくるものと、Webページから特定のデータを抜き出す(スクレイピングする)ものでした。

ここで紹介する WWW::Mechanize 、通称Mechメック は、Webサーバからデータを受け取り、そのあとにリンクをたどったり、入力欄に文字を入れてボタンを押す、なんてことができます。

以下は、Yahooの検索ページの入力欄に「Perl入学式」という文字を入力して、その検索結果を得るスクリプトです。

use strict;
use warnings;
use utf8;

# 日本語をターミナルで表示(出力)するときに警告が出ないよう書いておく
binmode STDOUT, ':encoding(UTF-8)';

use WWW::Mechanize;

# Yahooの検索ページのURL
my $yahoo_search_url = 'https://search.yahoo.co.jp/';

# WWW::Mechanizeのインスタンスを生成する
my $mech = WWW::Mechanize->new();

# Yahooの検索ページのHTMLをGETで取得する
$mech->get($yahoo_search_url);

# 取得したHTMLを解析し、フォームの入力欄に文字を入れて、
# そのフォームのボタン(検索ボタン)を押す
$mech->submit_form(
    # 最初のフォーム
    form_number => 0,

    # name属性が p の入力欄に 検索対象の文字を入れる
    fields => { p => 'Perl入学式' },
);

# 結果を表示する
print $mech->{content};

どうでしょう、ずらっとHTMLが表示されて、その中にかすかに検索結果らしきものが見えますね・・・これを整形したい・・・と思いませんか?

うちは思います。

ということで、ここで、先に登場したスクレイピングツールを使います。WWW::MechanizeでYahoo検索した結果から、タイトルとリンクをWeb::Scraperで取り出してみます。

use strict;
use warnings;
use utf8;

# 日本語をターミナルで表示(出力)するときに警告が出ないよう書いておく
binmode STDOUT, ':encoding(UTF-8)';

use WWW::Mechanize;

my $yahoo_search_url = 'https://search.yahoo.co.jp/';

my $mech = WWW::Mechanize->new();

$mech->get($yahoo_search_url);

$mech->submit_form(
    form_number => 0,
    fields      => { p => 'Perl入学式' },
);

my $mech_result = $mech->{content};

# -----ここまでが WWW::Mechanize の部分

# -----ここからは Web::Scraper の部分

use Web::Scraper;

my $scraper = scraper {

# 検索結果を取得するスクレイパーを用意しているところ
# Xpath形式で指定 //と書くことで前方の要素を省略する
# ここでは、
# id が web 要素のdivタグを撮ってきて、その下(直下とは限らない)にある liタグを持ってくる
# という内容
    process '//div[@id="web"]//li', 'search_result[]' => scraper {

    # さらに、その取得条件からタイトルとリンクを分けて取得し、
    # ハッシュで格納する
        process '//a', 'title' => 'TEXT';
        process '//a', 'url'   => '@href';
    };

};

# ここでスクレイピングが実行される
# スクレイプ対象にはmechで検索した後のページを入れる
my $res = $scraper->scrape( $mech_result );

# ここから結果表示
for my $result ( @{ $res->{search_result} } ) {
    print '-------' . "\n";
    print $result->{title} . "\n";
    print $result->{url} . "\n";
    print '-------' . "\n";
}

Web API

さっきので終わらせようと思ったんだけど、ふと、昨今のWebからのデータ取得をいうならWeb APIは外せないのでは?と思い至りました。遅い。

Web APIは簡単にいうと、プログラムが使いやすいように整形されたデータのやり取り、というところでしょうか。詳細はググったり調べたりでお願いします。

もちろん、上記のHTTP::TinyとJSON::PPモジュールを利用することでWeb APIで提供されているデータの取得と利用が容易になります。どちらもPerlのコアモジュールです(5.13.9 以降)。

ここでは、無料で利用できるAPIとして、Livedoorの天気予報APIを利用します。

weather.livedoor.com

地域は東京地方、今日(2019年8月4日)の最高気温を調べてみます。

HTTP::Tiny + JSON::PP

use strict;
use warnings;
use HTTP::Tiny;

# Livedoor天気APIのURLを変数に入れる
my $livedoor_weather_url = 'http://weather.livedoor.com/forecast/webservice/json/v1?city=130010';

# HTTP::TinyでAPIにアクセスして、getでデータを持ってくる
my $response = HTTP::Tiny->new->get($livedoor_weather_url);

# データを持ってくるのに成功したかどうかを判別
if ( $response->{success} ) {
    # 成功してたら、HTTPのステータスコードを表示。200ならok
    print "$response->{status} $response->{reason}\n";
}
# 失敗してたら、プログラム終了
else {
    print "Failed!\n";
    exit();
}

# 取得したデータはJSON形式なので、Perlで操作しやすいように変換する。

use JSON::PP;

my $decoded_response = decode_json($response->{content});

# どんなデータ構造になっているかわからなくなったら、Data::Dumperを使おう!
# use Data::Dumper;
# print Dumper $decoded_response;

# 最高気温
print $decoded_response->{forecasts}->[0]->{temperature}->{max}->{celsius}; # 34

34度!ヤダもう・・・

ところで、my $livedoor_weather_url = 'http://weather.livedoor.com/forecast/webservice/json/v1?city=130010'; の末尾の数字を 270000 に変えることで、大阪の最高気温を出したりすることができます。

ここを変数にすると、他の地域の気温を見たいときに楽ですね。

さらに、その数字を標準入力から入れられると楽かも。

さらにさらに、Webアプリから地域を選べるようにすれば・・・となります。

gist.github.com

そんな、Webアプリからの入出力を学べる Perl入学式 2019 第5回 in東京 お待ちしております!

perl-entrance-tokyo.connpass.com

ojo

Perl入学式 in沖縄の id:AnaTofuZ さんから教えてもらったのがこのツール。

metacpan.org

cpanm ojo でインストールして、コマンドラインから1行、ワンライナーです。

$ perl -Mojo -E'my $content = j g("http://weather.livedoor.com/forecast/webservice/json/v1?city=130010")->{content}->{asset}->{content};say $content->{forecasts}->[0]->{temperature}->{max}->{celsius}'

ということで

PerlでWebにアクセスしてデータ集める方法でした。