ニコニコ大百科解析事始め ~提供されるデータをJSONして触る~

# ニコニコ大百科データセットを使う
ニコニコ大百科データセットは IDR で公開されているニコニコ大百科の2008~2014年の記事とその記事へのコメントを集めたデータセットです。

知識抽出をはじめとする自然言語系の研究に向いていますが、Wikipedia のようにおとなしいデータセットではなく、かなり癖のあるデータセットとなっています。

例えばニコニコ大百科にある文は半分近くが主語が欠けている文章となっていますし、必ずしも文体が統一されているわけでもないですし、AAなんかも入っていたりします。

今回はこのデータセットの解析をしてみたいなーと思ってくれる **面白い人** を求めて簡単な前処理のツールとともにデータの中身を紹介していきます。


# 提供されているニコニコ大百科を前処理する
提供されているデータは **ちょっと特殊なCSV** で、適切な前処理を施すことで標準のCSVに変形することが出来ます。
また、HTMLはいくつかのタグが煩くて解析の都合上少し面倒になります。
このため

1. CSVを前処理する
2. ニコニコ大百科の記事(HTML)をJSONに変形する
3. ちらっと眺めてみる

をこの記事ではやってみようと思います。

# 前処理
前処理のために必要な環境として大事なのはメモリではなくディスク容量です。うっかり 50GB しか余剰容量がないと、前処理がエラーで落ちます。

また Python を使うなら CPU やメモリも多いほうが良いです。~~というか Pandas のパフォーマンスがあんまり…~~

## データ利用の申請
https://www.nii.ac.jp/dsc/idr/nico/nicopedia-apply.html

こちらから申請をします。申請をすると、少なくとも数日以内にダウンロードのための URL が頂けるので保管しておきましょう。

## 圧縮ファイルをダウンロードして展開する
URL からダウンロードしてこんな感じで適用に展開して下さい。

```
.
└── nico-dict
    └── zips
        ├── download.txt
        ├── head
        │   ├── head2008.csv
        │   ├── ...
        │   └── head2014.csv
        ├── head.zip
        ├── res
        │   ├── res2008.csv
        │   ├── ...
        │   └── res2014.csv
        ├── res.zip
        ├── rev2008.zip
        ├── rev2009
        │   ├── rev200901.csv
        │   ├── rev200902.csv
        │   ├── rev200903.csv
        │   ├── ...
        │   └── rev200912.csv
        ├── rev2009.zip
        ├──...
        ├── rev2013.zip
        ├── rev2014
        │   ├── rev201401.csv
        │   └── rev201402.csv
        └── rev2014.zip
```
## レポジトリのクローン
元々遅延評価や前処理の容易さからClojure (Lisp)を使って解析していたのですが、Pythonで解析できるように出来るだけ加工をしない HTML->JSON を行うためのツールを作りました。

https://github.com/MokkeMeguru/niconico-parser

からクローンして下さい。

```
git clone https://github.com/MokkeMeguru/niconico-parser
```

## CSV を整形する。

https://github.com/MokkeMeguru/niconico-parser/blob/master/resources/preprocess.sh　

を `zips/preprocess.sh` へコピーして 

```
sh preprocess.sh
```

して下さい。このファイルはCSVのエスケープを現代の仕様に修正するために必要な処理です。
(裏話：こいつの処理はかなりテストをしていますが、ひょっとするとバグがあるかもしれません。万が一バグがあったらコメントを下さい。)

## 記事ヘッダ情報をデータベースに保存する
ニコニコ大百科データセットは、大きく分けて
1. ヘッダ(記事ID、タイトル、タイトルの読み、カテゴリなど)
2. 記事本文
3. 記事へのコメント

になっています。この内 1. については簡単にデータベース化出来る程度の量なのでデータベースにしてしまします。

必要なファイルはhttps://github.com/MokkeMeguru/niconico-parser/blob/master/resources/create-table.sql と https://github.com/MokkeMeguru/niconico-parser/blob/master/resources/import-dir.sh です。
これらを `zips/head/<file>` となるように配置して

```
sh import-dir.sh
``` 

として下さい。すると `header.db` という sqlite3 の db が得られます。

試しにアクセスしてみましょう。

