QiitaAPIを利用して、Qiita記事情報収集が簡単になるツールを作った

# ツール: [get_qiita_informaion](https://github.com/developer-kikikaikai/get_qiita_informaion)

作りました。リンクは↑に貼っています。

### 概要

qiita記事のMarkdownデータや閲覧数、いいね、ストック数等の各種情報を取得することが出来るツール。

- 取得する内容はconfファイルによって自分で選択可能
- 自分の使いたいようにカスタマイズして、簡単に用途に合った情報収集が出来る。

要はページネーションとか面倒な部分をラップしたラッパーツールです。

### 使い方1

以下をcloneして、src内でmain.pyを実行します。
https://github.com/developer-kikikaikai/get_qiita_informaion

```
Usage: python3.6 main.py conf_path [option]
```

|option|説明|
|:---|:---|
|all|記事情報一覧をJson形式で表示します。<br>confファイルのdataフィールドにuserが指定されている場合はそのユーザー情報を表示します。<br>指定がない場合は最新記事を表示します。
|items|最新記事情報一覧をJson形式で表示します。<br>表示対象はconfファイルに依存します。|
|user_items|指定ユーザーの記事情報一覧をJson形式で表示します。<br>表示対象はconfファイルに依存します。<br>指定がない場合はv2ではaccess_tokenの設定されたユーザーが対象になります。|
|item|指定されたitem idの情報をJson形式で表示します。表示対象はconfファイルに依存します。|
|other|利用方法(この表示)が表示されます|

('user'指定>`access_token`指定。'user'指定は他ユーザーでも利用できる汎用版オプションという感じです。)

### 使い方2

srcパスをincludeして、QiitaMainをimportして使用すれば、取得したjson形式のデータをpythonで好きに加工できます。
使い方は以下です。

```python:
result=QiitaAPIMain(['dummy_data', 'conf_path', 'option', 'option2(あれば)']().action()
```

2018/10/11 conf_pathはファイルパスだけではなく、jsonファイルload済みのdict指定でもOKにしました。
これで動的にリクエストを変更できます。

### 設定値について

json形式。基本的には各値を結果に含めるかどうかを、dictのkeyに対してtrue or falseを指定することで表現しています。
"like":trueならいいね情報を表示する。みたいな感じ。概要はサンプルを交えて説明します。
設定値の詳細は[こちら](https://github.com/developer-kikikaikai/get_qiita_informaion/blob/master/conf/access_setting.json_sample)を参照ください。

### 取得結果

1つの記事情報は、設定値"item"要素内データのうち、設定値のpage, per_page, rawを除いたもの&&設定がtrueのものが含まれます。
こんな感じ。

```json:記事情報
{
	'title':"タイトル",
	'url':"URL",
	'html':"html形式のデータ",
	'markdown':"Markdown形式のデータ",
	'tags':"タグ一覧",
	'like':"いいね数"
}
```

また、一覧取得の場合は、以下のようなitemのidをキーにしたdict型のデータになります。

```json:記事一覧
{
	"記事のitemid":{記事情報}
}
```

# 使用例

6つサンプルを用意しました。トータル2～3時間くらいでサクッと作れました。共通コードは頭のこの呪文。以降紹介した部分は省いていきます。

```python:共通
#!/usr/bin/python3.6
#QiitaAPIをロード
sys.path.append('../../src') 
from QiitaAPIMain import *
```

## [サンプル1](https://github.com/developer-kikikaikai/get_qiita_informaion/tree/master/sample/backup_markdown). 指定ユーザー記事のバックアップを取得する。

実行すると、指定ユーザーの全公開記事が.mdファイルとして出力されます。

```python:sample/backup_markdown/backup.py
#タイトルをファイル名に出来るようにする為の処理。本質ではない。
def replase_name(ustr):
	#記号、改行排除
	text = re.sub(r'[!"“#$%&()\*\+\-\.,\/:;<=>?@\[\\\]^_`{|}~]', '', ustr)
	text = re.sub(r'[\n|\r|\t]', '', text)

	#日本語以外の文字も排除
	jp_chartype_tokenizer = nltk.RegexpTokenizer(u'([a-z]+|[A-Z]+|[ぁ-んー]+|[ァ-ンー]+|[\u4e00-\u9FFF]+|[ぁ-んァ-ンー\u4e00-\u9FFF]+)')
	text = "".join(jp_chartype_tokenizer.tokenize(text))

	return text
