2012年1月12日木曜日

Rubyでコーパス言語学(5)

ここまでのまとめ

第2回から第4回までで示したコードを以下にまとめた。単一のXMLファイルに対する処理はXMLというクラスに、複数のXMLファイルに対する処理はXMLFilesというクラスにまとめてある。次回以降、このファイルをxmlc.rbという名前で保存しておき、requireして使うことにする。

# -*- coding: utf-8 -*-
# xmlc.rb
require 'nokogiri'

class XML
  attr_accessor :xml
  def initialize(path)
    @xml = open(path){|f| Nokogiri::XML(f){|config| config.noblanks}}
  end
  def nodes(xpath = '//*')
    nodes = Hash.new(0)
    @xml.xpath('//*').each{|node| nodes[node.node_name] += 1}
    return nodes
  end
  def attributes(xpath = '//*')
    attributes = Hash.new(0)
    @xml.xpath(xpath).each do |node|
      node.attributes.keys.each do |attr|
        attributes["#{node.node_name}/@#{attr}"] += 1
      end
    end
    return attributes
  end
  def attr_values(xpath)
    values = Hash.new(0)
    @xml.xpath(xpath).each{|attr| values[attr.value] += 1}
    return values
  end
  def frequency(xpath, *attrs)
    attrs = [:text, :read, :base, :pos, :ctype, :cform] if attrs == []
    text = attrs.delete(:text)
    words = Hash.new(0)
    @xml.xpath(xpath).each do |word|
      items = []
      items << word.text if text
      items += attrs.map{|attr| word.attr(attr)}
      words[items] += 1
    end
    return words
  end
  def kwic(xpath)
    lines = []
    words = @xml.xpath(xpath)
    words.each do |tok|
      sen = tok.at_xpath('ancestor::sentence')
      senid = sen.attr('S-ID')
      tokid = tok.attr('id')
      prev = sen.xpath('.//tok[@id < %i]' % tokid)
      foll = sen.xpath('.//tok[@id > %i]' % tokid)
      lines << [senid, tokid, prev.text, tok.text, foll.text]
    end
    return lines
  end
  def regexp(re)
    list = []
    @xml.xpath('//sentence').each do |sen|
      pos1 = []; pos2 = [0]
      sen.text.scan(re){|s| pos1 << [$~.begin(0), $~.end(0)]}
      next if pos1 == []
      words = sen.xpath('.//tok')
      words.each{|tok| pos2 << pos2[-1] += tok.text.size}
      pos1.each do |b1, e1|
        b2 = pos2.find_index{|n| n > b1}
        e2 = pos2.find_index{|n| n >= e1}
        list << words[b2..e2]
      end
    end
    return list
  end
  def regexp_inspect(re)
    data = Hash.new(0)
    self.regexp(re).each do |words|
      key = words.map{|word|
        "#{word.text}(#{word.attr('pos').split('-').last})"
      }.join(' ')
      data[key] += 1
    end
    return data
  end
  def method_missing(sym, *args, &block)
    @xml.send sym, *args, &block
  end
end

class XMLFiles
  include Enumerable
  def initialize(path)
    @path = path
    @files = Dir.glob(path)
  end
  def each
    @files.each do |file|
      doc = open(file){|f| Nokogiri::XML(f){|config| config.noblanks}}
      yield doc
    end
  end
  def xpath(xpath)
    enum = Enumerator.new do |y|
      self.each do |doc|
        doc.xpath(xpath).each{|node| y << node}
      end
    end
    return enum
  end
end

上記のライブラリは以下のようにして使う。

# -*- coding: utf-8 -*-
require './xmlc.rb'
doc = XML.new('(中略)/950101.xml')

ここで変数docの中身はNokogiri::XML::Documentではなく新たに定義したXMLというクラスのオブジェクトであることに注意。Nokogiri::XML::Documentオブジェクトに直接アクセスしたい場合はdoc.xmlでアクセスすることができる。XMLクラスの役割は、第2回と第3回で示したような各種のメソッドを実装することである。XMLクラスには次のようなメソッドが実装してある。

  • nodes
  • attributes
  • attr_values
  • frequency
  • kwic
  • regexp
  • regexp_inspect