```
sqlite3 headers.db
sqlite3 > select * from article_header limit 10
 ...> ;
 1|ニコニコ大百科|ニコニコダイヒャッカ|a|20080512173939
 4|カレー|カレー|a|20080512182423
 5|初音ミクにオリジナルソング「貴方に花を 私に唄を」を歌わせてみた。|\N|v|20080719234213
 9|ゴーゴーカレー|ゴーゴーカレー|a|20080512183606
 13|本格的 ガチムチパンツレスリング|\N|v|20080513225239
 27|頭がパーン(P)┗(^o^ )┓三|\N|v|20080529215132
 33|【初音ミク】『少し楽しくなる時報』【アレンジ曲】|\N|v|20080810020937
 37|【 SYNC.ART'S × U.N.オーエンは彼女なのか？ 】 −Sweets Time−|\N|v|20080616003242
 46|ニコニコ動画流星群|\N|v|20080513210124
 47|ハイポーション作ってみた。|\N|v|20090102150209
```

ニコニコ大百科感が出ていて、Wikipediaにはない知識が得られそうな匂いがしますね。

## HTML->JSON!
ニコニコ大百科の記事の大きなの問題点を挙げるとするならば、変なタグが多いということを挙げることが出来ます。
Wikipediaとは異なり、整形のための `<br>` タグや `<span>` タグなどが溢れており、文を取り出そうとすると結構苦労する、というのが個人的な手応えでした。
(~~また AA なんかはほぼお手上げな状態です。AAのためのタグを作って…~~)

