perlでHTMLを解析して欲しい情報を抽出するためのコードをメモ

公開日:2014/01/21 更新日:2014/01/21
perlでHTMLを解析して欲しい情報を抽出するためのコードをメモのサムネイル

HTMLファイルから特定のタグに囲まれている情報だけを抽出したり、リンクだけを抽出したりしたいと思ったので、それを実現するコードを色々調べてperlで作成してみたのでメモしておきます。いわゆるスクレイピングするためのコードです。このコードでは、perlのHTML::TreeBuilderを使いました。

#2014/1/25追記 以下にメモしたコードそのままでは、HTML5で記述されたhtmlファイルから情報を抽出できません。HTML5にも対応したコードはperlでHTML5を解析して情報を抽出するコード(HTML::TagParser版)にメモしたので、見て頂ければと思います。

実行環境と使用したperlモジュール

実行環境はUbuntu12.04 64bit です。 使用したのは以下の2つのモジュールです。これらのインストールはcpanmで行いました。

  • HTML::TreeBuilder
  • LWP::UserAgent(Webサイトからhtmlファイルを取得する場合に必要)
ubuntuのPerl環境の構築手順については、以下のサイトが参考になりました。

HTML::TreeBuilderについて

ここではperlの「HTML::TreeBuilder」というモジュールを使用してhtmlファイルから必要な情報を抽出しました。HTML::TreeBuilderはどんなモジュールかというと、読み込んだhtmlファイルを解析して木構造形式のオブジェクトに変換してくれるモジュールです。

HTML::TreeBuilderに用意された関数を使用することで、htmlファイル内の全てのリンクを取得したり、特定のclass要素やid要素を持つタグに囲まれたテキストやリンクだけを取得することができます。以降に実際に作成したperlコードとその実行例を載せます。

perlのコード

今回作成したコードは、このサイトのトップページのhtmlファイルの冒頭部分から、新着順に並んだ投稿記事に関する以下の情報を抽出してターミナル上に表示します。

  • ① 投稿日 → <span class="entry-day">の中
  • ② URL → <div class="entry-title">の中
  • ③ 識別番号 → <div class="entry-title">のURLの中
  • ④ タイトル → <div class="entry-title">の中
  • ⑤ 説明 → <div class="entry-content clearfix">の<p>の中
htmlファイルで見ると、以下の画像の赤線部分になります。

virment_html_mod.png

今回使用したこのサイトのトップページのhtmlファイルを一応以下に載せておきます。以下のhtmlファイルは、このサイトのトップページにある新着記事3個分を抜粋したhtmlファイルになります。

<div id="post-1653" class="post-1653 post type-post status-publish format-standard hentry category-virtualbox">

  <div class="entry-date-top">
          <span class="entry-month">2014</span>
          <span class="entry-day">1/16</span>
  </div>


  <div class="entry-title"><a href="https://www.virment.com/mac/virtualbox/1653/" title="Permalink to VirtualBoxにおける仮想マシンの仮想ディスク容量を拡張するための手順" rel="bookmark">VirtualBoxにおける仮想マシンの仮想ディスク容量を拡張するための手順</a></div>


  <div class="entry-meta">    
  
  <span class="cat-links"><span class="entry-utility-prep entry-utility-prep-cat-links"> Category: </span> <a href="https://www.virment.com/category/mac/virtualbox/" title="VirtualBox の投稿をすべて表示" rel="category tag">VirtualBox</a></span>  </div><!-- .entry-meta -->
  
  <div class="entry-content clearfix">
    <span style=" float: left; margin-right: 1em;"><img width="250" height="150" src="./virtualiment_files/first_disp_mod-250x150.png" class="attachment-small" alt="first_disp_mod"></span>
    <p>VirtualBoxでは、仮想マシン作成時に仮想ディスク容量を決めますが、仮想マシンを使用していると仮想ディスクの容量が不足してくることがあると思います。ここでは、このような場合に仮想マシンの仮想ディスク容量を拡張して増やすための手順をメモします。 <a href="https://www.virment.com/mac/virtualbox/1653/" class="more-link"><span>続きを読む</span></a></p>
  </div> <!-- end .entry-content -->
  
  
 <div class="entry-meta-bottom">

  </div><!-- .entry-meta-bottom -->