#ファイルに出力するだけ
def writeFile(name, mddata):
	with open(name, 'w') as f:
		f.write(mddata)
#main処理。実処理は数行
def main(args):
	markdowns=QiitaAPIMain(['backup','only_markdown.json','user_items']).action()
	for item, result in markdowns.items():
		name=replase_name(result['title'])[:30]+"_"+str(item)+".md"
		#ファイル名長制限
		writeFile(name, result['markdown'])

if __name__ == '__main__':
	main(sys.argv)
```

confはこんな感じ。"api_ver":2は必須。
userにはバックアップを取りたいユーザー名を指定してください。(ユーザー名の代わりにitems内にaccess_tokenを指定すると、非公開記事もバックアップ出来ます)
具体的な設定としては、titleとmarkdownだけ表示したいので2つをtrueにしてdefault有効のtags, likeを無効に。

```json:sample/backup_markdown/only_markdown.json
{
	"api_ver":2,
	"//user":"自分のユーザーIDを設定してください",
	"user":"developer-kikikaikai",
	"data":{
		"show":{
			"item":{
				"title":true,
				"markdown":true,
				"tags":false,
				"like":false
			}
		}
	}
}
```

他の設定もこんな感じにtrue/falseを指定します。

## [サンプル2](https://github.com/developer-kikikaikai/get_qiita_informaion/tree/master/sample/get_html_items). ごくごくシンプルなhtml形式で記事を最新20件ローカルに出力

html戦闘力5の投稿者でもhtml記事が簡単に取得&&ローカル保存できます。戦闘力5だけあって、これが一番長文 && 時間がかかりました。replase_name, writeFileはサンプル1と同じものを使用。

```python:sample/get_html_items/html.py
def main(args):
	#headerを適当に
	htmls=QiitaAPIMain(['html','only_html.json','items']).action()
	for item, result in htmls.items():
		html_str='<!DOCTYPE html><html><meta charset="utf-8" />'
		title_res=replase_name(result['title'])
		html_str+=f'<head><title>{title_res}'
		html_str+='</title></head><body>'
		html_str+=f'<h1 class="it-Header_title" itemprop="headline">{title_res}</h1>'
		#ファイル名長制限
		html_str+=result['html']
		html_str+='</body></html>'
		writeFile(f"qiita_{item}.html", html_str)

if __name__ == '__main__':
	main(sys.argv)
```

confはサンプル1のmarkdownがhtmlに変わったくらいですね。後は無限に記事を取得しないようmax, page, per_pageを指定して制限をかけています。(per_page<=maxになっていないといけない仕様にしちゃった)

```json:sample/get_html_items/only_html.json
{
	"api_ver":2,
	"max":20,
	"data":{
		"show":{
			"item":{
				"page":1,
				"per_page":20,
				"title":true,
				"html":true,
				"tags":false,
				"like":false
			}
		}
	}
}
```

## [サンプル3](https://github.com/developer-kikikaikai/get_qiita_informaion/tree/master/sample/get_own_items). 自分の記事のviews, いいね数, ストック数, コメント数を取得する。

最初に作ったツールでやりたかったことですね。実質4行で書けるようになりました。

```python:sample/get_own_items/own_items.py
def main(args):
	print(json.dumps(QiitaAPIMain(['own','item_infos.json','user_items']).action(), ensure_ascii=False, indent=4)) 

if __name__ == '__main__':
	main(sys.argv)
