Edited at

スクレイピングを行いLDAを適用したときの作業ログ

More than 1 year has passed since last update.


概要

いくつかのURLからRubyでスクレイピングを行い、PythonでLDAを利用してトピックを抽出した。


  1. スクレイピング

  2. 形態素解析

  3. オリジナル辞書

  4. データ整形

  5. LDA実行


1. スクレイピング


Mechanizeの利用


gem install

$ bundle init

$ vim Gemfile

gem 'mechanize'

$ bundle install


mechanizeの利用

以下のようなサンプルファイルで動作すればOK。


sample.rb

require 'mechanize'

agent = Mechanize.new
search_page = agent.get('適当なURL')

search_page.search('body p').each do |y|
p y.text
end



2. 形態素解析


Mecabのインストール on Mac

$ brew search mecab

mecab mecab-ipadic

$ brew install mecab mecab-ipadic

$ mecab

mecabが起動すればOK


Nattoの利用

nattoは、システムにインストールされたmecabをラッパーするgem。


gem install

$ bundle init

$ vim Gemfile

gem 'natto'

$ bundle install


MECAB_PATHの指定

nattoを使用するためにはMECAB_PATHという環境変数を指定する必要がある。

$ find /usr/ -name "*mecab*" | grep dylib 

$ export MECAB_PATH=/usr//local/Cellar/mecab/0.996/lib/libmecab.dylib


http://yatta47.hateblo.jp/entry/2015/12/13/150525

https://github.com/buruzaemon/natto


mecabの利用

以下のようなサンプルファイルで動作すればOK。


sample.rb

require 'natto'

text = 'すもももももももものうち'
nm = Natto::MeCab.new
nm.parse(text) do |n|
puts "#{n.surface}\t#{n.feature}"
end


http://qiita.com/shizuma/items/d04facaa732f606f00ff

http://d.hatena.ne.jp/otn/20090509


3. オリジナル辞書

本来作るべきだが今回は省略。

今回はその代わりに、名詞、一般で代名詞と非自立を除く。

cond1 = features.include?('名詞')

cond2 = features.include?('一般')
cond3 = !features.include?('代名詞')
cond4 = !features.include?('非自立')
if cond1 && cond2 && cond3 && cond4
# 必要な処理
end


4. スクレイピングからデータ整形までのソースコード


目的

pythonとruby間ではjsonを使ってデータのやり取りを行う。具体的には以下のような対象ページのURLをまとめたcsvを用意し、そこからスクレイピングを行いLDAに必要なデータ構造に変換する。

url

URL1

URL2

...

URLN

最終的にdocumentごとに単語が並んだ以下のような配列を生成しjsonとして出力する。

[

['human', 'interface', 'computer'],
['survey', 'user', 'computer', 'system', 'response', 'time'],
['eps', 'user', 'interface', 'system'],
['system', 'human', 'system', 'eps'],
['user', 'response', 'time'],
['trees'],
['graph', 'trees'],
['graph', 'minors', 'trees'],
['graph', 'minors', 'survey']
]

http://tohka383.hatenablog.jp/entry/20111205/1323071336

http://peaceandhilightandpython.hatenablog.com/entry/2013/12/06/082106


実際のソースコード

gem 'mechanize'

gem 'natto'

# csvからURLの配列を生成するクラス

class UrlGetService
require 'csv'

def initialize(csv_path)
@csv_path = csv_path
end

def web_urls
@web_urls ||= -> do
rows = []
csv_file.each_with_index do |row, index|
unless index == 0
rows << row[0]
end
end
rows
end.call
end

private

attr_reader :csv_path

def csv_file
@csv_file ||= -> do
csv_text = File.read(csv_path)
CSV.parse(csv_text)
end.call
end
end

# 与えられたURLに対してスクレイピングを行うクラス
class WebScrapingService
require 'mechanize'

def initialize(url)
@url = url
end

def texts
@texts ||= -> do
texts = ''
page_contents.each do |content|
texts += content.text
end
texts
end.call
end

private

attr_reader :url

def page_contents
@page_contents ||= scraping_agent.get(url).search('body p')
end

def scraping_agent
@scraping_agent ||= Mechanize.new
end
end

