はじめに
本記事では,QiitaAPIを用いてQiitaに投稿された記事データを取得して,
Qiitaの記事データに現れたタグの数を集計し,記事データのタグ情報の共起関係を見てみたいと思います.
それによって,自分が知らない領域について知見を得るきっかけになればいいかなと思っています.
共起関係とは?
共起関係とは
ある単語がある文章(または文)中に出たとき、その文章(文)中に別の限られた単語が頻繁に出現すること。
(wikipediaより)
です.
今回のケースで言うと,ある記事データに設定されたタグ情報同士が,ほかの記事データでも頻繁に出現するかどうかです.
タグ情報同士がある記事データに設定されたことを(タグ情報に対して)共起関係といいます.
この共起関係が多ければ多いほどタグ情報同士の結びつきが強く,逆に少なければ結びつきは弱くなります.
詳細はこちらの記事をご覧ください.
環境設定
今回このプログラムを動かす環境などは以下の通りです.
- Ubuntu on GCP
- python 3.7.1 on pipenv
- Postgresql
GCPの環境設定
まず,今回はGCPのVMインスタンス上でプログラムを書いているのですが,
GCPの設定についてはこちらの記事が分かりやすいと思うので,参考にしてください.
pipenvの設定
pythonを実行する環境としてpipenv
の設定を行います.
pipenv
とはPython公式が正式に推薦するPythonパッケージングツールでpythonのバージョン管理を行うvenv
と,
パッケージ管理を行うpip
が連動し,ユーザにとってパッケージ管理を行いやすいツールです.
これを利用することで,各フォルダごとに固有のpython環境
を作ることができ,
パッケージの管理もフォルダごとに行えるようになります.
以下のpipenv
の導入手順は,このサイトに則っています
まずは,開いた環境のパッケージなどを最新化します.
$ sudo apt update
$ sudo apt upgrade -y
次に,pyenvの依存ライブラリをインストールします.
$ sudo apt install -y --no-install-recommends \
build-essential \
curl \
libbz2-dev \
libffi-dev \
liblzma-dev \
libncurses5-dev \
libncursesw5-dev \
libreadline-dev \
libsqlite3-dev \
libssl-dev \
llvm \
tk-dev \
wget \
xz-utils \
zlib1g-dev \
ca-certificates \
git \
python-dev
そうしたら,pyenv
をGitHub
よりclone
します.
$ git clone https://github.com/pyenv/pyenv.git ./pyenv
つぎに~/.bashrc
ファイルに以下の内容を追記します.
[your_name]の箇所は適宜変更しましょう.
# ~~前略~~
export PYENV_ROOT="/home/[your_name]/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
clone
したpyenv
をインストールします.
$ source ~/.bashrc
$ pyenv -v
pyenv 1.2.8-12-g775a4b63
そうしたら,pyenv
を使ってpython 3.7.1
をインストールします.
$ pyenv install 3.7.1
$ pyenv global 3.7.1
pyenv
を使ってpythonをインストールしたら,ようやくpipenv
をインストールします.
~ $ pip install pipenv
~ $ echo 'eval "$(pipenv --completion)"' >> ~/.bashrc
~ $ pipenv --version
> pipenv, version 2018.11.26
インストールしたら,これから作業を行うフォルダを作成し,適当なtest.py
を用意しきちんと動くか試します.
中身はprint('hello, world!')
とかで十分です.
~ $ mkdir qiita
~/qiita $ pipenv install
~/qiita $ pipenv run python test.py
> hello, world!
動いたら,一旦pipenv
の設定は終わりです.
データベースの設定
データをためておく場所として今回はデータベースのPostgresql
を利用します.
csvなどで保持してもいいのですが,保存する記事データの数と今後の利用を考えると,
データベースを利用した方がいいのでそうします.
(実際は,RDBではなくて,GCP上のNoSQLを使った方がいいのだけれど,それは今後の課題...)
まず,インストールと初期設定をします.
最初にpostgres
というユーザに対してパスワードを設定し,
qiita
という名前のデータベースを作成します.
~/$ sudo apt install postgresql postgresql-contrib -y
~/$ sudo -u postgres psql postgres
postgres=# \password
Enter new password:
Enter it again:
postgres=# create database qiita;
postgres=# \q
次に,今回は6種類のテーブルを作成します.
これらのテーブルを作成するにあたっては,この記事とこの記事を参考にしました.
(使わないテーブルも載せているのは,今後利用するからです.)
テーブル名 | 用途 |
---|---|
user_list | ユーザのpermanent_idとidを管理するテーブル |
user_info | ユーザのgithubとかtwitterとかの情報を管理するテーブル.(今回は使わない) |
user_list_used2search | ユーザのfollowerとかfolloweeの情報から新たなユーザを取得するためのテーブル.(今回は使わない) |
item_list | 記事データを管理するテーブル . |
tag_appear_count | タグの出現回数を記録するテーブル. |
value_list | 記事データを取得する際に利用する(記事の取得期間を保持するための)補助テーブル . |
これらの6つのテーブルを作成していきます.
コマンドは以下の通り.
(primary key
も一度に設定できますが,一回一回alter table
してます.)
~/$ psql -h localhost -U postgres -d qiita -W
qiita=# create table user_list (permanent_id integer, id text, followers_count integer, followees_count integer);
qiita=# alter table user_list add primary key(permanent_id);
qiita=# create table user_info (permanent_id integer, description text, facebook_id text, github_login_id text, items_coun
t integer, linkedin_id text, twitter_screen_name text);
qiita=# alter table user_info add primary key(permanent_id);
qiita=# create table user_list_used2search (permanent_id integer, id text, search_count integer, prcs_status integer);
qiita=# alter table user_list_used2search add primary key(permanent_id);
qiita=# create table item_list (item_id text, permanent_id integer, title text, body text, created_at text, updated_at text, likes_count integer, tags_str text);
qiita=# alter table item_list add primary key(item_id, permanent_id);
qiita=# create table value_list (key text, value text);
qiita=# alter table value_list add primary key(key);
qiita=# create table tag_appear_count (tag_name text, calc_date text, period text, count integer);
qiita=# alter table tag_appear_count add primary key(tag_name, calc_date);
qiita=# \d
List of relations
Schema | Name | Type | Owner
--------+-----------------------+-------+----------
public | item_list | table | postgres
public | tag_appear_count | table | postgres
public | user_info | table | postgres
public | user_list | table | postgres
public | user_list_used2search | table | postgres
public | value_list | table | postgres
(6 rows)
qiita=# \q
上記のように表示されたらとりあえずテーブルの設定は完了です.
Qiitaの記事データ取得
さて,ここからpython
を使って実際に記事データを取得していきます.
その前に,データ取得に必要なライブラリをインストールします.
~/qiita $ pipenv install requests pandas psycopg2
補助関数群の定義
まずは,記事のデータを本格的に取得する前にQiitaAPIから取得できるjsonをデータベースに入れる補助関数群funcs.py
を定義していきます.
最初に,funcs.py
のimportは以下の通りです.
import http.client
import datetime
import urllib.request
import json
import psycopg2
import http.client
import datetime
import urllib.request
import json
import requests
import time
import sys
このfuncs.py
を定義して,
別のファイルから簡単に呼ぶことができます.
import sys
sys.path.append("/home/[your_name]/qiita")
import funcs
# 以下のようにして呼べます.
funcs.[function_name()]
まずは,ユーザの情報を取得する関数を定義します.
先ほどのimport群の下に定義していきます.
この関数は,QiitaAPIで取得したユーザをデータベースに登録する関数です.
それぞれの引数psgr
,cur
は,postgresqlへの接続情報,json
,jsonstr
はユーザの情報のjsonです.
# import_s
def insert_single_user(psgr, cur, jsonstr):
permanent_id = jsonstr['permanent_id']
id = jsonstr['id']
followers_count = jsonstr['followers_count']
followees_count = jsonstr['followees_count']
description = "null" if jsonstr['description'] == None else jsonstr['description']
facebook_id = "null" if jsonstr['facebook_id'] == None else jsonstr['facebook_id']
github_login_id = "null" if jsonstr['github_login_name'] == None else jsonstr['github_login_name']
items_count = jsonstr['items_count']
linkedin_id = "null" if jsonstr['linkedin_id'] == None else jsonstr['linkedin_id']
twitter_screen_name = "null" if jsonstr['twitter_screen_name'] == None else jsonstr['twitter_screen_name']
try:
cur.execute("insert into user_info (permanent_id, description, facebook_id, github_login_id, items_count, linkedin_id, twitter_screen_name) values (%s, %s, %s, %s, %s, %s, %s)", (permanent_id, description, facebook_id, github_login_id, items_count, linkedin_id, twitter_screen_name))
cur.execute("insert into user_list (permanent_id, id, followers_count, followees_count) values (%s, %s, %s, %s)", (permanent_id, id, followers_count, followees_count))
psgr.commit()
except Exception as e:
psgr.commit()
def insert_user(psgr, cur, json):
for jsonstr in json:
insert_single_user(jsonstr)
次に記事データをDBに挿入する関数を定義します.
それぞれの引数は,inser_user()
と同じです.
Qiitaでは1つの記事に対してタグを5つ設定できるのですが,データベースに入れるときにはそれを1つの文字列として結合し,挿入しています.
# import_s
# insert_user()_s
def insert_item(psgr, cur, json):
for jsonstr in json:
permanent_id = jsonstr['user']['permanent_id']
item_id = jsonstr['id']
title = jsonstr['title']
body = jsonstr['rendered_body']
created_at = jsonstr['created_at']
updated_at = jsonstr['updated_at']
likes_count = jsonstr['likes_count']
#concatenate
tag_list = []
for t in jsonstr['tags']:
tag_list.append(t['name'])
tags_str = ",".join(tag_list)
try:
cur.execute("insert into item_list (item_id, permanent_id, title, body, created_at, updated_at, likes_count, tags_str) values (%s, %s, %s, %s, %s, %s, %s, %s)", (item_id, permanent_id, title, body, created_at, updated_at, likes_count, tags_str))
psgr.commit()
except Exception as e:
psgr.commit()
insert_single_user(psgr, cur, jsonstr['user'])
最後に,データベースとはあまり関係ないですが,
ソースコードを簡単にするための補助関数を定義します.
my_header()
はQiitaAPIを利用するときに1時間あたりに1000回APIを叩けるようにするための情報を取得する関数です.
get_connection()
は,Postgresqlの情報をDataFrameに読み込む際に必要な情報をまとめた関数です.
後々に複数回呼ぶことになるので,ここにまとめておきます.
[your_code]と書かれた部分の取得方法についてはこの記事を参考に取得してください.
# import_s
# insert_user()_s
# isert_item()
def my_header():
"""
# get qiita header with specific header
"""
h = {
"Authorization": "Bearer [your_code]",
"content-type" : "application/json"
}
return h
def get_connection():
# connection info
connection_config = {
'host': 'localhost',
'database': 'qiita',
'user': 'postgres',
'password': '***'
}
return psycopg2.connect(**connection_config)
さて,これで記事データを取得する準備はできました.
記事データの取得
それでは,次に記事データの取得を行っていきます.
まずは,importと事前準備の部分です.
QiitaAPIでは普通に取得しようとすると,今日の日付から10000件過去のデータしか取得することができません.
そこで,取得する期間を絞って取得していきます.
変数のstart
とend
に関しては,後々にデータベースから値を取得して都度変更していきます.
import sys
sys.path.append("/home/[your_name]/qiita")
import funcs
import psycopg2
import time
import os
import requests
import sys
import math
import datetime
url = 'https://qiita.com/api/v2/items'
start = "2018-10-20"
end = "2018-11-1"
# in order not to over the limitation accessing 1000 times in one hour
sleep_sec = 3.6
さて,以下が指定された期間の記事データを取得するコードです.
最初にwith
句でデータベースの接続を行って,記事データの取得とデータベースへの挿入を行っています.
1回実行するたびに,データベースvalue_list
に保存された前回実行日を取得し,
取得した日から5日間の間に投稿された記事データをデータベースに登録します.
QiitaAPIでは,1回のrequestで取得できる記事の件数が100件しかないため,
responseのヘッダのTotal-Count
より記事の総数を取得し,
すべての記事データを取得できるようにループを回します.
なお,取得した記事データには,ユーザの情報も付属しているため,
ユーザの情報も同時にデータベースへ登録を行います(登録は,funcs.insert_item()
内でしています).
最後に,取得した日をデータベースに登録するまでが一連のプログラムの流れです.
# import_s
with psycopg2.connect("host=localhost dbname=qiita user=postgres password=***") as psgr:
with psgr.cursor() as cur:
# get start day
cur.execute("select * from value_list where key = 'get_item_start'")
row = cur.fetchall()
for r in row:
end = r[1]
new_end_day = datetime.datetime.strptime(end, "%Y-%m-%d")
new_start_day = new_end_day - datetime.timedelta(days=5)
start = str(new_start_day.year) + "-" + str(new_start_day.month) + "-" + str(new_start_day.day)
end = str(new_end_day.year) + "-" + str(new_end_day.month) + "-" + str(new_end_day.day)
p = {
'per_page': 100,
'page':1,
'query': 'created:>{} created:<{}'.format(start, end)
}
r = requests.get(url, params=p, headers=funcs.my_header())
# item count in the whole day
total_count = int(r.headers['Total-Count'])
# to get all items
loop_count = math.ceil((total_count - 100)/100)
funcs.insert_item(psgr, cur, r.json())
for i in range(loop_count):
# for access limitation
time.sleep(sleep_sec)
print("loop : "+str(i+2))
p['page'] = i + 2
r = requests.get(url, params=p, headers=funcs.my_header())
print("remaining : " + str(r.headers['Rate-Remaining']))
funcs.insert_item(psgr, cur, r.json())
cur.execute("update value_list set value = %s where key = %s ", (start, 'get_item_start'))
cur.execute("update value_list set value = %s where key = %s ", (end, 'get_item_end'))
psgr.commit()
ここまで,来たらcron
で定期実行できるようにします.
ただ,pipenv
環境のため,shellscript
を書いて簡単に実行できるようにします.
cd /home/[your_name]/qiita
/home/[your_name]/pyenv/shims/pipenv run python get_new_item_backwards.py
shellscript
を書いたら,cron
に登録します.
今回は10分間隔での実行にしましたが,もう少し早くデータを取得し終えたい場合は5分間隔でもいいかなと思います.
ちなみに,10分間隔だと,1日でおよそ2年分の記事データを取得でき,
Qiitaのサービスが開始した2011年からの記事データをおよそ4日で取得できます.
*/10 * * * * sh /home/[your_name]/qiita/sh_get_new_item_backwards.sh
タグの共起関係の取得
ある程度,データがたまったら,タグの出現回数の計算とタグの共起関係を取得していきます.
まずは,タグの出現回数のプログラムから.
このプログラムは,取得した記事データの全期間のタグ情報を取得し,出現回数をカウントしています.
(実際の可視化の際には,期間を指定した方が面白くなりそうではあるので,今後実装していきたいです.)
内容としては,データベースに登録されたタグ情報はカンマ(,
)区切りで保存したので,
それをsplit
して1つ1つの要素にした後,
pandas
のvalue_counnts()
関数を呼ぶだけで,同じ文字列の出現回数を計算しています.(pandas
が優秀すぎる)
あとは,1行ごとにデータベースへ登録を行っているだけです.
import sys
sys.path.append("/home/[your_name]/qiita")
import funcs
import pandas as pd
import psycopg2
import sys
import datetime
# datetime
today = datetime.datetime.now()
date = str(today.year) + "-" + str(today.month) + "-" + str(today.day)
# https://stackoverflow.com/questions/37224002/split-pandas-series-into-dataframe-by-delimiter
item_df = pd.read_sql(sql="select permanent_id,tags_str from item_list", con=funcs.get_connection())
tag_df = item_df['tags_str'].str.split(',')
tmp = []
for tag in tag_df:
tmp.append([e.strip() for e in tag])
tag_df = pd.DataFrame(tmp)
tag_all = pd.concat([tag_df[0],tag_df[1],tag_df[2],tag_df[3],tag_df[4]]).dropna()
tag_df = tag_all.value_counts()
with psycopg2.connect("host=localhost dbname=qiita user=postgres password=***") as psgr:
with psgr.cursor() as cur:
for i,t in tag_df.iteritems():
cur.execute("insert into tag_appear_count (tag_name, calc_date, period, count) values (%s, %s, %s, %s)", (i, date, 'whole', str(t)))
さて,最後のプログラムはタグ同士の共起関係を取得,計算するプログラムです.
その前に,import群と補助関数を定義します.
このget_related_tags_with_search_word()
は,
記事データのうちタグに引数search
を含む記事のタグを取得する関数です.
返り値は,search
ワードと同じ記事中に書かれたタグと,そのタグの出現回数を返します.
今回は,出現回数が200回以上のタグのみに絞りました.
また,search
を含むタグはヒットさせたくないので,正規表現で検索結果から除外するようにします.
import sys
sys.path.append("/home/[your_name]/qiita")
import funcs
import pandas as pd
import psycopg2
import sys
import datetime
import re
def get_related_tags_with_search_word(search='python'):
# compile
try:
pattern = r'%s' % (search)
repattern = re.compile(pattern)
except Exception as e:
return pd.DataFrame()
# get DataFrame
item_df = pd.read_sql(sql="select tags_str from item_list where tags_str like %s", params=['%{}%'.format(search)],con=funcs.get_connection())
# split tags_str
tag_df = item_df['tags_str'].str.split(',')
tmp = []
for tag in tag_df:
tmp.append([e.strip() for e in tag])
tag_df = pd.DataFrame(tmp)
tag_all = pd.concat([tag_df[0],tag_df[1],tag_df[2],tag_df[3],tag_df[4]]).dropna()
# get tags_str and count
df = pd.DataFrame()
for i,t in tag_all.value_counts().iteritems():
if not repattern.match(i) and t > 200:
df = pd.concat([df, pd.DataFrame({'count':t}, index=[i])])
if df.empty:
return pd.DataFrame(columns=[search])
df.columns=[search]
return df
さて,補助関数を定義したので,共起関係を取得する部分を書いていきます.
ただ,補助関数のおかげでsearch
に共起関係を探したいキーワードを入れるだけです.
# import_s
# get_related_tags_with_search_word()
p_df = get_related_tags_with_search_word(search='Python')
print(p_df)
以下がsearch=Python
とした実行結果です.
やはりPythonでは,DeepLearning
や機械学習
と言ったキーワードと共起関係にあるようです.
Python
機械学習 1617
DeepLearning 999
Django 980
TensorFlow 738
pandas 724
numpy 608
matplotlib 570
MachineLearning 540
Jupyter 528
OpenCV 503
AWS 493
Keras 483
RaspberryPi 420
Anaconda 400
Flask 363
scikit-learn 360
初心者 343
自然言語処理 335
docker 323
Chainer 315
Mac 298
python2.7 290
lambda 286
Ruby 254
数学 249
pyenv 247
pip 238
Twitter 231
データ分析 225
Windows 225
Selenium 222
JavaScript 219
スクレイピング 210
画像処理 205
以下がsearch=JavaScript
とした実行結果です.
Node.js
やvue.js
といったフレームワークと共起している関係が見て取れます.
また,私はgulp
というキーワードをこの共起関係を使って知ることができました.
JavaScript
Node.js 1648
jQuery 1340
HTML 1068
vue.js 826
HTML5 818
CSS 716
React 694
reactjs 615
es6 526
TypeScript 507
webpack 399
AngularJS 390
PHP 338
Chrome 290
Ruby 279
babel 270
npm 246
redux 240
CoffeeScript 234
Rails 228
GoogleAppsScript 217
es2015 207
Electron 204
gulp 204
おわりに
今回は記事データの取得と,タグの共起関係を計算するだけに終わってしまいましたが,
本番はこれを可視化するところなので,今後はD3.jsを使っていい感じに可視化したいと思っています.
Qiitaの記事データを眺めるだけでも面白いので,みなさんもぜひやってみてください~