```

設定ファイルはこんな感じ。これ、views, stock, commentは1つのitemに対してそれぞれ1 リクエスト、合計3リクエスト必要なので重いです。なのでmaxも指定。

```json:sample/get_html_items/only_html.json
{
	"api_ver":2,
	"max":20,
	"data":{
		"//access_token":"自分のaccess_tokenを設定してください",
		"access_token":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
		"show":{
			"item":{
				"page":1,
				"per_page":20,
				"title":true,
				"tags":false,
				"like":true,
				"views":true,
				"stock":true,
				"comment":true
			}
		}
	}
}
```

※multi_reqブランチで並列処理も試し中。処理実装自体は出来ますが、同時アクセス過多になるとQiita側が503エラーを返してきます。retry && waitを入れれば何とか動きそうですが、結果1スレッド処理と時間が変わらなさそうなので多分不採用になります。

## [サンプル4](https://github.com/developer-kikikaikai/get_qiita_informaion/tree/master/sample/get_raw_qiitav2_items). QiitaAPIv2 itemsの生データを取得。

お前の加工データなんかいらん！お前はページネーションだけ処理すればいいんや！という方はこちらをどうぞ。confファイルに"raw":trueと記載すればOKです。pythonコードはサンプル3と同じく4行なので省略。

```json:sample/get_raw_qiitav2_items/raw_data.json
{
	"api_ver":2,
	"max":5,
	"data":{
		"show":{
			"item":{
				"raw":true,
				"page":5,
				"per_page":1
			},
			"user":{
				"raw":true
			}
		}
	}
}
```

## [サンプル5](https://github.com/developer-kikikaikai/get_qiita_informaion/tree/master/sample/parallel_processing). 並列処理での5000件の記事を一気に取得。

こういうことが出来るから、API化は強いですよね。confを5つ用意して、こんな感じのコードを書きます。writeFileはサンプル1と同じ。
multiprocessing.ProcessでAPI取得を子プロセス化してます。

```python:sample/parallel_processing/parallel.py
def call_items(index):
	result=QiitaAPIMain(['html',f'pagenation_parallel_{index}.json','items']).action()
	writeFile(f'page_{index}_plus_10.txt', json.dumps(result, ensure_ascii=False, indent=4))

#5プロセスで5000item取得を目指す。
def main(args):
	for n in range(1,6):
		multiprocessing.Process(target=call_items,args=([n])).start()

if __name__ == '__main__':
	main(sys.argv)
```

confはこんな感じ。("page":yのyはx-1。pagenation_parallel_2.jsonなら11)

```json:parallel_processing/pagenation_parallel_x.json
{
	"api_ver":2,
	"max":1000,
	"data":{
		"//access_token":"access tocken指定の方が記事取得の制限が緩いので。自分のtokenを使ってください。",
		"access_token":"aaaaaaaaaa",
		"show":{
			"item":{
				"page":y1,
				"per_page":100
			}
		}
	}
}
```

5000件が10秒ちょっとで取得できます。

Linuxならaccess_tokenはまとめてこんな感じで置換できます。

```
ls *.json | xargs sed -i "s/aaaaaaaaaa/write_your_access_token_here/g"
```

## [サンプル6](https://github.com/developer-kikikaikai/get_qiita_informaion/tree/master/sample/parallel_processing_without_anyconf). 並列処理の記事取得、設定ファイルは1つでいいように

こんな感じにQiitaAPIMainの第2引数はjson.loads後のオブジェクトでも動作するようにしました。これでconfを複数用意することなんて必要なくなります。

```python:sample/parallel_processing/parallel.py
def call_items(obj):
    result=QiitaAPIMain(['html',obj,'items']).action()
    writeFile(f"page_{obj['data']['show']['item']['page']}.txt", json.dumps(result, ensure_ascii=False, indent=4))

#5プロセスで5000item取得を目指す。
def main(args):
    with open('pagenation_parallel.json') as f:
        conf=json.loads(f.read())
    print(conf)
    for n in range(0,5):
        multiprocessing.Process(target=call_items,args=([conf])).start()
        conf['data']['show']['item']['page']+=10

if __name__ == '__main__':
    main(sys.argv)
