11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Qiita:Team のデータを収集して分析する

Last updated at Posted at 2016-12-31

仕事でQiita:Teamを使っていて、年末だし今年の投稿データを集計しようと思い立ちました。Qiitaには公式なAPIあるし、それをたたいておけばデータをいい感じに集められるのかと思ったのですが、調べてみると正直かなり不十分・不完全でした。

Qiita API v2ドキュメント

APIへの不満

  • 投稿(items)取得のデータで取れないものが多い
    • DBの設計がすけてみえるようなAPIの設計ですが、取得する方としては以下のようなものは一気に取りたいですね
      • 閲覧数
      • コメント数
      • いいね数
  • 1000リクエスト/時間しかたたけない
    • 上記のように一気に取れないデータがあって、ループの中で個別にリクエストしないといけないのにこのリクエスト制限はきついと思いました
    • これ普通のqiitaのほうと一緒のAPIだからそういう制限だと思うんですが、1人あたり700円近く支払っている有料サービスとしては少なくなくないですかね
    • クオータの制限は有料のほうは上げてほしいですね
  • いいねのAPIについてはサービス側の仕様変更についていっておらずちゃんとしたデータを返さない
    • 単純ないいねだけじゃなくて、:eyes::scream::bow: とかいろんなパターンのリアクションができるようになったのですが、それによってデータがちゃんと返ってこなくなった

API + スクレイピング

仕方がないので、APIでとれる部分は取りつつ、スクレイピングも組み合わせてデータを取ります。
コードは実際にやったものからいろいろ省いてますので、動くかわからないです。参考程度です。

APIへの問い合わせ

まずは、teamのほうじゃなくて、qiita.comの設定画面のほうにいってアクセストークンを取得します。

image

サンプルコード

いろいろ省いてますが、pythonでのサンプルコードです。ひとまず、記事一覧はこれでとれます。page=1 が固定で入ってますが、ここをループの中で適宜増やしていけばいいです。


import csv
import requests

DOMAIN = "xxxx.qiita.com"
ACCESS_TOKEN = "xxxxx"

url = 'https://{}/api/v2/items?page=1&per_page=100'.format(DOMAIN)
result = requests.get(url,headers={'Authorization': 'Bearer {}'.format(ACCESS_TOKEN)})
csv_data = []
for d in result.json():
    comments_url = 'https://{}/api/v2/items/{}/comments'.format(DOMAIN, d['id'])
    comments_result = requests.get(comments_url,
                                         headers={'Authorization': 'Bearer {}'.format(ACCESS_TOKEN)})
    csv_data.append(
        [d['id'], d['user']['id'], d['user']['profile_image_url'], d['coediting'], d['title'], d['url'],
         len(d['body']), len(comments_result.json()), d['created_at']])
with open('result_temp.csv', 'w') as f:
    writer = csv.writer(f, lineterminator='\n')
    writer.writerows(csv_data)

スクレイピング

いいね数、コメント数の取得

コメント数はAPIでとれるのはとれるのですが、いいね数は上述の通りAPI経由で正しい値が取れません。なので、記事一覧ページから取得することにします。

ホーム

スクレイピングにはSeleniumとPhantomJSを使いました。ちなみに、通常は2段階認証をかけているので、ログインの突破が難しいです。2段階認証かけてるところへのスクレイピングってやり方あるのでしょうか?仕方ないので、一時的に専用ユーザー作ってそれだけID/Passで通過できるようにしました。


import csv
from selenium import webdriver

DOMAIN = "xxxx.qiita.com"
USER_ID = "xxx"
USER_PASS = "XXX"

csv_data = []
driver = webdriver.PhantomJS()
login_url = 'https://{}/'.format(DOMAIN)
driver.get(login_url)
user_id = driver.find_element_by_id("identity")
user_id.clear()
user_id.send_keys(USER_ID)
password = driver.find_element_by_id("password")
password.clear()
password.send_keys(USER_PASS)
driver.find_element_by_css_selector('input.loginSessionsForm_submit').click()

list_url = 'https://{}/?page={}'.format(DOMAIN, 1)
driver.get(list_url)
for el in driver.find_elements_by_css_selector('div.teamItems_element_body'):
    url = el.find_element_by_css_selector('h1 a').get_attribute("href")
    comments = int(el.find_elements_by_css_selector('div.teamItems_element_count')[0].text)
    likes = int(el.find_elements_by_css_selector('div.teamItems_element_count')[1].text)
    csv_data.append([url, comments, likes])

