CaboChaで処理されたデータをもとに段落をオブジェクトとして扱うためのParagraphクラス

第4回北海道開発オフでは、Ruby構文解析CaboChaの簡単なテストを動かしてみました。まともにRubyに取り組んだのは久々でしたが(かつてはクラスも書かずに挫折……!)、あらためてゴリゴリ書いてみて、書きやすいなという印象を受けました。
そんなわけで、いい機会なので、一つの段落を配列の配列として扱えるクラスを書いてみました。設計やら構成やらが中途半端なのは気に留めない方向で!

paragraph.rb

#!/usr/bin/ruby

require 'kconv'
require 'CaboCha'

$KCODE = 'u'

class Paragraph < Array
  begin
    Cab = CaboCha::Parser.new([$0] + ARGV)
  rescue Exception
    retry
  end
  def initialize(line)
    return if line.size > 5000
    begin
      tree = Cab.parse(line.kconv(Kconv::EUC, Kconv::UTF8))
    rescue Exception
      tree = []
      return
    end
    punct   = true
    cid     = 0
    revise  = 0
    linking = {}
    tree.size.times do |i|
      begin
        token = Token.new(tree.token(i))
      rescue Exception
        retry
      end
      if token.hasChunk then
        if punct then
          self.push([])
          linking = {}
          punct = false
        end
        linked = cid - revise
        linkin = token.chunk.link - revise
        begin
          self[-1].push(Chunk.new(linkin))
        rescue Exception
          retry
        end
        if linking.keys.include? linked then
          self[-1][-1].linked = linking[linked]
        end
        if linking.keys.include? linkin then
          linking[linkin].push(linked)
        else
          linking[linkin] = [linked]
        end
        cid += 1
      end
      if token.pos[/^記号-(読点|空白|括弧開|括弧閉)/u] then
        next
      elsif token.pos[/^記号-句点/u] then
        punct = true
        self[-1][-1].link = -1
        revise = cid
      elsif token.pos[/^(()?|(名詞|動詞)-非自立|動詞-接尾)/u] && self[-1].size < 1 then
        next
      else
        self[-1][-1].push_token(token)
      end
    end
    self[-1][-1].link = -1
  end
end

class Paragraph::Chunk
  def initialize(link)
    @head    = Head.new
    @func    = Func.new
    @link    = link
    @linked  = []
    @to_func = false
  end
  def push_token(token)
    if token.pos[/^(()?|(名詞|動詞)-非自立|動詞-接尾)/u] && @head.size > 0 then
      @to_func = true
    end
    if @to_func then
      @func.push_token(token)
    else
      @head.push(token)
    end
  end
  attr_accessor :head, :func, :link, :linked
end

class Paragraph::Chunk::Head < Array
  def surface
    if self.size == 0
      ''
    else
      self.map{|token| token.surface}.join
    end
  end
  def base
    if self.size == 0 then
      '' 
    elsif self.size == 1 then
      self[0].base
    else
      self[0..-2].map{|token| token.surface}.join + self[-1].base
    end
  end
  def pos
    return nil if self.size < 1
    if self[-1].pos[/^名詞-非自立/u] then
      'noun-affix'
    elsif self[-1].pos[/^名詞/u] then
      'noun'
    elsif self[-1].pos[/^動詞/u] then
      'verb'
    elsif self[-1].pos[/^形容詞/u] then
      'adjective'
    elsif self[-1].pos[/^副詞/u] then
      'adverb'
    elsif self[-1].pos[/^連体詞/u] then
      'adnominal'
    elsif self[-1].pos[/^接続詞/u] then
      'conjunction'
    elsif self[-1].pos[/^感動詞/u] then
      'exclamation'
    else
      nil
    end
  end
end

class Paragraph::Chunk::Func < Array
  def initialize
    @flag = true
  end
  def push_token(token)
    if @flag then
      self.push(Part.new)
    end
    if token.pos[/^助詞-係助詞/u] then
      self.push(Part.new) if self[-1].size > 0
      @flag = true
    elsif token.ctype.size > 0 then
      @flag = true
    else
      @flag = false
    end
    self[-1].push(token)
  end
end

class Paragraph::Chunk::Func::Part < Array
  def surface
    self.map{|e| e.surface}.join
  end
  def base
    last = self.last
    self[0..-2].map{|e| e.surface}.join + last.base
  end
end

class Paragraph::Token
  def initialize(token)
    @hasChunk = token.hasChunk
    @chunk    = token.chunk if token.hasChunk
    @surface  = token.surface.kconv(Kconv::UTF8, Kconv::EUC) # 表層型
    @base     = token.base.kconv(Kconv::UTF8, Kconv::EUC)    # 原型
    @read     = token.read.kconv(Kconv::UTF8, Kconv::EUC)    # 読み
    @pos      = token.pos.kconv(Kconv::UTF8, Kconv::EUC)     # 品詞
    @ctype    = token.ctype.kconv(Kconv::UTF8, Kconv::EUC)   # 活用型
    @cform    = token.cform.kconv(Kconv::UTF8, Kconv::EUC)   # 活用形
    @ne       = token.ne                                     # 固有表現
  end
  attr_reader :hasChunk, :chunk, :surface, :base, :read, :pos, :ctype, :cform, :ne
end

test_paragraph.rb

#!/usr/bin/ruby

require 'paragraph'

$KCODE = 'u'

loop do
  print 'INPUT> '
  line = gets.chomp
  if line == '' then
    puts 'exit.'
    break
  end
  p = Paragraph.new(line)
  p.each_with_index do |s, index|
    puts "文<#{index + 1}>:"
    s.each_with_index do |c, index|
      puts "#{index}: [#{c.head.surface}(#{c.head.base})] => #{c.link}"
      if c.head.surface.size > 0 then
        puts '>>' + c.head.last.pos
      end
      c.func.each do |t|
        puts "#{t.surface}(#{t.base})"
      end
      if c.linked.length > 0 then
        puts c.linked.join(', ')
      end
    end
  end
end

出力

$ ruby test_paragraph.rb
INPUT> キリンさんが好きです。でも、ゾウさんのほうがもっと好きです。
文<1>:
0: [キリンさん(キリンさん)] => 1
>>名詞-接尾-人名
が(が)
1: [好き(好き)] => -1
>>名詞-形容動詞語幹
です(です)
0
文<2>:
0: [でも(でも)] => 4
>>接続詞
1: [ゾウさん(ゾウさん)] => 2
>>名詞-接尾-人名
の(の)
2: [ほう(ほう)] => 4
>>名詞-非自立-一般
が(が)
1
3: [もっと(もっと)] => 4
>>副詞-一般
4: [好き(好き)] => -1
>>名詞-形容動詞語幹
です(です)
0, 2, 3

反省会

  • Rubyなら私も幸せになれるやも!(これをPerlでやるとめんどい)
  • 句点以外(「!」や「?」)でも文を区切れるようにしとこう(CaboChaの問題でこれは難しいっぽい)
  • Perlのくせが抜けていない気がする(「動くからいいや」とか思っちゃうあたり)