</div> <!-- end #post-1653 .post_class -->        
                
          <div id="post-1622" class="post-1622 post type-post status-publish format-standard hentry category-linux">

  <div class="entry-date-top">
          <span class="entry-month">2014</span>
          <span class="entry-day">1/15</span>
  </div>


  <div class="entry-title"><a href="https://www.virment.com/linux/1622/" title="Permalink to Linux Mint 16で日本語入力環境を整えるまでの手順" rel="bookmark">Linux Mint 16で日本語入力環境を整えるまでの手順</a></div>


  <div class="entry-meta">    
  
  <span class="cat-links"><span class="entry-utility-prep entry-utility-prep-cat-links"> Category: </span> <a href="https://www.virment.com/category/linux/" title="Linux の投稿をすべて表示" rel="category tag">Linux</a></span>  </div><!-- .entry-meta -->
  
  <div class="entry-content clearfix">
    <span style=" float: left; margin-right: 1em;"><img width="250" height="150" src="./virtualiment_files/menu1_mod-250x150.png" class="attachment-small" alt="menu1_mod"></span>
    <p>最近、Linux Mintというディストリビューションに興味を持ち、言語を英語に設定してインストールしました。インストールしたのはLinux Mint 16です。Ubuntuをベースにしているそうで、Ubuntuを使っていた自分にとっては何の抵抗も無く使えたので、今後使って行こうと思っています。ただ、日本語入力ができなかったので、日本語入力環境を整える必要がありました。ここに日本語入力できるようにするまでの手順をメモしておきます。 <a href="https://www.virment.com/linux/1622/" class="more-link"><span>続きを読む</span></a></p>
  </div> <!-- end .entry-content -->
  
  
 <div class="entry-meta-bottom">

  </div><!-- .entry-meta-bottom -->

</div> <!-- end #post-1622 .post_class -->        
                
          <div id="post-1594" class="post-1594 post type-post status-publish format-standard hentry category-virtualbox">

  <div class="entry-date-top">
          <span class="entry-month">2014</span>
          <span class="entry-day">1/15</span>
  </div>


  <div class="entry-title"><a href="https://www.virment.com/mac/virtualbox/1594/" title="Permalink to VirtualBoxが仮想マシンの仮想ディスクなどを格納するフォルダの変更方法" rel="bookmark">VirtualBoxが仮想マシンの仮想ディスクなどを格納するフォルダの変更方法</a></div>


  <div class="entry-meta">    
  
  <span class="cat-links"><span class="entry-utility-prep entry-utility-prep-cat-links"> Category: </span> <a href="https://www.virment.com/category/mac/virtualbox/" title="VirtualBox の投稿をすべて表示" rel="category tag">VirtualBox</a></span>  </div><!-- .entry-meta -->
  
  <div class="entry-content clearfix">
    <span style=" float: left; margin-right: 1em;"><img width="250" height="150" src="./virtualiment_files/menubar-250x150.png" class="attachment-small" alt="menubar"></span>
    <p>VirtualBoxを使用していると、仮想マシンの仮想ディスクがハードディスクを圧迫してきます。この対策として、VirtualBoxが仮想マシンの仮想ディスクやその他のデータを格納するフォルダ(仮想フォルダと呼ぶみたいなので、以降でもこの呼び名を使います。)を、別のハードディスクや別のパーティションに変更することが挙げられます。ここではこの方法についてメモします。簡単です。ただし、仮想マシンが起動しなくなったり、その他のデータが消えたりする可能性がなくはないので、全て自己責任でお願い致します。大事なデータはバックアップを取ってから作業して下さい。 <a href="https://www.virment.com/mac/virtualbox/1594/" class="more-link"><span>続きを読む</span></a></p>
  </div> <!-- end .entry-content -->
  
  
 <div class="entry-meta-bottom">

  </div><!-- .entry-meta-bottom -->

</div> <!-- end #post-1594 .post_class -->

そして以下が作成したperlコードです。下記のperlコードが置いてある同じディレクトリに上記のhtmlファイルを「virtualiment.html」という名前で保存してあります。ただし、以下のコードはperl初心者の私が色々なサイトを参考にして合体させて作成したコードであり、perl的にもプログラム的にも間違っている、良くないコードの可能性が十分にあります。現状では以下のコードで望みの動作を確認できているだけです。以下のコードはあくまで参考程度にして頂き、自己責任での使用をお願い致します。また、間違いなどがあればぜひ指摘して下さい。