# スクレイピングの結果を形態素解析し単語の配列を作るクラス
class MorphologicalAnalysisService
require 'natto'
`export MECAB_PATH=/usr//local/Cellar/mecab/0.996/lib/libmecab.dylib`

def initialize(texts)
@texts = texts
end

def words
words = []
morphological_analysis_agent.parse(texts) do |word|
features = word.feature.split(/,/)
cond1 = features.include?('名詞')
cond2 = features.include?('一般')
cond3 = !features.include?('代名詞')
cond4 = !features.include?('非自立')
if cond1 && cond2 && cond3 && cond4
words << word.surface
end
end
words
end

private

attr_reader :texts

def morphological_analysis_agent
@morphological_analysis_agent ||= Natto::MeCab.new
end
end

# 3つのクラスを利用してJSONをdumpするクラス
class DictionaryOutputService
require 'json'

def initialize(csv_path)
@csv_path = csv_path
end

def output_json
open('sample.json', 'w') do |f|
JSON.dump(words_array, f)
end
end

private

attr_reader :csv_path

def words_array
@words_array ||= -> do
web_urls.each_with_object([]) do |url, arr|
texts = WebScrapingService.new(url).texts
words = MorphologicalAnalysisService.new(texts).words
white_lists = words.inject(Hash.new(0)) { |h, a| h[a] += 1; h }.select { |_, c| c > 1 }.map { |w, _| w }
arr << words.select { |w| white_lists.include?(w) }
end
end.call
end

def web_urls
UrlGetService.new(csv_path).web_urls
end
end

# 以下のように実行する
csv_path = "YOUR_CSV_PATH/file_name.csv"
DictionaryOutputService.new(csv_path).output_json


5. LDA実行


pyenvによるバージョン管理

システムのpythonをそのまま使うのではなく、インストールしバージョン管理したpythonを利用する。

git clone https://github.com/yyuu/pyenv.git ~/.pyenv


~/.bashrc

export PYENV_ROOT=$HOME/.pyenv

export PATH=$PYENV_ROOT/bin:$PATH
eval "$(pyenv init -)"

3.5系であればgensimのインストールで転けない。

sourve ~/.bashrc

pyenv install 3.5.0
pyenv shell 3.5.0


http://qiita.com/Kodaira_/items/feadfef9add468e3a85b


gensimのインストール

pythonでLDAを行うためにはgensimというモジュールを使用する。setuptoolsがgensimのインストールのために必要

sudo easy_install -U setuptools

gensimのインストールを行う。numpyなどの依存ツールもupdateする。

sudo -H pip install gensim -U


ソースコード


lda.py

from gensim import models, corpora

if __name__ == '__main__':
# 本来はこのtextsはJSONファイルなどを読み込む
texts = [['human', 'interface', 'computer'],
['survey', 'user', 'computer', 'system', 'response', 'time'],
['eps', 'user', 'interface', 'system'],
['system', 'human', 'system', 'eps'],
['user', 'response', 'time'],
['trees'],
['graph', 'trees'],
['graph', 'minors', 'trees'],
['graph', 'minors', 'survey']]

dictionary = corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]

lda = models.ldamodel.LdaModel(corpus=corpus, num_topics=20, id2word=dictionary)

# Topics
for topic in lda.show_topics(-1):
print('topic')
print(topic)

# Topic of each document
for topics_per_document in lda[corpus]:
print('topic of ecah document')
print(topics_per_document)



https://radimrehurek.com/gensim/tut1.html#corpus-formats

https://openbook4.me/projects/193/sections/1154

http://sucrose.hatenablog.com/entry/2013/10/29/001041


参考: RでLDAを実行

# 必要なパッケージ軍

install.packages("lda")
install.packages("ggplot2")
install.packages("reshape2")

# フリーデータ
data(cora.documents)
data(cora.vocab)

## トピック数
K <- 10

# 関数の実行
result <- lda.collapsed.gibbs.sampler(cora.documents,
K, # トピック数
cora.vocab,
25, # サンプリング回数
0.1, # ハイパーパラメタα
0.1, # ハイパーパラメタβ
compute.log.likelihood=TRUE)

# トピック毎の頻出単語トップ5
top.words <- top.topic.words(result$topics, 5, by.score=TRUE)