ニコニコ大百科データセットを使う
ニコニコ大百科データセットは IDR で公開されているニコニコ大百科の2008~2014年の記事とその記事へのコメントを集めたデータセットです。
知識抽出をはじめとする自然言語系の研究に向いていますが、Wikipedia のようにおとなしいデータセットではなく、かなり癖のあるデータセットとなっています。
例えばニコニコ大百科にある文は半分近くが主語が欠けている文章となっていますし、必ずしも文体が統一されているわけでもないですし、AAなんかも入っていたりします。
今回はこのデータセットの解析をしてみたいなーと思ってくれる 面白い人 を求めて簡単な前処理のツールとともにデータの中身を紹介していきます。
提供されているニコニコ大百科を前処理する
提供されているデータは ちょっと特殊なCSV で、適切な前処理を施すことで標準のCSVに変形することが出来ます。
また、HTMLはいくつかのタグが煩くて解析の都合上少し面倒になります。
このため
- CSVを前処理する
- ニコニコ大百科の記事(HTML)をJSONに変形する
- ちらっと眺めてみる
をこの記事ではやってみようと思います。
前処理
前処理のために必要な環境として大事なのはメモリではなくディスク容量です。うっかり 50GB しか余剰容量がないと、前処理がエラーで落ちます。
また Python を使うなら CPU やメモリも多いほうが良いです。というか Pandas のパフォーマンスがあんまり…
データ利用の申請
こちらから申請をします。申請をすると、少なくとも数日以内にダウンロードのための 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 を行うためのツールを作りました。
からクローンして下さい。
git clone https://github.com/MokkeMeguru/niconico-parser
CSV を整形する。
を zips/preprocess.sh
へコピーして
sh preprocess.sh
して下さい。このファイルはCSVのエスケープを現代の仕様に修正するために必要な処理です。
(裏話:こいつの処理はかなりテストをしていますが、ひょっとするとバグがあるかもしれません。万が一バグがあったらコメントを下さい。)
記事ヘッダ情報をデータベースに保存する
ニコニコ大百科データセットは、大きく分けて
- ヘッダ(記事ID、タイトル、タイトルの読み、カテゴリなど)
- 記事本文
- 記事へのコメント
になっています。この内 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解析のツール があります。
今回はLispを使って簡単に処理させてみました。詳しいコードは…まあ良いでしょう()…
lein preprocess-corpus -r /path/to/nico-dict/zips
とまあこんな感じで実行して下さい。(Jar実行の場合はこちらから(求バグ報告))
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項目ずつ説明すると
- 記事ID
- JSON化+前処理した記事
- 記事更新日
- ページ内に含まれているリンク (
<a>
タグ)のリスト - タイトル
- タイトルのよみ
- カテゴリ ("a"=単語 "v"=動画 "i"=商品 "l"=生放送 "c"=多分コミュニティ記事(仕様書にないカテゴリです))
今回はJSON化+前処理の効力はあんまり紹介できないですが、例えば <p>ほげ<span/>ほげ<br/>ばー</p>
みたいなのが扱いやすくなっているのと、グラフへ持ち込んだり Snorkel のようなツールを適用しやすくなっていることを挙げることが出来ます。
ちょっと統計してみる
前処理ツール作ったよ!だけではあんまりにもつまらないので、ちょっと統計みたいなことをやってみましょう。
データ処理といえば Python + Pandas らしいので、Python + Pandas を使って調べてみることにします。(とはいえPandasは処理がめっちゃ重いとか遅いとかなので、本格的な解析をするなら別のツールを使って下さい。)
以下 Jupyter Notebook のように進めていきます。
依存関係のインポート
import pandas as pd
import json
from pathlib import Path
from pprint import pprint
グローバル変数の宣言
環境ごとに変えて下さい。
############################
# グローバル変数 (適宜変更します) #
############################
# 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を読み込むための関数の定義
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>
タグ) が記事の種類ごとにどんな風な散らばりを示しているのかを眺めてみることにします。
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化されたリンク情報をパースします。
# 生データの確認
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
簡単に統計を取ってみましょう。
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 記事だけではちょっと物足りないので、データを増やしてみましょう。
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化したデータの文字数を記事サイズとみなしてパッと相関を調べてみます。
dfts.corr()
# links article_size
# links 1.000000 0.713465
# article_size 0.713465 1.000000
とまあ少なくとも強い正の相関はありそうです。
もうちょっと踏み込むとこんな感じになります。
# 単語記事について
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化した記事データを獲得することが出来ます。獲得例は レポジトリ を見て下さい。
但しこれは 相手のサーバに負荷をかける ので、ちょっとツールを試してみる、などの目的に使って下さい。間違っても大学のIPから絨毯爆撃スクレイピングかますような真似はしないで下さい。