driver.quit()

with open('result_like.csv', 'w') as f:
    writer = csv.writer(f, lineterminator='\n')
    writer.writerows(csv_data)

閲覧数の取得

記事の閲覧数は個別記事のページにしか情報がありません。先に保存しておいたCSVファイルからURLのリストを取り出して、ひたすら記事を開いて取得していきます。


import csv
from selenium import webdriver
DOMAIN = "xxxx.qiita.com"
USER_ID = "xxx"
USER_PASS = "XXX"

csv_data = []
driver = webdriver.PhantomJS()
login_url = 'https://{}/'.format(DOMAIN)
driver.get(login_url)
user_id = driver.find_element_by_id("identity")
user_id.clear()
user_id.send_keys(USER_ID)
password = driver.find_element_by_id("password")
password.clear()
password.send_keys(USER_PASS)
driver.find_element_by_css_selector('input.loginSessionsForm_submit').click()

# いいね数のデータをCSVから取得しておく
like = {}
with open('result_like.csv', 'r') as f:
    reader = csv.reader(f)
    for row in reader:
        like[row[0]] = row[2]

with open('result_temp.csv', 'r') as f:
    reader = csv.reader(f)
    for row in reader:
        url = row[5]
        driver.get(url)
        for el in driver.find_elements_by_css_selector('ul.dropdown-menu li'):
            if el.get_attribute('class') == 'teamArticle_header_dropdown-view':
                pv = int(el.get_attribute('innerText').replace('views', '').strip())
                csv_data.append([row[0], row[1], row[2], row[3], row[4], url, row[6], row[7], like[url], pv, row[8]])

with open('result_pv.csv', 'w') as f:
    writer = csv.writer(f, lineterminator='\n')
    writer.writerows(csv_data)

これで、最終的にいいね数、閲覧数が含まれたCSVデータができました。

集計をかける

集計には、pandas+jupyterを使いました。pandas初めて使ったのですが、CSVの集計する上ですごい便利ですね。今までは、エクセルかもしくは何かSQLが使えるものに入れて集計やってましたが、次回以降は断然これでいいと思いました。

ちなみにガチでやる人たちは、ローカルもしくはどこかのサーバーにjupyterのサーバー立ててというのが一般的だと思いますが、ゆるふわにやる場合は、Macのスタンドアロンアプリとして動くバージョンのPineappleもおすすめです。ちょっと中に入っているライブラリ群が古いのが気になるので、PR出してアップデートしたいところ。

pineapple
Pineapple_-_Python_Notebooks_for_Mac_OS_X.png

手元でpythonコードの動きをさっと確認したいときとか、APIの問い合わせを生データで確認したいときとかにもよく使ってます。

CSVの読み込み


import pandas as pd
names = ['id', 'user.id', 'user.image', 'coediting', 'title', 'url', 'body.length', 'comments','likes','views', 'created_at']
table = pd.read_csv('/path/to/result_pv.csv', header=None, names=names)

並び替え

並び替えはエクセルとかでも簡単ですが、こんな感じで、降順ソートができます。

ranking = table.sort(['views'], ascending=False)
ranking

グループ集計

ユーザーごとのレコード件数での集計と並び替え

count_by_user = table.groupby(['user.id','user.image'])['id'].count().reset_index()

count_by_user_sorted = count_by_user.sort(['id'], ascending=False)
count_by_user_sorted

ユーザーごとの閲覧数、いいね数、コメント数、本文文字数の集計

sum_by_user = table.groupby(['user.id'])['body.length','comments','likes','views'].sum().reset_index()

sum_by_user_sorted = sum_by_user.sort(['views'], ascending=False)

sum_by_user_sorted

レコード件数での集計と各合計の集計をマージ。簡単ですね。

merged = pd.merge(count_by_user_sorted, sum_by_user_sorted, how='inner', on='user.id')

merged

markdownとして出力

ここはあんまりいいやり方見つからなかったので、力業でmarkdownのテーブルの行を出力して、別途用意しておいたmarkdownのヘッダ部と手作業で合体させました。pandasの0.17以降だと.to_htmlというのが用意されているのでそれが使えそう。

for i,r in enumerate(ranking.iterrows()):
    print("|{0}|<a href='{4}'><img src='{2}' width=48 /></a>|[{3}]({4})|{5}|{6}|{7}|".format(i+1, r[1][1], r[1][2], r[1][4], r[1][5],r[1][7],r[1][8], r[1][9]))
11
6
0

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
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?