Python
QiitaAPI
python2.7
REST-API
Requests

Qiita API v2 を使って自身の全投稿をエクスポートする Python スクリプトを書いた

Qiita API v2 の練習も兼ねて、自身が投稿した記事全てをエクスポートするスクリプトを書いてみました。勉強がてらまとめてみます。

今回学んだこと

  • Qiita API v2 でデータを GET する
  • Qiita API v2 でアクセストークンを使用する
  • GET したデータをファイルに保存する
  • requests ライブラリの基本的な使い方

Qiita API v2 について

詳しくは Qiita API v2ドキュメント - Qiita:Developer を見ていただくとして、ここでは要点をメモしておきます。

所感としては GitHub API v3 とよく似た、シンプルな使い心地だと思いました。

全般

  • Rate Limit は非認証なら60回/h、認証したら1000回/h
  • Pagination の per_page は初期値が20、上限は100
  • 認証についてはログイン後の画面でアクセストークンを発行し、リクエストヘッダの Authorization フィールドに含めるのがお手軽

アクセストークンの作り方

Qiita にログイン後、設定画面から発行できます。これをリクエストヘッダに含めてやるだけで認証済ユーザとして API を叩けます。

トークンの権限は何種類か選べますが、GET しかしないなら read_qiita で十分でしょう。

レスポンスヘッダのサンプル

下記は https://qiita.com/api/v2/authenticated_user/items (認証済ユーザの投稿一覧) を GET した場合ですが、レスポンスヘッダはおおよそ以下のような感じです。

{
  "Rate-Reset": "1500863004",
  "X-XSS-Protection": "1; mode=block",
  "X-Content-Type-Options": "nosniff",
  "Rate-Remaining": "989",
  "transfer-encoding": "chunked",
  "Total-Count": "8",
  "Vary": "Origin",
  "X-Request-Id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "Rate-Limit": "1000",
  "Server": "nginx",
  "Connection": "keep-alive",
  "X-Runtime": "0.431045",
  "ETag": "W/\"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"",
  "Link": "<https://qiita.com/api/v2/authenticated_user/items?page=1&per_page=100>; rel=\"first\", <https://qiita.com/api/v2/authenticated_user/items?page=1&per_page=100>; rel=\"last\"",
  "Cache-Control": "max-age=0, private, must-revalidate",
  "Date": "Mon, 24 Jul 2017 01:53:18 GMT",
  "X-Frame-Options": "SAMEORIGIN",
  "Content-Type": "application/json; charset=utf-8"
}

色々ありますが、よく見るのは以下の値だと思います。

  • Rate-Limit: リクエスト回数上限。ここでは1000回。
  • Rate-Remaining: あと何回リクエストできるか。ここでは989回(既に11回分リクエストしている)
  • Rate-Reset: Rate-Remaining が回復する時間。エポックタイム。ここでは 1500863004 = 2017年7月24日(月) 11:23:24
  • Link: 1度に全件取得できなかった場合に、「今は何ページ目を取得しているのか」とか「次/前のページを取る URL は何か」といった Pagination 情報が入っている。
  • Total-Count: 取得したデータ件数。ここでは8件。

スクリプトの実装

ちょうど Qiita 記事をバックアップしたかったので、「自身が投稿した全ての記事を取得する」スクリプトを書きました。

成果物は GitHubにアップロードしました。Windows + Python 2.7 です。自分用なので若干手抜きです。たとえば Pagination に対応していないので 100 件以上は取れません。

スクリプト全文

こんな感じになりました。GitHubにもアップロードしてます

# -*- coding: utf-8 -*-

import json
import os
import sys

import requests

def abort(msg):
    print 'Error!: {0}'.format(msg)
    sys.exit(1)

def ustr2filename(ustr):
    """ Windows で有効なファイル名に変換する. """

    ret = ustr

    # ファイル名として使えない文字を除外.
    invalid_chars = u'\\/:*?"<>|'
    for invalid_char in invalid_chars:
        ret = ret.replace(invalid_char, u'')

    # ターミナルのエンコーディングでエンコード.
    ret = ret.encode(sys.stdout.encoding)

    return ret