nodes、attributes、attr_valuesの各メソッドは、XML文書中のノード名、属性名、属性の値の一覧と頻度(出現数)を取得するためのメソッドである。戻り値はハッシュであるので、ppを使って表示すると見やすい。なおnodesとattributesは引数を省略すると全てのノードや属性をリストアップするが、引数としてxpathを指定することで特定の種類のノードやその属性のみリストアップすることもできる。nodesとattributesの引数となるxpathはノード(".../tok"など)、attr_valueの引数となるxpathは属性(".../@pos"など)でなければならない。

require 'pp'
pp doc.nodes
pp doc.attributes
pp doc.attr_values('//tok/@pos')

[結果]
{"document"=>1, "sentence"=>1134, "chunk"=>10268, "tok"=>26739}

{"sentence/@S-ID"=>1134,
 "sentence/@KNP"=>1134,
 (中略)
 "tok/@cform"=>26739,
 "sentence/@MEMO"=>14}

{"名詞-人名"=>623,
 "名詞-普通名詞"=>4756,
 (中略)
 "感動詞"=>11,
 "未定義語-その他"=>2}

frequencyメソッドは特定の語の頻度を数えるためのメソッドである。第1引数には検索する語をxpathで指定する。活用語の活用形のリストを取得するためにも使える。

pp doc.frequency('//tok[@pos="判定詞"]')

[結果]
{["な", "な", "だ", "判定詞", "判定詞", "ダ列基本連体形"]=>7,
 ["で", "で", "だ", "判定詞", "判定詞", "ダ列タ系連用テ形"]=>53,
 (中略)
 ["であろう", "であろう", "だ", "判定詞", "判定詞", "デアル列基本推量形"]=>1,
 ["だろう", "だろう", "だ", "判定詞", "判定詞", "ダ列基本推量形"]=>1}

frequencyメソッドは、第2引数以降で出力する属性の種類を指定することができる。省略した場合のデフォルトは[:text, :read, :base, :pos, :ctype, :cform]である(:textはtext()関数として解釈される)。第2引数以降を指定した場合は、指定した属性に基づいて集計が行われる。

pp doc.frequency('//tok[@pos="判定詞"]', :pos)

[結果]
{["判定詞"]=>212}

kwicメソッドはkwicもどきの結果を二次元配列で返す。

pp doc.kwic('//tok[@pos="判定詞"]')

[結果]
[["950101008-006",
  "11",
  "ようやく経済も明るさを取り戻しつつある微妙な段階",
  "な",
  "ので、今は解散の時期ではないと考えている。"],
 ["950101008-006",
  "19",
  "ようやく経済も明るさを取り戻しつつある微妙な段階なので、今は解散の時期",
  "で",
  "はないと考えている。"],
(以下略)

regexpメソッドとregexp_inspectメソッドは正規表現検索してマッチした部分のノードを返す。regexpメソッドの戻り値はNokogiri::XML::NodeSetのリストである。regexp_inspectは見やすいように文字列化して集計したものを返す。

pp doc.regexp_inspect(/とは/)

[結果]
{"こと(形式名詞) は(副助詞)"=>12,
 "えと(普通名詞) は(副助詞)"=>1,
 "と(格助詞) は(副助詞)"=>10,
 "もともと(副詞) は(副助詞)"=>1,
 "あと(副詞) は(副助詞)"=>2}

その他、XMLクラスにはmethod_missingメソッドが仕込んであり、未定義のメソッドは@xmlに丸投げする。例えば、doc.xpath(...)は実際にはdoc.xml.xpath(...)として実行される。もう一つのXMLFilesクラスは第4回で示したのと同じものなのでここでは説明しない。XMLFilesクラスは今のところeachとxpathしかメソッドを定義していないが、今後拡張していくことにする。

0 件のコメント:

コメントを投稿