Qiita APIを使ってある期間の記事情報を取得した話

# はじめに
　同期からQiita APIなるものがあることを知り、触ってみたいなと思ったのがきっかけです。少しですが私自身も記事を書いているので、どんな記事が読まれているのか、どんなタイトルに惹かれるのかなど解析できるのではないだろうかと思いました。本記事ではQiita APIを使って記事情報を取得する方法を紹介したいと思います。

# Qiita APIって？
　Qiita APIを使えば、Qiitaで公開されている記事情報（記タイトル、本文、投稿日、LGTMの数、コメントの数など）を取得することができます。しかも、コードもそこまで難しくはありません。私自身初めてQiita APIを触りましたが一日もあれば記事情報の取得まですることができました。Qiita APIについて詳細を知りたい人は[こちら](https://qiita.com/api/v2/docs#%E6%A6%82%E8%A6%81)をご参照ください。

# 記事情報の取得コード
　それでは、コードを紹介していきたいと思います。少し長くなってしまったので、「準備」と「実行」の２つに分けて紹介したいと思います。このコードを通して、2021年8月に作成されたQiita記事情報（タイトル、タグ、url、コメントの数、LGTMの数、作成日、記事を書いたユーザーID、本文）を取得します。

## 準備
　実行の前に準備をしていきます。準備では、アクセストークンや取得する期間の設定などを行います。記事の取得にはアクセストークンが必要になります。[こちらの記事](https://qiita.com/miyuki_samitani/items/bfe89abb039a07e652e6)の「Qiitaのアクセストークンの取得」を参考にしてアクセストークンを取得しましょう。取得したアクセストークンをtokenという変数に代入します。

```Python:準備
#ライブラリのインポート
import numpy as np
import pandas as pd
import math
import requests
import json
from tqdm.notebook import tqdm
import time
import datetime

token = "+++++Please your token+++++"

headers = {
    "content-type": "application/json",
    "Authorization": "Bearer " + token
}

# Qiitaのurl
qiita_url = "https://qiita.com/api/v2"

# タグでフィルターを掛けられる。例えばPythonとタグ付けされた記事のみの取得も可能です。
tag_no = "/items"

wait_time = 3600

# この期間に作成されたQiitaの記事情報を取得
start = '2021-07-31'
end = '2021-08-31'

# 半月ごとの日付をリスト化
date_list = [d.strftime('%Y-%m-%d') for d in pd.date_range(start, end, freq="SM")]
start_list = date_list[:-1]
end_list = date_list[1:]

columns = ["title", "tags", "article_url","comments_count", "likes_count", "created_at", "user_id", "text"]
df = None
```

**コードの補足**

- wait_timeについてですが、記事情報へのアクセスリクエストは、認証しているユーザでは1時間に1000回まで、認証していないユーザーはIPアドレスごとに1時間に60回までだそうです。そのため、アクセス制限がきたら1時間（3600秒）待機するようにコーディングしました。

- 8月中の記事を取得したかったので便宜上、2021年7月31日からとしています。実際取得したのは標準時（日本は+9時間）の影響なのか、8月1日の9:00から8月31日の9:00までになっています。これは今後の課題です。
- 半月ごとの日付をリスト化していますが、途中経過などを見たくてこのようにしています。目的に対する本質的なものではありません。
- 実行の部分で取得したデータをdfにどんどん入れていきます。そのためにNoneを入れています。Noneを入れておくと最初に計算や代入、結合したものと同じ型になります。

## 実行
　記事情報を取得してデータフレームにどんどん格納していくコードになっています。for文を入れ子にして実行しているので、もっと良い方法などあるかもしれません。

```Python:実行
for i in tqdm(range(len(start_list))):

    # 記事取得する期間の設定
    start_date =  start_list[i]
    end_date = end_list[i]

    query = "&query=created:>" + start_date  + "+created:<" + end_date

    url_query = qiita_url + tag_no + "?" + query

    res = requests.get(url_query, headers=headers)
    
    if res.status_code == 403:
        after_time = datetime.datetime.fromtimestamp(time.time() + wait_time)
        print("アクセス制限です。少なくとも {} までは待機します。".format(after_time.strftime('%Y/%m/%d %H:%M:%S')))
        time.sleep(wait_time)
        res = requests.get(url_query, headers=headers)
    
    total_count = int(res.headers['Total-Count'])
    
    print("{} から {} に作成された記事は {} 個です。".format(start_date, end_date, total_count))
    
    jsondata = json.loads(res.text)
    page_count = math.ceil(total_count / 100)
    
    hundred = math.floor(total_count / 100)
    fraction = total_count %100
    
    par_pages = []
    for _ in range(hundred):
        par_pages.append(100)
    
    par_pages.append(fraction)
    
    for p in range(page_count):
        par_page = par_pages[p]
        p += 1
        page = "page=" + str(p)
        res = requests.get(qiita_url + tag_no + "?" + page +"&per_page=" + str(par_page) + query, headers=headers)
        
        if res.status_code == 403:
            after_time = datetime.datetime.fromtimestamp(time.time() + wait_time)
            print("アクセス制限です。少なくとも {} までは待機します。".format(after_time.strftime('%Y/%m/%d %H:%M:%S')))
            time.sleep(wait_time)
            res = requests.get(qiita_url + tag_no + "?" + page +"&per_page=" + str(par_page) + query, headers=headers)
            
        jsondata = json.loads(res.text)
        
        for item in jsondata:
            # 各記事を取得
            article_data = []
            item_id =  item['id']

            url = qiita_url + tag_no + "/" + item_id

            res = requests.get(url, headers=headers)
            
            if res.status_code == 403:
                after_time = datetime.datetime.fromtimestamp(time.time() + wait_time)
                print("アクセス制限です。少なくとも {} までは待機します。".format(after_time.strftime('%Y/%m/%d %H:%M:%S')))
                time.sleep(wait_time)
                res = requests.get(url, headers=headers)
                
            json_content = json.loads(res.text)

            title = json_content["title"]

            tags = []
            for j in range(len(json_content["tags"])):
                tags.append(json_content["tags"][j]["name"])

            article_url = json_content["url"]
            comments_count = json_content["comments_count"]
            likes_count = json_content["likes_count"]
            created_at = json_content["created_at"]
            user_id = json_content["user"]["id"]
            text = json_content["body"]
            
            article_data.append([title, tags, article_url,comments_count, likes_count, created_at, user_id, text])

            article_data = pd.Series(article_data[0], index = columns)
            article_data = pd.DataFrame([article_data])
            df = pd.concat([df, article_data], axis = 0, ignore_index=True)
```

**コードの補足**

- 1行目のtqdm()関数は進捗確認ができよく使っています。
- 「res.status_code == 403」ではアクセス制限であるという条件です。この場合の処理を下に書いており、time.sleep(wait_time)で1時間待機しています。
- total_countは指定した期間にある記事の総数を表しています。1ページに100記事あり、ページをずらしながら記事の取得をしています。そこでページ数（page_count）とページごとの記事数（par_pages）という変数を作成しています。

　これを実行すると、取得した記事情報がデータフレームとして入手できます。実行中には次の図のように表示されていました。

![qiita_api_ログ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1625514/0183a7a6-5fb0-305e-d05a-404fd67b673b.png)

アクセス制限が来た際にはしっかりと1時間待機していますね。また、一ヶ月だけで8000記事弱の投稿があるようでこんなに投稿されていたのだと新しい発見もありました。

# Word Cloudで簡単に可視化

```Python:WordColud
import re
import neologdn
import texthero as hero

# 文字列型にする関数
def set_str(text):
    return str(text)

# タグを半角スペースでつなげていくコード
def del_symbol(text):
    return text.replace("[","").replace(",", " ").replace("'", "").replace("]", "")

tags = data_frame["tags"].apply(set_str).apply(del_symbol).apply(neologdn.normalize)

# Word Coludの表示
hero.visualization.wordcloud(tags,font_path = "NotoSansCJKjp-hinted/NotoSansCJKjp-DemiLight.otf",
                             colormap='viridis', 
                             width=500, 
                             height=500, 
                             background_color='black')
```

**コードの補足**

- textheroを用いてWord Cloudの結果を表示しています。Word Cloudは単語の出現頻度を可視化した図でどんな単語が多いのか視覚的にわかります。
- デフォルトだと日本語を図に含めることができないので日本語表示できるフォントの入手が必要になります。[こちら](https://zenn.dev/yagiyuki/articles/e05bc37d6c3bc283c5f1)を参考にしてみてください。

　これを実行した結果は次のような図になりました。日本語もしっかり表示されていますね(小さいですが・・・)。図を見るとやはりPythonが大きく表示されています。次いでAWSといったろころでしょうか。このように可視化するとインパクトがあり、面白いですね。

![WordCloud_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1625514/3cc3299c-ff6f-5a6d-22b1-109ea80a2e04.png)


# さいごに
　本記事では、Qiita APIを使って記事情報の取得を行いました。アクセス制限やコーディングの技量のため時間がかかってしまいます（8000記事弱を取得するのに8時間から9時間かかっています）が、目的の情報を取得できたことは嬉しく思います。また、APIを使えばこのように簡単にできるので他のAPIも試してみたいですね。コード改良のアドバイスやおすすめAPIなどあれば教えていただくと大変助かります。

　読んでくださりありがとうございました。