use strict;
use warnings;
use LWP::UserAgent;
use HTML::TreeBuilder;

# HTML::TreeBuilderのオブジェクト作成
my $tree = new HTML::TreeBuilder;

# parse_fileでローカルに保存された「virtualiment.html」を解析、木構造に変換して取得
$tree->parse_file( "virtualiment.html" );
$tree->eof();

my $date;
my $url;
my @id;
my $title;
my $desc;

        # id要素の値が「post-3桁以上4桁以下」である部分を全て取得
    foreach my $tag ($tree->look_down("id",qr/post-\d{3,4}/)) {

        # class要素の値が「entry-date-top」で、その中のclass要素の値が「entry-date」である部分のテキストを取得
        $date = ($tag->look_down("class","entry-date-top")->look_down("class", "entry-day"))->as_text;

        # class要素の値が「entry-title」である部分のリンクを取得
        $url = ($tag->look_down("class", "entry-title")->extract_links('a', 'href'))->[0]->[0];

        # $urlの中の数字のうち3桁以上4桁以下の数字を取得
        @id = grep{/\d{3,4}/}(split( /\D+/, $url));

        # class要素の値が「entry-title」である部分のテキストを取得
        $title = $tag->look_down("class","entry-title")->as_text;

        # class要素の値が「entry-content clearfix」で、その中のpタグの部分のテキストを取得
        $desc = $tag->look_down("class","entry-content clearfix")->look_down("_tag", "p")->as_text;

          print "date is ".$date."\n";
          print "url is ".$url."\n";
          print "ID is ".$id[0]."\n";
          print "title is ".$title."\n";
          print "description is ".$desc."\n\n";
        
    }

# オブジェクトを削除
$tree = $tree->delete;                                                                                     

上記のコードで使用している関数とその説明メモを以下に載せます。

look_down

look_downを使うことで、class要素の値、id要素の値やタグ名を指定して情報を抽出できます。以下にそれぞれの場合についてメモします。

id要素を使った抽出

$tree->look_down("id",qr/post-\d{3,4}/)

上記コードの20行目の部分でid要素を使った抽出をしています。具体的には、id要素で値として「qr/post-\d{3,4}/」をもつ部分を抽出しています。この「qr/post-\d{3,4}/」は正規表現のリファレンスであり、「post-3桁以上4桁以下の数字」を意味します。つまり、この行ではid要素で値として「post-3桁以上4桁以下の数字」を持つ部分を木構造で取得します。

「qr/post-\d{3,4}/」について分からない方は、正規表現とメタ文字について以下のサイトに詳しく説明されており分かりやすいので参考になると思います。

class要素の値を指定して抽出

$date = ($tag->look_down("class","entry-date-top")->look_down("class", "entry-day"))->as_text;

23行目では、class属性の値が「entry-date-top」であり、さらにその中でclass属性の値が「entry-day」である部分のテキストを取得しています。このように、look_downを続けて記述することで、木構造の階層を下へ下へと検索できます。なお、指定した部分の中からテキストを取得するために、「as_text」を使います。

タグ名を指定して抽出

 $desc = $tag->look_down("class","entry-content clearfix")->look_down("_tag", "p")->as_text;

35行目では、class属性の値が「entry-content clearfix」であり、さらにその中で「pタグ」の部分のテキストを取得しています。

extract_linksを使ってリンクを抽出

$url = ($tag->look_down("class", "entry-title")->extract_links('a', 'href'))->[0]->[0];

26行目では、class属性の値が「entry-title」であり、さらにその中にある<a href=>のリンク部分をextract_linksによって抽出しています。extract_linksはリファレンスの配列を返すので、そのリファレンスが指し示す先にある値を取得するために「->[0]」が2回続いています。ただ、正直ここらへんが書き方と意味を十分に理解していないので、本来こんな書き方はしない、また間違っている可能性があります。一応これで動くことを確認しています。なお、配列、リファレンスについては以下のサイトを参考になりました。

「配列の配列」の基礎のサンプル | サンプルコードによるPerl入門

grepとsplit

@id = grep{/\d{3,4}/}(split( /\D+/, $url));

