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


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

1. スクレイピング
2. 形態素解析
3. オリジナル辞書
4. データ整形
5. LDA実行


# 1. スクレイピング

## Mechanizeの利用

### gem install

```bash
$ bundle init
$ vim Gemfile
```

```Gemfile
gem 'mechanize'
```

```bash
$ bundle install
```

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

```ruby: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

```bash
$ brew search mecab
mecab mecab-ipadic

$ brew install mecab mecab-ipadic
```

```bash
$ mecab
```

mecabが起動すればOK

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

### gem install

```bash
$ bundle init
$ vim Gemfile
```

```Gemfile
gem 'natto'
```

```bash
$ bundle install
```

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

```bash
$ 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。

```ruby: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. オリジナル辞書
本来作るべきだが今回は省略。

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

```ruby
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として出力する。

```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

## 実際のソースコード
```Gemfile
gem 'mechanize'
gem 'natto'
```

```ruby
# 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を利用する。

```bash
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のインストールで転けない。

```bash
sourve ~/.bashrc
pyenv install 3.5.0
pyenv shell 3.5.0
```

---

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

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

```bash
sudo easy_install -U setuptools
```

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

```bash
sudo -H pip install gensim -U
```

## ソースコード
```python: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を実行
```R
# 必要なパッケージ軍
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)
```