HTMLを解析するのに楽なのが DSL(domain-specific language) を使った方法です。有名なものだと [Kotlin のHTML解析のツール](https://github.com/Kotlin/kotlinx.html) があります。

今回はLispを使って簡単に処理させてみました。詳しいコードは…まあ良いでしょう()…

```
lein preprocess-corpus -r /path/to/nico-dict/zips
```

とまあこんな感じで実行して下さい。([Jar実行の場合はこちらから(求バグ報告)](https://github.com/MokkeMeguru/niconico-parser/releases/tag/0.8.0))
10~15 分くらいで 20~30GB くらいディスクを食べ切ります。

中身をちらっと見てみましょう。

```
head -n 1 rev2008-jsoned.csv 
```

```
1,"{""type"":""element"",""attrs"":null,""tag"":""body"",""content"":[{""type"":""element"",""attrs"":null,""tag"":""h2"",""content"":[""概要""]},{""type"":""element"",""attrs"":null,""tag"":""p"",""content"":[""ニコニコ大百科とは(省略)である。""]}]}",200xxxxxxxx939,[],ニコニコ大百科,ニコニコダイヒャッカ,a
```

1項目ずつ説明すると
1. 記事ID
2. JSON化＋前処理した記事
3. 記事更新日
4. ページ内に含まれているリンク (`<a>` タグ)のリスト
5. タイトル
6. タイトルのよみ
7. カテゴリ ("a"=単語 "v"=動画 "i"=商品 "l"=生放送 "c"=多分コミュニティ記事(仕様書にないカテゴリです))

今回はJSON化＋前処理の効力はあんまり紹介できないですが、例えば `<p>ほげ<span/>ほげ<br/>ばー</p>` みたいなのが扱いやすくなっているのと、グラフへ持ち込んだり [Snorkel](http://kento1109.hatenablog.com/entry/2018/01/09/121048) のようなツールを適用しやすくなっていることを挙げることが出来ます。

# ちょっと統計してみる
前処理ツール作ったよ！だけではあんまりにもつまらないので、ちょっと統計みたいなことをやってみましょう。
データ処理といえば Python + Pandas らしいので、Python + Pandas を使って調べてみることにします。(とはいえPandasは処理がめっちゃ重いとか遅いとかなので、本格的な解析をするなら[別のツール](https://github.com/vaexio/vaex/)を使って下さい。)

以下 Jupyter Notebook のように進めていきます。


## 依存関係のインポート
```python
import pandas as pd
import json
from pathlib import Path
from pprint import pprint
```

## グローバル変数の宣言
環境ごとに変えて下さい。

```python
############################
# グローバル変数 (適宜変更します) #
############################
# CSVのヘッダ
header_name = ('article_id', 'article', 'update-date',
               'links', 'title', 'title_yomi', 'category''')
dtypes = {'article_id': 'uint16',
          'article': 'object',
          'update-date': 'object',
          'links': 'object',
          'title': 'object',
          'title_yomi': 'object',
          'category': 'object'
}

# サンプルの CSV
sample_filepath = "/home/meguru/Documents/nico-dict/zips/rev2014/rev201402-jsoned.csv"
sample_filepath = Path(sample_filepath)

# サンプルの CSVs
fileparent = Path("/home/meguru/Documents/nico-dict/zips")
filepaths = [
    "rev2014/rev201401-jsoned.csv",
    "rev2014/rev201402-jsoned.csv",
    "rev2013/rev201301-jsoned.csv",
    "rev2013/rev201302-jsoned.csv",
    "rev2013/rev201303-jsoned.csv",
    "rev2013/rev201304-jsoned.csv",
    "rev2013/rev201305-jsoned.csv",
    "rev2013/rev201306-jsoned.csv",
    "rev2013/rev201307-jsoned.csv",
    "rev2013/rev201308-jsoned.csv",
    "rev2013/rev201309-jsoned.csv",
    "rev2013/rev201310-jsoned.csv",
    "rev2013/rev201311-jsoned.csv",
    "rev2013/rev201312-jsoned.csv",
]
filepaths = filter(lambda path: path.exists(),  map(
    lambda fpath: fileparent / Path(fpath), filepaths))
##################
```

## 前処理済みCSVを読み込むための関数の定義
```python
def read_df(csvfile: Path, with_info: bool = False):
    """read jsoned.csv file
    args:
    - csvfile: Path
    a file path you want to read
    - with_info: bool
    with showing csv's information
    returns:
    - df
    readed data frame
    notes:
    if you call this function, you will got some log message
    """
    df = pd.read_csv(csvfile, names=header_name, dtype=dtypes)
    print('[Info] readed a file {}'.format(csvfile))
    if with_info:
        df.info()
    return df


def read_dfs(fileparent: Path, csvfiles: List[Path]):
    """read jsoned.csv files
    args:
    - fileparent: Path
    parent file path you want to read
    - csvfiles: List[Path]
    file paths you want to read
    returns:
    - dfl
    concated dataframe
    note:
    given.
        fileparent = \"/path/to\"
        csvfiles[0] = \"file\"
    then.
        search file <= \"/path/to/file\"
    """
    dfl = []
    for fpath in filepaths:
        dfi = pd.read_csv(fileparent / fpath,
                          index_col=None, names=header_name, dtype=dtypes)
        dfl.append(dfi)
    dfl = pd.concat(dfl, axis=0, ignore_index=True)
    return dfl
```

## サンプルに 1 ファイル読み込んで眺める
今回はHTML内のリンク (`<a>` タグ) が記事の種類ごとにどんな風な散らばりを示しているのかを眺めてみることにします。

```python
df = read_df(sample_filepath, True)
# [Info] readed a file /home/meguru/Documents/nico-dict/zips/rev2014/rev201402-jsoned.csv
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 6499 entries, 0 to 6498
# Data columns (total 7 columns):
# article_id     6499 non-null int64
# article        6499 non-null object
# update-date    6499 non-null int64
# links          6499 non-null object
# title          6491 non-null object
# title_yomi     6491 non-null object
# category       6491 non-null object
# dtypes: int64(2), object(5)
# memory usage: 355.5+ KB
```

とりあえずこのファイルそのものは 6.5k の記事があることが確認できました。

次にリンクの数を計算するために、JSON化されたリンク情報をパースします。

```python
# 生データの確認
df['links'][0]
# => '[{"type":"element","attrs":{"href":"http://wwwxxxxhtml"},"tag":"a","content":["高知xxxxサイト"]}]'
dfs= pd.DataFrame()
dfs['links']= df['links'].map(lambda x: len(json.loads(x)))
dfs['links'][0]
# => 1
```

簡単に統計を取ってみましょう。

```python
dfs['category']=df['category']
dfsg=dfs.groupby('category')
dfsg.describe()
#            links                                                      
#            count       mean         std  min  25%   50%    75%     max
# category                                                              
# a         5558.0  41.687298  209.005652  0.0  0.0   2.0  11.00  2064.0
# c           36.0  54.305556  109.339529  0.0  2.0   2.0  38.25   376.0
# i            4.0   7.500000    5.507571  2.0  3.5   7.0  11.00    14.0
# l          786.0  22.760814  106.608535  0.0  0.0   2.0   9.00  1309.0
# v          107.0  32.887850   46.052744  0.0  3.0  11.0  37.00   153.0
```
"a"=単語 "v"=動画 "i"=商品 "l"=生放送 "c"=コミュニティ記事 ですから、平均的に見れば **コミュニティ記事のリンク** が多いことになります。
しかし中央値や最大値を見ると単語記事についてはより詳しく見てみる(分類してみる)必要がありそうだと観察できますね。

## サンプルデータを増やして確認してみる
6k 記事だけではちょっと物足りないので、データを増やしてみましょう。

```python
dfl = read_dfs(fileparent, filepaths)
# >>>         article_id                                            article  ...             title_yomi category
# 0             8576  {"type":"element","attrs":null,"tag":"body","c...  ...                  カベキック        a
# [223849 rows x 7 columns]
dfls = pd.DataFrame()
dfls['links'] = dfl['links'].map(lambda x: len(json.loads(x)))
dfls['category'] = dfl['category']
dflsg = dfls.groupby('category')
dflsg.describe()
#              links
#              count       mean         std  min  25%  50%   75%     max
# category
# a         193264.0  32.400566  153.923988  0.0  0.0  2.0  10.0  4986.0
# c           1019.0  34.667321   77.390967  0.0  1.0  2.0  34.0   449.0
# i            247.0   6.137652    6.675194  0.0  1.0  3.0  10.0    28.0
# l          24929.0  20.266477  100.640253  0.0  0.0  1.0   5.0  1309.0
# v           3414.0  14.620387   22.969974  0.0  1.0  6.0  16.0   176.0
```

全体を通してみると、動画のリンク数の低下に伴った、生放送と動画のリンクの平均値が逆転していることが確認できます。また単語記事のリンク数の振れ幅が大きすぎる点は 1 サンプルの場合と同じく確認できます。また直感に反するものとしては、 **単語記事は平均値よりも第3四分位が小さい** という結果が得られています。

以上の結果から少なくとも記事の種類ごとにリンク数は結構変わっていることがわかり、それぞれ個別に性質を観察してから研究したほうが良さそうだ、という気持ちになることができると思います。(ここからどのように研究して結果を出すかは見ている人に投げます)

## 記事のリンク数と記事サイズは相関があるのか
前の実験から特に単語記事について分散が大きいことが確認できたと思います。この原因として **普段ニコニコ大百科を眺めている経験と勘から** 、記事サイズとリンク数の相関というのが思いつきました。なのでJSON化したデータの文字数を記事サイズとみなしてパッと相関を調べてみます。

```python
dfts.corr()
#                  links  article_size
# links         1.000000      0.713465
# article_size  0.713465      1.000000
```

とまあ少なくとも強い正の相関はありそうです。

もうちょっと踏み込むとこんな感じになります。

```python
# 単語記事について
dfts[dfts['category'] == "a"].loc[:, ["links", "article_size"]].corr()
#                  links  article_size
# links         1.000000      0.724774
# article_size  0.724774      1.000000



# コミュニティ記事について
dfts[dfts['category'] == "c"].loc[:, ["links", "article_size"]].corr()
#                links  article_size
# links        1.00000       0.63424
# article_size 0.63424       1.00000

# 商品記事について
dfts[dfts['category'] == "i"].loc[:, ["links", "article_size"]].corr()
#                  links  article_size
# links         1.000000      0.254031
# article_size  0.254031      1.000000

# 生放送記事について
dfts[dfts['category'] == "l"].loc[:, ["links", "article_size"]].corr()
#                 links  article_size
# links         1.00000       0.58073
# article_size  0.58073       1.00000

# 動画記事について
dfts[dfts['category'] == "v"].loc[:, ["links", "article_size"]].corr()
#                  links  article_size
# links         1.000000      0.428443
# article_size  0.428443      1.000000
```

# News
Webに公開されている記事を解析するためのCLIを開発しました。

```
lein parse-from-web -u https://dic.nicovideo.jp/a/<contents-title>
```
のようにしてJSON化した記事データを獲得することが出来ます。獲得例は [レポジトリ](https://github.com/MokkeMeguru/niconico-parser) を見て下さい。

但しこれは **相手のサーバに負荷をかける** ので、ちょっとツールを試してみる、などの目的に使って下さい。間違っても大学のIPから絨毯爆撃スクレイピングかますような真似はしないで下さい。