はじめに
Qiita APIを利用した投稿記事取得の際、取得結果が複数ページになる場合にページネーションを行う方法のメモです。
Qiita APIでのページネーションに関する仕様
Qiita APIのドキュメントには、ページネーションに関連して、以下の2つの記載があります。
- ページを指定できるAPIでは、Linkヘッダ を含んだレスポンスを返す
- ページを指定できるAPIでは、要素の合計数が Total-Count レスポンスヘッダに含まれる
Curlコマンドで確認
上記を実際に、curlコマンドで確認してみます。
$ curl -D - -s -o /dev/null -H 'Authorization: Bearer [アクセストークン]' 'https://qiita.com/api/v2/items' | grep -e 'link' -e 'total-count'
link: <https://qiita.com/api/v2/items?page=1>; rel="first", <https://qiita.com/api/v2/items?page=2>; rel="next", <https://qiita.com/api/v2/items?page=34025>; rel="last"
total-count: 680484
実装
APIのレスポンス仕様を参考にして、以下の2種類の方法でページネーションを実装します。
- Total-Countヘッダを利用した実装
- Linkヘッダを利用した実装
(その1)Total-Countヘッダを利用した実装
def pagenation_by_total_count(token = None, query = None):
df_ret = pd.DataFrame()
# クエリパラメータの準備
params = prepare_parameter(query)
# アクセストークンが指定された場合に付与
req_headers = prepare_headers(token)
for page_num in range(1, 101):
params['page'] = str(page_num)
url = "https://qiita.com/api/v2/items?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers=req_headers)
with urllib.request.urlopen(req) as res:
body = json.load(res)
# DataFrameに記事情報を格納
df = pd.json_normalize(body)
df_ret = pd.concat([df_ret, df])
print("Page: " + str(page_num))
# Total-Countヘッダの値から最後のページまで取得したかを判断
total_count = int(res.headers['Total-Count'])
if page_num >= (total_count + 99) // 100:
print('# of articles: ', total_count)
break
return df_ret
-
prepare_parameter(query)
とprepare_headers(token)
は、それぞれ引数に与えられた検索クエリを含めたクエリパラメータの用意と、アクセストークンが与えられた場合のヘッダの用意をしています。中身は、コード全体の節を参照してください。 - PandasのDataFrameに取得した記事情報を格納します。
- Total-Countヘッダから記事数の合計を取得し、1ページ100件として計算したページ数と比較し、最後のページまで取得したかを判断します。
(その2)Linkヘッダを利用した実装
def pagenation_by_link_header(token = None, query = None):
df_ret = pd.DataFrame()
# クエリパラメータの準備
params = prepare_parameter(query)
# アクセストークンが指定された場合に付与
req_headers = prepare_headers(token)
# 初期化
page_num = 0
next_url = "https://qiita.com/api/v2/items?" + urllib.parse.urlencode(params)
while page_num < 100 and next_url is not None:
page_num += 1
req = urllib.request.Request(next_url, headers=req_headers)
with urllib.request.urlopen(req) as res:
body = json.load(res)
# DataFrameに記事情報を格納
df = pd.json_normalize(body)
df_ret = pd.concat([df_ret, df])
print("Page: " + str(page_num))
# Linkヘッダに次ページのURLがある場合は「next_url」にセットする
next_url = None
link = res.headers['link']
if link is not None:
m = re.match(r'.*<(https://qiita.com/api/v2/.*?)>;\s*rel="next"', link)
if m is not None:
next_url = m.group(1)
return df_ret
- その1と同様に、PandasのDataFrameに取得した記事情報を格納します。
- 正規表現を用いて、Linkヘッダに次ページのURLがある場合は取得します。
-
next_url
という変数に次に取得するページがある場合は、そのURLを格納します。
コード全体
最後にコード全体を掲載します。
pagenation.py
import urllib.request
import json
import re
import csv
import pandas as pd
ACCESS_TOKEN = "YOUR ACCESS TOKEN"
# クエリパラメータを組み立てる(1ページ100件固定)
def prepare_parameter(query):
params = {
'per_page': '100'
}
if (query is not None):
params['query'] = query
return params
# 認証トークンが指定された場合にヘッダに付与する
def prepare_headers(token):
req_headers = {}
if (token is not None):
req_headers = {
'Authorization': 'Bearer ' + token
}
return req_headers
def pagenation_by_total_count(token = None, query = None):
df_ret = pd.DataFrame()
# クエリパラメータの準備
params = prepare_parameter(query)
# アクセストークンが指定された場合に付与
req_headers = prepare_headers(token)
for page_num in range(1, 101):
params['page'] = str(page_num)
url = "https://qiita.com/api/v2/items?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers=req_headers)
with urllib.request.urlopen(req) as res:
body = json.load(res)
# DataFrameに記事情報を格納
df = pd.json_normalize(body)
df_ret = pd.concat([df_ret, df])
print("Page: " + str(page_num))
# Total-Countヘッダの値から最後のページまで取得したかを判断
total_count = int(res.headers['Total-Count'])
if page_num >= (total_count + 99) // 100:
print('# of articles: ', total_count)
break
return df_ret
def pagenation_by_link_header(token = None, query = None):
df_ret = pd.DataFrame()
# クエリパラメータの準備
params = prepare_parameter(query)
# アクセストークンが指定された場合に付与
req_headers = prepare_headers(token)
# 初期化
page_num = 0
next_url = "https://qiita.com/api/v2/items?" + urllib.parse.urlencode(params)
while page_num < 100 and next_url is not None:
page_num += 1
req = urllib.request.Request(next_url, headers=req_headers)
with urllib.request.urlopen(req) as res:
body = json.load(res)
# DataFrameに記事情報を格納
df = pd.json_normalize(body)
df_ret = pd.concat([df_ret, df])
print("Page: " + str(page_num))
# Linkヘッダに次ページのURLがある場合は「next_url」にセットする
next_url = None
link = res.headers['link']
if link is not None:
m = re.match(r'.*<(https://qiita.com/api/v2/.*?)>;\s*rel="next"', link)
if m is not None:
next_url = m.group(1)
return df_ret
def main():
# Total-Countヘッダを利用して、「QiitaAPI」タグの記事を取得
df_total_count = pagenation_by_total_count(token = ACCESS_TOKEN, query = 'tag:QiitaAPI')
df_total_count.to_csv('pagenation_by_total_count.csv', index=False, columns=['title', 'likes_count', 'created_at', 'url'])
# Linkヘッダを利用して、「Python」タグの2020年に作成された記事を取得
df_link = pagenation_by_link_header(token = ACCESS_TOKEN, query = 'tag:Python created:2020')
df_link.to_csv('pagenation_by_link.csv', index=False, columns=['title', 'likes_count', 'created_at', 'url'])
if __name__ == "__main__":
main()
- 2種類の実装で、それぞれ記事を取得し、記事の「タイトル」、「LGTMの数」、「作成日」、「URL」をCSVに書き出します。
- Total-Countヘッダを利用して、「QiitaAPI」タグの記事を取得
- Linkヘッダを利用して、「Python」タグの2020年に作成された記事を取得