のんびり読書日記

日々の記録をつらつらと

WikipediaのキーワードリンクをDBMに保存する

データマイニングの手法を試してみるときは、Wikipediaのデータが量もたくさんあって使いやすいと思うけど、配布されているテキストファイルのままではちょっと扱いが面倒。

DBに入れておければプログラムからも使いやすくていいんだけど、MySQLとかのリレーショナルデータベースに入れてしまうと、MySQLからデータを取得するところが重いので、頻繁にデータにアクセスする必要がある場合はプログラムの処理時間を大幅に増加させてしまう可能性がある。こういう場合はDBMに入れてしまうのが便利。

今回はWikipediaのキーワード間のリンク情報をTokyoCabinetのHashDBに保存してみる。

作成したプログラムは下の通り。wikipediaからキーワード間のリンク情報を取得する部分は、下のページを参考にした。

Wikipediaのキーワードリンクを使って関連語データを作ってみた

#!/usr/bin/perl 
#
# Wikipedia のキーワード感のリンク構造を
# TokyoCabinetのHashDBに保存する
#
# Usage:
#  1) キーワード <-> IDの対応DBMの作成(keyword2id.hdb, id2keyword.hdb)
#   % bunzip2 -c jawiki-latest-pages-articles.xml.bz2 | \
#     perl wp_keyword_link.pl -t -d output/
#
#  2) キーワードリンクDBMの作成(keywordlink.hdb)
#   % bunzip2 -c jawiki-latest-pages-articles.xml.bz2 | \
#     perl wp_keyword_link.pl -l -d output/
#
# 参考ページ: 
#  [を] Wikipediaのキーワードリンクを使って関連語データを作ってみた
#  http://chalow.net/2007-06-09-3.html

use strict;
use warnings;
use Getopt::Long;
use Text::MeCab;
use TokyoCabinet;

use constant {
    DBM_KEYWORD2ID   => 'keyword2id.hdb',
    DBM_ID2KEYWORD   => 'id2keyword.hdb',
    DBM_KEYWORD_LINK => 'keywordlink.hdb',
};

sub usage_exit {
    print <<'USAGE';
Usage: 
 1) Make id <-> keyword table
  $ wp_keyword_link.pl -d directory -t < wikipedia_data
     -t, --table                 id <-> keyword table mode
     -d, --directory directory   output dir

 2) Make keyword-link DBM
  $ wp_keyword_link.pl -d directory -l < wikipedia_data
     -l, --link                  keyword link mode
     -d, --directory directory   output dir
USAGE
    exit 1;
}

