LoginSignup
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-09-09

概要

いくつかの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

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)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3