Pythonの初心者がPythonのお勉強のため、Qiita APIを叩いてみたメモ。
目標は自分の記事のビュー数といいね数とストック数をまとめて取得すること。Qiitaではこれらをまとめて表示できるページがないので、このようなツールがあれば便利。
参考リンク
同じようなことをやっている人はいるので参考にさせていただいた。
- Qiita APIを利用して記事のViewsとストック数がまとめて取得できるAPIを作ってみた
- Qiita API v2 を使って自身の全投稿をエクスポートする Python スクリプトを書いた
- GASでQiita APIを使ってView・いいね・ストック数の一覧を取得する
完成イメージ
できあがったものはこのようなもの。
$ QIITA_TOKEN=hogehoge
$ qiitacheck --help
usage: qiitacheck.py [-h] [-o {text,csv,json}] [-f FILENAME]
[--sort-by {views,likes,stocks}] [--reverse]
Qiitaのビュー数、いいね数、ストック数を取得します。
optional arguments:
-h, --help show this help message and exit
-o {text,csv,json}, --output {text,csv,json}
出力形式を指定します
-f FILENAME, --filename FILENAME
出力先のファイル名を指定します
--sort-by {views,likes,stocks}
結果を指定のキーでソートします
--reverse ソートを降順にします
環境変数QIITA_TOKENにアクセストークンをセットしてから実行してください。
$ qiitacheck --sort-by likes --reverse
+---------------------------------------------------------------------------------------+-------+-------+--------+----------------------+
| Title | Views | Likes | Stocks | Id |
+---------------------------------------------------------------------------------------+-------+-------+--------+----------------------+
| ArduinoをBluemixに接続するチュートリアルのTips | 1822 | 18 | 18 | 5f9f475a8051ed9f0f52 |
| Windows 7でlocalhost宛のパケットをキャプチャする | 16136 | 16 | 10 | 4e9b0c855c620a53e6fa |
(省略)
| MacからターミナルでAIXに接続する | 959 | 0 | 4 | 050371e525f5aa6f0592 |
| JavaScriptで日付をJDBCタイムスタンプエスケープ形式に変換する | 977 | 0 | 1 | dc9af6532759ed633ad0 |
+---------------------------------------------------------------------------------------+-------+-------+--------+----------------------+
$
アクセストークンは環境変数で渡す。-o csv
でcsv出力、-o json
でjson形式での出力が可能。ファイルとして出力する場合は-f <filename>
を指定する。なお、各記事について2回のAPIを実行しているため、何回も実行しているとQiita APIの利用制限(認証済みだと1000回/h、認証していないと60回/h)にひっかかってエラーになる。
準備
以下リンク先からQiita APIのアクセストークンを発行する。
curlでAPIを確認
curl
でAPIを叩いて確認してみる。
いいね数
いいね数は以下の認証中ユーザーの記事一覧APIで取得できる。このAPIでは記事のリストが返されるので、各記事のlikes_count
フィールドをとる。
QIITA_TOKEN=hogehoge
curl -s -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/authenticated_user/items | jq -r '.[] | .title + ": " + ( .likes_count | tostring )'
$ curl -s -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/authenticated_user/items | jq -r '.[] | .title + ": " + ( .likes_count | tostring )'
hostPathとlocalのPersistentVolumeの違い: 0
LibertyコンテナのJVM引数にPod名を渡す: 0
VagrantでkubeadmでKubernetesを起動する: 0
Certified Kubernetes Application Developer (CKAD) 受験ログ: 2
(省略)
このAPIでは1ページに取得できる記事数が限られていることに注意。パラメータで?page=1&per_page=20
のように指定することができるが、1ページの件数はデフォルトで20件で最大でも100件。HTTPヘッダに次のページのリンクが含まれているので、ページネーションして全てのページを取得する必要がある。
curlでヘッダだけをみるにはcurl -D - -s -o /dev/null
を使うと便利。
curl -D - -s -o /dev/null -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/authenticated_user/items
link
ヘッダに次のページへのリンクが含まれている。最後のページにはrel="next"
というリンクがない。
$ curl -D - -s -o /dev/null -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/authenticated_user/items
HTTP/2 200
date: Fri, 21 Dec 2018 08:41:09 GMT
content-type: application/json; charset=utf-8
(省略)
link: <https://qiita.com/api/v2/authenticated_user/items?page=1>; rel="first", <https://qiita.com/api/v2/authenticated_user/items?page=2>; rel="next", <https://qiita.com/api/v2/authenticated_user/items?page=4>; rel="last"
total-count: 62
(省略)
Pythonでのページネーションの処理は以下のリンク先を参考にした(というかほぼそのままコピペした)。
ビュー数
ビュー数も認証中ユーザーの記事一覧APIの各記事のpage_views_count
フィールドから取得できそうに思えるが、なぜかnull
が返ってきてしまっていてとれない。
curl -s -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/authenticated_user/items | jq -r '.[] | .title + ": " + ( .page_views_count | tostring )'
$ curl -s -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/authenticated_user/items | jq -r '.[] | .title + ": " + ( .page_views_count | tostring )'
hostPathとlocalのPersistentVolumeの違い: null
LibertyコンテナのJVM引数にPod名を渡す: null
VagrantでkubeadmでKubernetesを起動する: null
Certified Kubernetes Application Developer (CKAD) 受験ログ: null
(省略)
なので、個別記事取得APIから取得する必要がある。
個別記事取得APIのpage_views_count
フィールドにはちゃんと値が含まれている。
ITEM_ID=fugafuga
curl -s -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/items/${ITEM_ID} | jq -r '.page_views_count'
$ curl -s -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/items/${ITEM_ID} | jq -r '.page_views_count'
384
$
ストック数
ストック数は認証中ユーザーの記事一覧APIや、個別記事取得APIではとれず、記事をストックしているユーザー一覧APIから取得する必要がある。
このAPIではユーザーのリストが返されるが、jq
でリストの数を数える場合はlength
を使う。
curl -s -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/items/${ITEM_ID}/stockers | jq length
$ curl -s -H "Authorization: Bearer ${QIITA_TOKEN}" https://qiita.com/api/v2/items/${ITEM_ID}/stockers | jq length
4
$
Python
以下のように作成。→GitHub
#!/usr/bin/env python3
import argparse
import csv
import json
import logging
import os
import sys
import prettytable
import requests
formatter = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
logging.basicConfig(level=logging.WARNING, format=formatter)
logger = logging.getLogger(__name__)
def get_next_url(response):
"""次のページがある場合は'rel="next"'としてurlが含まれるので、urlを抽出して返す。
ない場合はNoneを返す。
link: <https://qiita.com/api/v2/authenticated_user/items?page=1>;
rel="first", <https://qiita.com/api/v2/authenticated_user/items?page=2>;
rel="next", <https://qiita.com/api/v2/authenticated_user/items?page=4>;
rel="last"
:param response:
:return: 次のurl
"""
link = response.headers['link']
if link is None:
return None
links = link.split(',')
for link in links:
if 'rel="next"' in link:
return link[link.find('<') + 1:link.find('>')]
return None
def get_items(token):
"""ページネーションして全ての記事を取得し、
ストック数とビュー数は一覧に含まれないので、それらの情報も追加して返す。
:param token:
:return: 記事のリスト
"""
url = 'https://qiita.com/api/v2/authenticated_user/items'
headers = {'Authorization': 'Bearer {}'.format(token)}
items = []
while True:
response = requests.get(url, headers=headers)
response.raise_for_status()
items.extend(json.loads(response.text))
logger.info('GET {}'.format(url))
# 次のurlがあるかを確認する
url = get_next_url(response)
if url is None:
break
# 各記事についてビュー数とストック数の情報を取得して追加する
# page_views_countは一覧APIにもフィールドはあるがnullが返ってくる
for item in items:
# ビュー数
url = 'https://qiita.com/api/v2/items/{}'.format(item['id'])
logger.info('GET {}'.format(url))
response = requests.get(url, headers=headers)
response.raise_for_status()
item['page_views_count'] = json.loads(response.text)['page_views_count']
# ストック数
url = 'https://qiita.com/api/v2/items/{}/stockers'.format(item['id'])
logger.info('GET {}'.format(url))
response = requests.get(url, headers=headers)
response.raise_for_status()
users = json.loads(response.text)
for user in users:
logger.info({
'id': user['id'],
'name': user['name']
})
item['stocks_count'] = len(users)
return items
def sort_items(items, sort_by, reverse):
"""リストをソートする
:param items:
:param sort_by:
:param reverse:
:return:
"""
if sort_by == 'views':
if reverse:
items.sort(key=lambda x: -x['page_views_count'])
else:
items.sort(key=lambda x: x['page_views_count'])
elif sort_by == 'likes':
if reverse:
items.sort(key=lambda x: -x['likes_count'])
else:
items.sort(key=lambda x: x['likes_count'])
elif sort_by == 'stocks':
if reverse:
items.sort(key=lambda x: -x['stocks_count'])
else:
items.sort(key=lambda x: x['stocks_count'])
def output_text(items, filepath):
"""テキストで整形して標準出力に出力する。
ファイル名が指定された場合はファイルに出力する。
:param items:
:param filepath:
:return:
"""
table = prettytable.PrettyTable()
table.field_names = ['Title', 'Views', 'Likes', 'Stocks', 'Id']
table.align['Title'] = 'l'
table.align['Views'] = 'r'
table.align['Likes'] = 'r'
table.align['Stocks'] = 'r'
table.align['Id'] = 'l'
for item in items:
table.add_row([item['title'],
item['page_views_count'],
item['likes_count'],
item['stocks_count'],
item['id']])
if filepath:
with open(filepath, 'w') as text_file:
text_file.write(table.get_string())
else:
print(table)
def output_csv(items, filepath):
"""csvに整形して標準出力に出力する。
ファイル名が指定された場合はファイルに出力する。
:param items:
:param filepath:
:return:
"""
def write_rows(writer, items):
for item in items:
writer.writerow({
'Title': item['title'],
'Views': item['page_views_count'],
'Likes': item['likes_count'],
'Stocks': item['stocks_count'],
'Id': item['id']
})
fieldnames = ['Title', 'Views', 'Likes', 'Stocks', 'Id']
if filepath:
with open(filepath, 'w') as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
write_rows(writer, items)
else:
writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames)
writer.writeheader()
write_rows(writer, items)
def output_json(items, filepath):
"""jsonに整形して標準出力に出力する。
ファイル名が指定された場合はファイルに出力する。
:param items:
:param filepath:
:return:
"""
my_list = []
for item in items:
my_list.append({
'Title': item['title'],
'Views': item['page_views_count'],
'Likes': item['likes_count'],
'Stocks': item['stocks_count'],
'Id': item['id']
})
if filepath:
with open(filepath, 'w') as json_file:
json.dump(my_list, json_file, ensure_ascii=False, indent=4)
else:
print(json.dumps(my_list, ensure_ascii=False, indent=4))
def main():
# コマンド引数の処理
parser = argparse.ArgumentParser(description='Qiitaのビュー数、いいね数、ストック数を取得します。',
epilog='環境変数QIITA_TOKENにアクセストークンをセットしてから実行してください。')
parser.add_argument('-o', '--output',
default='text',
action='store',
type=str,
choices=['text', 'csv', 'json'],
help='出力形式を指定します')
parser.add_argument('-f', '--filename',
action='store',
type=str,
help='出力先のファイル名を指定します')
parser.add_argument('--sort-by',
action='store',
type=str,
choices=['views', 'likes', 'stocks'],
help='結果を指定のキーでソートします')
parser.add_argument('--reverse',
action='store_true',
help='ソートを降順にします')
args = parser.parse_args()
# APIからデータを取得
token = os.environ['QIITA_TOKEN']
items = get_items(token)
# items = [
# {'title': 'aaa',
# 'page_views_count': 11,
# 'likes_count': 22,
# 'stocks_count': 33,
# 'id': 'hogehoge'},
# {'title': 'bbb',
# 'page_views_count': 44,
# 'likes_count': 55,
# 'stocks_count': 66,
# 'id': 'fugafuga'}
# ]
# リストをソートする
sort_items(items, args.sort_by, args.reverse)
# ファイル出力先のパスを決める
if args.filename:
# dockerで実行している場合はファイルの出力先を/tmpにする
try:
os.environ['IS_DOCKER']
# フルパスが与えられた場合はファイル名だけにする
filename = os.path.basename(args.filename)
filepath = os.path.join('/tmp', filename)
except KeyError:
filepath = args.filename
else:
filepath = None
# 結果を出力する
if args.output == 'csv':
output_csv(items, filepath)
elif args.output == 'json':
output_json(items, filepath)
else:
output_text(items, filepath)
if __name__ == '__main__':
main()
- テキスト出力は
prettytable
モジュールで整形 - コマンド引数は
argparse
モジュールで処理 - 記事数が20件以上の場合もページネーションして全ての記事を取得
- 結果はソート可能
実行にはrequests
とprettytable
モジュールが必要。
pip3 install requests prettytable
export QIITA_TOKEN=hogehoge
./qiitacheck.py --help
Dockerで動かす
pipモジュールのインストールなどを気にせず、どこでも使いやすいようにDockerイメージにしてCLIっぽく使ってみる。
イメージ作成
DockerHubのpythonイメージの説明を参考にしてrequirements.txt
とDockerfile
を作成する。
requests
prettytable
FROM python:3-alpine
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY qiitacheck.py ./
ENV IS_DOCKER TRUE
ENTRYPOINT [ "python", "./qiitacheck.py" ]
- CLIっぽく使いたいので、pythonの実行コマンドは
CMD
ではなくENTORYPOINT
を指定している - Dockerで実行する場合はファイル出力先のパスをマウント先のディレクトリに変更する必要があるので、Dockerで実行されているかを判定するための
IS_DOCKER
環境変数を設定している
イメージをビルドする。
docker build -t sotoiwa540/qiita-checker:1.0 .
docker push sotoiwa540/qiita-checker:1.0
実行
エイリアスを作成する。
alias qiitacheck='docker run --rm -it -e QIITA_TOKEN=${QIITA_TOKEN} -v ${PWD}:/tmp sotoiwa540/qiita-checker:1.0'
- トークンの環境変数をコンテナの環境変数にも設定している
- ファイル出力する場合のために、カレントディレクトリをマウントしている
トークンを環境変数としてエクスポートする。
export QIITA_TOKEN=hogehoge
実行する。csv出力の場合の例。
$ qiitacheck -o csv -f test.csv --sort-by likes --reverse
$ cat test.csv
Title,Views,Likes,Stocks,Id
ArduinoをBluemixに接続するチュートリアルのTips,1822,18,18,5f9f475a8051ed9f0f52
Windows 7でlocalhost宛のパケットをキャプチャする,16136,16,10,4e9b0c855c620a53e6fa
SwiftでHTTPリクエストを再帰的に複数並列で行い、全てのリクエストが完了してから次の処理,2445,14,17,8dc15a224d2cf6c6be91
Certified Kubernetes Administrator (CKA) 受験ログ,386,10,4,3509ca3d18d1bed00dee
(省略)