29行目では、grepとsplitを使って、$urlの中から3桁以上4桁以下の数字を抽出して取得しています。grepは、変数の中から、指定した条件にマッチする要素を取り出す関数です。splitは指定したパターンを区切りとして文字列を分割する関数です。grep、splitについては、以下のサイトにそれぞれ載っているので必要な方はご参照下さい。

まずgrepの条件である「/\d{3,4}/」は、3桁以上4桁以下の数字を意味します。 次に「split( /\D+/, $url)」は、パターンとして「\D+」、対象文字列として「$url」をそれぞれ指定しています。splitが処理する対象は「$url」であり、$urlには上記の場合だと各投稿記事のURLが格納されています。そしてパターンである「\D+」は、「数字以外の文字」を意味します。つまり、「split( /\D+/, $url)」により、$urlの中身である投稿記事のURLを数字以外の文字によって分割します。ここの例では、splitによって投稿記事のURLに含まれる数字がリストとして取り出されます。

以上から、29行目では、grepとsplitを使って、投稿記事のURLに含まれる3桁以上4桁以下の数字を取得しています。この数字は特に意味なくwordpressが付ける投稿記事の識別番号です。

実行結果

上記のコードをターミナルで実行すると、以下のような内容が出力されます。

$ perl test.pl
date is 1/16
url is https://www.virment.com/mac/virtualbox/1653/
ID is 1653
title is VirtualBoxにおける仮想マシンの仮想ディスク容量を拡張するための手順
description is VirtualBoxでは、仮想マシン作成時に仮想ディスク容量を決めますが、仮想マシンを使用していると仮想ディスクの容量が不足してくることがあると思います。ここでは、このような場合に仮想マシンの仮想ディスク容量を拡張して増やすための手順をメモします。 続きを読む

date is 1/15
url is https://www.virment.com/linux/1622/
ID is 1622
(以下省略)

上記のように、virtualimentのトップページにある新着記事の各情報がずらずらと表示されます。

ローカルのhtmlファイルではなく、直接Webからhtmlファイルを取得する場合

この場合は、以下の部分を

$tree->parse_file( "virtualiment.html" );
$tree->eof();

以下のように書き換えて使います。

# urlを指定する
my $site = 'https://www.virment.com';

# user agentを指定
my $user_agent = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)";

# LWP使って$siteで指定したサイトにアクセスし、そのサイトのhtmlファイルを取得する
my $ua = LWP::UserAgent->new('agent' => $user_agent);
my $response = $ua->get($site);
my $html = $response->content;
$tree->parse($html);
$tree->eof();

#2014/1/24追記 2014/1/24にこのサイトのテーマを変更してトップページのhtmlファイルが変わりました。そのため、上記のようにこのサイトのhtmlファイルを直接取得しても、冒頭に記載した以下の情報が変わってしまっているので、実行しても何の情報も表示されません。

  • ① 投稿日 → <span class="entry-day">の中
  • ② URL → <div class="entry-title">の中
  • ③ 識別番号 → <div class="entry-title">のURLの中
  • ④ タイトル → <div class="entry-title">の中
  • ⑤ 説明 → <div class="entry-content clearfix">の<p>の中

したがって、Webからhtmlファイルを直接取得して試したい場合は、上記の5つの部分に対応するタグや値をこのサイトの新しいhtmlファイルの情報に置き換える必要があります。

#2014/1/25追記 詳しくはこちらperlでHTML5を解析して情報を抽出するコード(HTML::TagParser版)にメモしたので、見て頂ければと思います。

splitについてのメモ

上記では、splitを使って$urlの文字列に含まれる数字以外の文字によって分割しました。ここで分割とはどういうことかというと、例えば以下のコードによって文字列「$str」に対して「split(/\D+/,$str)」を実行すると、

use strict;
use warnings;

my $str = "This12is34a56pen";
my @array;


@array = split(/\D+/, $str);

foreach my $tmp(@array){
   print $tmp."\n";
}

以下のような実行結果になります。

$ perl split_test.pl 

12
34
56

上記を見ると、数字が取り出されて表示されているのが分かります。なお、「12」の上に空白があるのは、$strの先頭にある「This」が区切り文字であり、この「This」の前の要素が$strを分割した後に残る文字として出力されているからです。

参考サイト様

以下のサイトがとても参考になりました。ありがとうございました。

開発アプリ

nanolog.app

毎日の小さな出来事をなんでも記録して、ログとして残すためのライフログアプリです。