```

# 終わりに

調べてみたらもう煎じすぎて味がしないレベルのツール作成ですが、個人的なPDCAサイクルやpythonコード実装の練習になりました。

# 参考

ツール作成のきっかけとなったお二方
[Qiita APIを利用して記事のViewsとストック数がまとめて取得できるAPIを作ってみた](https://qiita.com/kai_kou/items/3f10ce93f4aa3b57b2d0)
[みんな大好きQiitaのバックアップツールを作ったので公開](https://qiita.com/i-tanaka730/items/6ba423abbcddde62c187)

参考にさせていただいた偉大なツール利用者、先人達
[Qiita API v2 を使って自身の全投稿をエクスポートする Python スクリプトを書いた](https://qiita.com/sta/items/5074df5fcb81d890897b)
[絶対に見逃せない投稿が、そこにはある](https://qiita.com/koher/items/b5c7c949aa62c60d5457)
[QiitaAPIでサービスを作ろうとしたら浅はかな考え過ぎた話](https://qiita.com/nnlmmc/items/0d7b68bd68ed16b7ea09)
[Qiita API v2 の概要(非公式)](https://qiita.com/tag1216/items/b0b90e30c7e581aa2b00)
[Qiita殿堂入り記事ランキングを作った物語](https://qiita.com/youwht/items/0a12079671750c39fbfa)
[PC重たいのでターミナルでQiitaの記事検索・閲覧ツール[gota]を作った](https://qiita.com/gorilla0513/items/d53ef65cb556201c3506)
[Qiitaの投稿をガーッと取得するバッチ処理用QiitaAPI Pythonラッパー](https://qiita.com/tag1216/items/7e23630d97293e35ea4c)

python関連:
有効ファイル名の参考:[日本語以外の文字と記号を排除し日本語の平文コーパスを作成する呪文](https://qiita.com/haminiku/items/5907cb81325083cb36c7)
並列処理: [Pythonでの並行処理と並列処理](https://qiita.com/ogihara/items/1b7f002b7d14a2710bce)


--------------

### 余談

ツールの構成や不満点について、自己Q/A方式でメモ書き

#### Q. 何故inputをjson confファイルにしたのか？何故独自responseフォーマットにしたのか？

- A. バージョンアップでの仕様変更を吸収できる作りにしたかったから

ちょうど私がQiitaを使いはじめたころ、いい記事の検索方法ないかな～と探すと、いい感じのサイトが
「APIの仕様変更によりxxxが取得できなくなりました。アップデートの時間も取れない。。。残念」
となっているのを見かけ、すごく寂しい気持ちになったのをよく覚えています。

その印象が強かったので、今回はまず「version差分が吸収出来る」をテーマに作成しました。(1レスポンスでストック数欲しいよね！じゃあフォーマット変えるか！ってのもあったけど)

内部のクラス構成としては以下のようになっています。

- QiitaAPI抽象クラスを各バージョンが継承し、jsonのdata部を解析する(dataの中はversionに依存)
- QiitaAPIGeneratorが唯一各バージョンのクラス名を知るものとして、confの内容を元にクラスインスタンスを生成する(ただし渡すのはdata部。中身は知らない)
- QiitaAPIMainはQiitaAPIGenerator経由でただ抽象化されたメソッドをコールするだけ

という感じに、各々ある程度抽象に依存する形になってるんじゃないかな。

#### Q. コメント、ストック数、ユーザー情報の取得を並列化したら軽くなるのでは？

A. 試してみたけど挙動が安定しないので無理でした。

実験したコードを以下に上げています。process, thread試してみたけど、アクセス途中で503エラーが返るようになるんですよね。
https://github.com/developer-kikikaikai/get_qiita_informaion/tree/multi_req

多分攻撃防止だと思いますが。503エラーならretryみたいな形も試してみましたが、結果そんなに普通の1スレッド処理と変わらない感じ && 不安定 && 実験しすぎて公式のアクセス制限に引っかかってしまい、もう限界！諦める！ってなっちゃいました。
もし全情報を取得したいのであれば、バックグラウンドでpython3.6 main.py conf_path item IDを利用してひとつづつ地道にゆっくり取得した方が堅実かな。この辺はその都度値が変わるし

#### Q. タグといいねがデフォルト有効なのはなんで？一々falseって書かなきゃいけないじゃん？

A. すまん、なんとなくとしか。タグはフィルターとして、いいねは満足感としてほしいかなと思った。
QiitaAPIv2.pyのself.MNG_PROP_SHOWがデフォ値なので適当に書き換えてね。

#### Q. `user`指定<`access_token`指定のがよくね？

- A. 今思うと確かに(all⇒'user', items⇒ユーザーなし, user_items⇒自分になるから)。でも挙動までversionの特性に依存するのも違うかなと思って(言い訳

#### Q. コメントやストックしてくれたユーザー情報は取れないの？

A. Qiita APIv2の生データでは取れます。

正直APIとして定義するほどの使い道が思いつかなかったので、rawで生データが取得できるという形に逃げました。

#### Q. v3が出たらアップデートする？

A. 飽きていなければします。~~正直並列処理で503が返ってきた時点で飽きが…~~