def get(url, params, headers):
    r = requests.get(url, params=params, proxies=proxies, headers=headers)
    return r

def post(url, data_dict, headers_dict):
    r = requests.post(url, data=json.dumps(data_dict),
                      proxies=proxies, headers=headers_dict)
    return r

def print_response(r, title=''):
    c = r.status_code
    h = r.headers
    print '{0} Response={1}, Detail={2}'.format(title, c, h)

def assert_response(r, title=''):
    c = r.status_code
    h = r.headers
    if c<200 or c>299:
        abort('{0} Response={1}, Detail={2}'.format(title, c, h))

class Article:
    def __init__(self, d):
        self._title      = d['title']
        self._html_body  = d['rendered_body']
        self._md_body    = d['body']
        self._tags       = d['tags']
        self._created_at = d['created_at']
        self._updated_at = d['updated_at']
        self._url        = d['url']

        user = d['user']
        self._userid   = user['id']
        self._username = user['name']

    def save_as_markdown(self):

        title = ustr2filename(self._title)
        body  = self._md_body.encode('utf8')

        filename = '{0}.md'.format(title)
        fullpath = os.path.join(MYDIR, filename)
        with open(fullpath, 'w') as f:
            f.write(body)

    def list2file(filepath, ls):
        with open(filepath, 'w') as f:
            f.writelines(['%s\n' % line for line in ls] )

MYDIR = os.path.abspath(os.path.dirname(__file__))

proxies = {
    "http": os.getenv('HTTP_PROXY'),
    "https": os.getenv('HTTPS_PROXY'),
}
token = os.getenv('QIITA_ACCESS_TOKEN')
headers = {
    'content-type'  : 'application/json',
    'charset'       : 'utf-8',
    'Authorization' : 'Bearer {0}'.format(token)
}

# 認証ユーザの投稿一覧
url = 'https://qiita.com/api/v2/authenticated_user/items'
params = {
    'page'     : 1,
    'per_page' : 100,
}
r = get(url, params, headers)
assert_response(r)
print_response(r)

items = r.json()
print '{0} entries.'.format(len(items))
for i,item in enumerate(items):
    print '[{0}/{1}] saving...'.format(i+1, len(items))
    article = Article(item)
article.save_as_markdown()

使用した Qiita API

認証中のユーザの投稿の一覧 - Qiita API v2ドキュメント https://qiita.com/api/v2/docs#get-apiv2authenticated_useritems を使いました。

これを実行すると、投稿データが配列で返ってきます。投稿データの詳細は ユーザからの投稿 - Qiita API v2ドキュメント を見ていただくとして、重要なのは以下でしょうか。

  • rendered_body: HTML 形式の本文
  • body: Markdown 形式の原稿
  • title: 投稿のタイトル

今回は body の値で保存することにしました。

使用した言語とライブラリ

Python 2.7 の requests ライブラリを使いました。HTTP for Humans.(人間のためのHTTP) のとおり、とても使いやすいです。

Python については、3.x が主流だとはわかっていますが、使い慣れているので 2.7 を使いました :sweat:

保存方法

あまり凝っても仕方がないので、以下のように単純にしました。

  • 1投稿 = 1ファイルで保存する
  • ファイル名は (記事タイトル).md
  • 保存先はスクリプトと同じディレクトリ

ただし Windows の場合、注意点がいくつかあり、スクリプトにも反映しています。

  • Windows のターミナル(コマンドプロンプト)はエンコーディングが cp932 なので、そのまま UTF-8 で日本語タイトルをファイル名にすると文字化けする可能性がある → sys.stdout.encoding でターミナルのエンコーディングをゲットし、それに従って変換してから保存する
  • Windows ではファイル名として使用できない半角記号がちらほらある → 取り除く

おわりに

最低限の使い方はわかったと思うので、他にも色々いじくって遊んでみたいです。Qiita にも既に関連記事が投稿されているので、併せて読んでみようと思います。

ちなみに Qiita API v2 の Rate Limit は 1000回/h なので、1分で 16 回くらい使える計算です。乱用しなければ大丈夫でしょう。