sub word_ok {
    my $word = shift;
    return unless $word;
    return if $word =~ m{[:\|\n\t\[\]]};;
    return if $word =~ m{^[\#\s]};;
    $word =~ s/\s*\(.+?\)$//;
    return if $word =~ /一覧$/;
#    return if $word =~ /\d+月\d+日$/;
    return if $word =~ /^\d{4}年$/;
    return $word;
}

sub make_keyword_id_table {
    my $dir = shift;
    return if !$dir;

    # open dbms
    my $dbpath = "$dir/".DBM_KEYWORD2ID;
    my $kw2iddb = TokyoCabinet::HDB->new;
    if (!$kw2iddb->open($dbpath,
        $kw2iddb->OWRITER | $kw2iddb->OCREAT | $kw2iddb->OTRUNC)) {
        die "cannot open $dbpath";
    }
    $dbpath = "$dir/".DBM_ID2KEYWORD;
    my $id2kwdb = TokyoCabinet::HDB->new;
    if (!$id2kwdb->open($dbpath,
        $id2kwdb->OWRITER | $id2kwdb->OCREAT | $id2kwdb->OTRUNC)) {
        die "cannot open $dbpath";
    }

    my $flag = 0;
    my $content;
    while (my $line = <STDIN>) {
        chomp $line;
        if (!$flag && $line =~ m{<page>}) {
            $flag = 1
        }
        if ($flag) {
            $content .= $line;
        }
        if ($flag && $line =~ m{</page>}) {
            my ($title) = $content =~ m{<title>([^<]+)<};
            my ($id) = $content =~ m{<id>(\d+)<};
            $title = word_ok($title);
            if ($title) {
                print "$id\t$title\n";
                $kw2iddb->putasync($title, $id);
                #$kw2iddb->putasync($title, pack('N', $id));
                $id2kwdb->putasync($id, $title);
            }
            $flag = 0;
            $content = '';
        }
    }

    # close dbms
    if (!$kw2iddb->close()) {
        die 'DBM error: '.$kw2iddb->errmsg($kw2iddb->ecode);
    }
    if (!$id2kwdb->close()) {
        die 'DBM error: '.$id2kwdb->errmsg($id2kwdb->ecode);
    }
}

sub make_keyword_link {
    my $dir = shift;
    return if !$dir;

    my $dbpath = "$dir/".DBM_KEYWORD2ID;
    my $kw2iddb = TokyoCabinet::HDB->new;
    if (!$kw2iddb->open($dbpath, $kw2iddb->OREADER)) {
        die "cannot open $dbpath";
    }
    $dbpath = "$dir/".DBM_KEYWORD_LINK;
    my $kwlinkdb = TokyoCabinet::HDB->new;
    if (!$kwlinkdb->open($dbpath,
        $kwlinkdb->OWRITER | $kwlinkdb->OCREAT | $kwlinkdb->OTRUNC)) {
        die "cannot open $dbpath";
    }

    my $flag = 0;
    my $content;
    while (my $line = <STDIN>) {
        if (!$flag && $line =~ m{<page>}) {
            $flag = 1
        }
        if ($flag) {
            $content .= $line;
        }
        if ($flag && $line =~ m{</page>}) {
            my ($title) = $content =~ m{<title>([^<]+)<};
            my ($id) = $content =~ m{<id>(\d+)<};
            $title = word_ok($title);
            if ($title) {
                my %count;
                while ($content =~ m{\[\[(.+?)\]\]}g) {
                    my $word = word_ok($1);
                    next if !$word;
                    next if $word eq $title;
                    $count{$word}++;
                }
                next if !%count;
                my @words = sort { $count{$b} <=> $count{$a} } keys %count;
                my $buf;
                foreach my $word (@words) {
                    my $wid = $kw2iddb->get($word);
                    next if !$wid;
                    $buf .= pack('Nn', $wid, $count{$word});
                }
                $kwlinkdb->putasync($id, $buf);
                print "$id\t$title\n";
            }
            $flag = 0;
            $content = '';
        }
    }
   
    if (!$kwlinkdb->close()) {
        die 'DBM error: '.$kwlinkdb->errmsg($kwlinkdb->ecode);
    }
    if (!$kw2iddb->close()) {
        die 'DBM error: '.$kw2iddb->errmsg($kw2iddb->ecode);
    }
}

sub main {
    my ($opt_dir, $opt_table, $opt_link);
    GetOptions(
        'directory=s' => \$opt_dir,
        'table'       => \$opt_table,
        'link'        => \$opt_link,
    );
    if (!$opt_dir) {
        usage_exit();
    }
    elsif ($opt_table) {
        make_keyword_id_table($opt_dir);
    }
    elsif ($opt_link) {
        make_keyword_link($opt_dir);
    }
    else {
        usage_exit();
    }
}

main();

__END__

実際に使用するときは、まずは各キーワードの文字列とそのIDの対応表をDBMに保存する。

% bunzip2 -c jawiki-latest-pages-articles.xml.bz2 | perl wp_keyword_link.pl -t -d output
% ls output
id2keyword.hdb  keyword2id.hdb
% tchmgr list -pv id2keyword.hdb | head
5       アンパサンド
8       エスペラント
10      言語
11      日本語
12      地理学
13      欧州連合
14      EU
26      SandBox
27      漫画
28      日本
% tchmgr list -pv keyword2id.hdb | head
アンパサンド    1056181
エスペラント    828574
言語    10
日本語  11
地理学  12
欧州連合        13
EU      531237
SandBox 26
漫画    27
日本    795475

次にリンク構造を保存したDBMを作成する。このDBMではキーワードのIDをkeyとして、そのキーワードの説明ページからリンクしているキーワードのIDとそのリンク数のペアを複数個つなげたものをvalueとしている( id => id1,count1 id2,count2, ...)。ただしDBMのサイズ節約のために、value部分は文字列で保存するのではなく、perlのpack関数を使ってバイナリで保存している(pack('Nn*', $val))。

% bunzip2 -c jawiki-latest-pages-articles.xml.bz2 | perl wp_keyword_link.pl -l -d output
% ls output
id2keyword.hdb  keyword2id.hdb  keywordlink.hdb

リンクDBからデータを取得するときは、以下のようなスクリプトから行う。

#!/usr/bin/perl 

use strict;
use warnings;
use TokyoCabinet;
use Data::Dumper;

my $dbpath = 'output/keywordlink.hdb';
my $dbm = TokyoCabinet::HDB->new;
if (!$dbm->open($dbpath, $dbm->OREADER)) {
    die "cannot open $dbpath";
}

$dbm->iterinit();
while (my $key = $dbm->iternext()) {
    my $val = $dbm->get($key);
    my %count = unpack('(Nn)*', $val);
    print "$key\n";
    print Dumper(\%count);
}

これでWikipediaのデータを使って、HITSやPageRankなどいろいろなグラフ理論の手法を試すことができるね!

次回はこのデータを使って、実際にHITSもしくはPageRankwikipediaキーワードリンク構造に適用してみよう。