2013-06-09 :-)
_ [はてな][スクレイピング][ruby][mechanize]はてなブログをスクレイピングする
# coding: utf-8 # hatenablog をスクレイピング # # 使い方: # hatenablog.rb <hatenablog URI> [カテゴリ] # # 例: # ruby hatenablog.rb http://jkondo.hatenablog.com/ > jkondo.txt # # ruby hatenablog.rb http://dennou-kurage.hatenablog.com/ 仕事観 > kurage.txt # require 'mechanize' require 'uri' require 'pp' def get_text(uri) agent = Mechanize.new agent.get(uri) texts = "" while true agent.page.at("//div[@class='entry-content']").children.each {|node| text = node.text texts << text } link = agent.page.link_with(:text => '次のページ') break if link == nil agent.page.link_with(:text => '次のページ').click end return texts end def main(argv) uri_base = argv[0] cat = "" if argv[1] != nil cat = "category/" + URI::encode(argv[1]) end uri = uri_base + cat text = get_text(uri) puts text end main(ARGV)
_ [自然言語処理][係り受け解析][構文解析][ruby][NLP]テキストを係り受け解析する
前提
テキスト解析サンプルコード:係り受け解析 - Yahoo!デベロッパーネットワーク
日本語係り受け解析API の制限が以下のようになっている。
日本語係り受け解析Web APIは、24時間以内で1つのアプリケーションIDにつき50000件のリクエストが上限となっています。また、1リクエストの最大サイズを4KBに制限しています。詳しくは「利用制限」をご参照ください。
UTF-8 エンコード 1 文字は最大 4 バイトでエンコードされるとする。
それを URI.encode すると最大 12 バイトになると見積もる
( たとえば 4 バイト 0xAA 0xBB 0xCC 0xDD であるとすると、これが URI.encode されるとテキストで表現されるので %AA %BB %CC %DD の 12 文字、つまり 12 バイトになる )
↓リクエストパラメータのうち &sentence= までで 152 バイトある。
http://jlp.yahooapis.jp/DAService/V1/parse?appid=<あなたのアプリケーションID>&sentence=
↓リクエストに指定できる sentence としては 3848 バイト。
(4 * 1000) - 152 = 3848
上述のように 1 文字 12 バイトとして 320 文字まで指定できる。
3848 / 12 = 320
320 文字はギリギリなので、これをプログラムで扱うときは。まあざっくり 300 文字までとしてみる。
与えられたテキストが長文だった場合を考慮して、最初に「。」でぶったぎっておいて、300 文字を超えないように文を再度連結していく。しかしそもそも「。」までが 300 文字を超えるような文もあるわけだが、それは無視する(どこで区切るのか割りと重要かもそれんけどよく分からないのでスルー)。
実装
とりあえず作ったのがこちら。
syntactic.rb としておく。
# coding: utf-8 # Yahoo デベロッパーネットワーク の 日本語係り受け解析API を使う # http://developer.yahoo.co.jp/webapi/jlp/da/v1/parse.html require 'uri' require 'open-uri' require 'rexml/document' require 'pp' module NLP module Syntactic SENTENCE_LENGTH_MAX = 300 def parse(xml) syntactic ||= [] doc = REXML::Document.new(xml) doc.elements.each('ResultSet/Result/ChunkList/Chunk') do |chunk| chunks = "" chunk.elements.each('MorphemList') do |ml| ml.elements.each("Morphem/Surface") do |mo| chunks << mo.text end end syntactic << chunks end return syntactic end def build_sweep(text) text.gsub!("\n", "") text.rstrip! text.lstrip! text.gsub!(/\A +/, "") text.gsub!(/ /, "") text.chomp! text.gsub!(/[\r\n]/, "") return text end def build_sentence(text) text = build_sweep(text) request_sentence ||= [] if text.length < SENTENCE_LENGTH_MAX request_sentence << text else lump = "" text.split(/。/).each {|sentence| s = sentence + "。" if (lump + s).length < SENTENCE_LENGTH_MAX lump << s else request_sentence << lump lump = "" lump << s end } end return request_sentence end # 文章が長いのは無視 def _analysis(text) apiuri = "http://jlp.yahooapis.jp/DAService/V1/parse" appid = "?appid=" + ここにあたなのAPI IDを入れる sentence = "&sentence=" + URI.encode(text) request_uri = apiuri + appid + sentence syntactics = "" begin response = open(request_uri).read() syntactics = parse(response) rescue => e end return syntactics end def analysis(text) syntactics ||= [] sentences = build_sentence(text) sentences.each {|s| syntactics << _analysis(s) } return syntactics end end end
使うときはこう。
# coding: utf-8 # # 与えられたファイルを係り受け解析する # require 'pp' require './syntactic' include NLP::Syntactic def build(text) return NLP::Syntactic::analysis(text) end def read(text) File.open(text).read end def main(argv) infile = argv[0] text = read(infile) syntactic = build(text) puts syntactic.join("\n") end main(ARGV)
_ [ベイズ][自然言語処理][ナイーブベイズ][ruby][NLP]文章をナイーブベイズする
以前作業しておいたナイーブベイズ[ 20130505#p04 ] を利用する。
# -*- encoding: utf-8 -*- # # ナイーブベイズを用いたテキスト分類 - 人工知能に関する断創録 # http://aidiary.hatenablog.com/entry/20100613/1276389337 # def maxint() return 2 ** ((1.size) * 8 -1 ) -1 end def sum(data) return data.inject(0) {|s, i| s + i} end include Math require 'pp' require 'json' require 'yaml' module NLP # Multinomial Naive Bayes class NaiveBayes def initialize() # カテゴリの集合 @categories = [] # ボキャブラリの集合 @vocabularies = [] # wordcount[cat][word] カテゴリでの単語の出現回数 @wordcount = {} # catcount[cat] カテゴリの出現回数 @catcount = {} # denominator[cat] P(word|cat)の分母の値 @denominator = {} end # ナイーブベイズ分類器の訓練 def train(data) data.each {|d| cat = d[0] @categories << cat } @categories.each {|cat| @wordcount[cat] ||= {} @wordcount[cat].default = 0 @catcount[cat] ||= 0 } # 文書集合からカテゴリと単語をカウント data.each {|d| cat, doc = d[0], d[1, d.length-1] @catcount[cat] += 1 doc.each {|word| @vocabularies << word @wordcount[cat][word] += 1 } } @vocabularies.uniq! # 単語の条件付き確率の分母の値をあらかじめ一括計算しておく(高速化のため) @categories.each {|cat| s = sum(@wordcount[cat].values) @denominator[cat] = s + @vocabularies.length } end # 事後確率の対数 log(P(cat|doc)) がもっとも大きなカテゴリを返す def classify(doc) best = nil max = -maxint() @catcount.each_key {|cat| _p = score(doc, cat) if _p > max max = _p best = cat end } return best end # 単語の条件付き確率 P(word|cat) を求める def wordProb(word, cat) return (@wordcount[cat][word] + 1).to_f / (@denominator[cat]).to_f end # 文書が与えられたときのカテゴリの事後確率の対数 log(P(cat|doc)) を求める def score(doc, cat) # 総文書数 total = sum(@catcount.values) # log P(cat) sc = Math.log((@catcount[cat]) / total.to_f) doc.each {|word| # log P(word|cat sc += Math.log(wordProb(word, cat)) } return sc end # 総文書数 # def to_s() # total = sum(@catcount.values) # return "documents: #{total}, vocabularies: #{@vocabularies.length}, categories: #{@categories.length}" # end end end # end of module if __FILE__ == $0 # Introduction to Information Retrieval 13.2の例題 data = [ ["yes", "Chinese", "Beijing", "Chinese"], ["yes", "Chinese", "Chinese", "Shanghai"], ["yes", "Chinese", "Macao"], ["no", "Tokyo", "Japan", "Chinese"] ] # ナイーブベイズ分類器を訓練 nb = NLP::NaiveBayes.new nb.train(data) p nb puts "P(Chinese|yes) = #{nb.wordProb('Chinese', 'yes')}" puts "P(Tokyo|yes) = #{nb.wordProb('Tokyo', 'yes')}" puts "P(Japan|yes) = #{nb.wordProb('Japan', 'yes')}" puts "P(Chinese|no) = #{nb.wordProb('Chinese', 'no')}" puts "P(Tokyo|no) = #{nb.wordProb('Tokyo', 'no')}" puts "P(Japan|no) = #{nb.wordProb('Japan', 'no')}" # テストデータのカテゴリを予測 test = ['Chinese', 'Chinese', 'Chinese', 'Tokyo', 'Japan'] puts "log P(yes|test) = #{nb.score(test, 'yes')}" puts "log P(no|test) = #{nb.score(test, 'no')}" puts nb.classify(test) end
_ [構文解析][係り受け解析][社畜][ベイズ][自然言語処理][ナイーブベイズ][ruby][NLP]文章が社畜的かどうかをナイーブベイズ推定する
準備
社畜的文章の学習データとして 脱社畜ブログ のカテゴリ 仕事観 を利用する。
非社畜的文章の学習データとして jkondoのはてなブログ を利用する。
先ほどのコード[ 20130609#p04 ]でスクレイピングする。
% ruby hatenablog.rb http://jkondo.hatenablog.com/ > jkondo.txt
% ruby hatenablog.rb http://dennou-kurage.hatenablog.com/ 仕事観 > kurage.txt
先ほどのコード[ 20130609#p05 ]で係り受け解析しておく
% ruby parse.rb kurage.txt > kurage.2.txt
% ruby parse.rb jkondo.txt > jkondo.2.txt
判定の実装
先ほどのナイーブベイズのコード [ 20130609#p06 ] を利用する。
企業戦士として名高い クラウド・ストライフさん のセリフを社畜判定してみる。( FFシリーズ セリフ人気投票 )
# coding: utf-8 require './naivebayes' require './syntactic' include NLP::Syntactic def get_words(text) words = NLP::Syntactic::analysis(text) words.flatten! words.map! {|w| w.gsub(/[、。\n]/, "") } return words end def build_learning(filepath, cat) lines = File.open(filepath).readlines() lines.map! {|w| w.gsub(/[、。\n]/, "") } data = [cat, *lines] return data end def classify(nb, text) words = get_words(text) cat = nb.classify(words) # puts nb.score(words, "社畜") # puts nb.score(words, "人間") return "#{text} => #{cat}" end def main(argv) shatiku_file = argv[0] not_shatiku_file = argv[1] shatiku_data = build_learning(shatiku_file, "社畜") not_shatiku_data = build_learning(not_shatiku_file, "人間") nb = NLP::NaiveBayes.new nb.train([shatiku_data]) nb.train([not_shatiku_data]) text = %w( 興味ないね エアリスはもう喋らない・・・笑わない・・・泣かない、怒らない・・・! 大切じゃない物なんか無い! 俺は俺の現実を生きる 俺は、お前の生きた証だ 指先がチリチリする。口の中はカラカラだ。目の奥が熱いんだ! オレが・・・お前の生きた証・・・ 引きずりすぎて少しすり減ったかな・・・ お前の分まで生きよう。そう決めたんだけどな… もう・・・揺るがないさ・・ 帰るぞ 俺は幻想の世界の住人だった。でも、もう幻想はいらない……俺は俺の現実を生きる ここに女装に必要な何かがある。おれにはわかるんだ。いくぜ! 星よ・・・降り注げ!! 罪って…許されるのかな? まだ終わりじゃない…終わりじゃないんだ! 俺はクラウド、ソルジャークラス1st ) text.each {|t| puts classify(nb, t) } end main(ARGV)
実行
% ruby shatiku.rb kurage.2.txt jkondo.2.txt 興味ないね => 社畜 エアリスはもう喋らない・・・笑わない・・・泣かない、怒らない・・・! => 社畜 大切じゃない物なんか無い! => 社畜 俺は俺の現実を生きる => 社畜 俺は、お前の生きた証だ => 社畜 指先がチリチリする。口の中はカラカラだ。目の奥が熱いんだ! => 社畜 オレが・・・お前の生きた証・・・ => 社畜 引きずりすぎて少しすり減ったかな・・・ => 人間 お前の分まで生きよう。そう決めたんだけどな… => 社畜 もう・・・揺るがないさ・・ => 社畜 帰るぞ => 社畜 俺は幻想の世界の住人だった。でも、もう幻想はいらない……俺は俺の現実を生きる => 社畜 ここに女装に必要な何かがある。おれにはわかるんだ。いくぜ! => 人間 星よ・・・降り注げ!! => 社畜 罪って…許されるのかな? => 社畜 まだ終わりじゃない…終わりじゃないんだ! => 人間 俺はクラウド、ソルジャークラス1st => 社畜
ふむ。