Qiita API v2 で自身のストックを全部消す Python スクリプトを書いてみた

600件以上溜まってるストックを全部消したかったので書いてみました。スクリプトは [GitHubにアップロードしてます](https://github.com/stakiran/qiita_stock_canceler)。

# Qiita API v2 について
Qiita API v2 の全般情報は以前書いた [Qiita API v2 を使って自身の全投稿をエクスポートする Python スクリプトを書いた](https://qiita.com/sta/items/5074df5fcb81d890897b) の方でまとめています。

# スクリプトについて

## 動機
自分の書いた記事への反応のみを通知してほしいのに **「ストックした記事」が更新された時も通知されてしまうのが煩わしい** からです。

運営に問い合わせてみたところ、この通知を切る方法は現時点では無いとのことでした。

さて、ストックを消していきましょうか……と行きたいところですが、ストック数は 600 を超えており、手作業で一つずつ消すのは苦痛です。そこで API を使って一気に消すことにしました。

## (余談1) そもそもなぜそんなにストックしている?
Qiita には「いいね」と「ストック」がありますが、私は以下のように認識していて、

- いいね: 投稿者に対する労い
- ストック: 個人的なブックマーク

いつも機械的にいいねとストックを同時に行ってました。

## (余談2) 結局どんな運用にした?
ブックマーク機能部分は [いいねした投稿](https://qiita.com/sta/like) から見れるため **ストックは行わずいいねだけ行う** ことにしました。

# 動作のデモ
以下は当方のアカウントで試してみた例です。665件のストックを全部消してます。

```
$ python stock_canceler.py -u sta
getting between 1 to 100...
getting between 101 to 200...
getting between 201 to 300...
getting between 301 to 400...
getting between 401 to 500...
getting between 501 to 600...
getting between 601 to 700...
1/655 TITLE:Windows環境＋node.js＋mocha＋istanbulでUTしてコードカバレッジを取る...
2/655 TITLE:grunt+istanbul+mochaでNode.jsのテスト＆カバレッジ計測を行う...
3/655 TITLE:システムで「性別」の情報を扱う前に知っておくべきこと...
...
653/655 TITLE:2015年センター試験数学IAを全てプログラム(Python)で解く...
654/655 TITLE:100万倍速いプログラムを書く...
655/655 TITLE:いい結婚相手を見つける最適な方法を検証してみた...
Fin.

$ python stock_canceler.py -u sta
getting between 1 to 100...
Fin.
```

# スクリプトの実装

## 使用した Qiita API
必要なのは以下二つです。

- (1) [指定ユーザーのストック一覧を GET する](https://qiita.com/api/v2/docs#get-apiv2usersuser_idstocks)
  - `GET /api/v2/users/:user_id/stocks`
  - ユーザーとして自分自身を指定すれば自分のストック一覧を取れます
- (2) [指定した記事を自分のストックから DELETE する](https://qiita.com/api/v2/docs#delete-apiv2itemsitem_idstock)
  - `DELETE /api/v2/items/:item_id/stock`
  - ストックから消したい記事の ID が必要ですが、これは (1) から取ってきます

使い方としては、

- まず (1) で自分のストック一覧を全部 GET する
- GET したストック一覧を走査して……
  - item_id を取り出す
  - (2) のリクエストを送る

このようになります。

## 使用した言語とライブラリ
- Python 3.6
- [requests](https://github.com/requests/requests) ライブラリ

## ページネーションについて
Qiita API では 1 度に 100 件までしかデータを取れないので、100 件より多く取りたい場合は [ページネーション](https://qiita.com/api/v2/docs#%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%8D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3) を考える必要があります。

ドキュメントに従うならレスポンスの Link ヘッダやら Total-Count やらを見て、上手いこと繰り返しリクエストを送ればよいのですが、面倒くさかったので本スクリプトでは横着してます。

以下にコードを抜粋します。

まずストック一覧をリクエストするコードが以下のようになっていて、

```python
def get_stocks(target_userid, page, per_page):
    url = 'https://qiita.com/api/v2/users/{}/stocks'.format(target_userid)
    params = {
        'page'     : page,
        'per_page' : per_page,
    }
    r = get(url, params, headers)

    items = r.json()
    stocks = []
    is_last = False

    # int(r.headers['Total-Count']) 見た方が良い?
    # でも len(items) と Total-Count が食い違うケースはそうはないと思う(思いたい)ので
    # とりあえず len で判定.
    # ★ per_page(=100)件分取れなかった＝データ全部取れた、とみなす
    if len(items) < per_page:
        is_last = True

    for i,item in enumerate(items):
        stock = Stock(item)
        stocks.append(stock)

return [stocks, is_last]
```

これを以下のコードで繰り返し呼び出すことでページネーション（っぽいこと）を実現しています。

```python
PAGE_START_NO = 1
target_userid = args.username
per_page = 100

cur_page = PAGE_START_NO
all_stocks = []

# ★ 1-100件、101-200件、というふうに繰り返すループ
while True:
    a = (cur_page-1)*per_page + PAGE_START_NO
    b = a + per_page - 1
    print('getting between {} to {}...'.format(a, b))

    stocks, is_last = get_stocks(target_userid, cur_page, per_page)
    all_stocks.extend(stocks)

    if is_last:
        break

    cur_page += 1
```

長いですが、要するに

- 1-100件目を取ってくる
- 101-200件目を取ってくる
- ……

とひたすら繰り返していき、 **100件分取れなかった時点を終わりとみなし** ています。自分用のスクリで、一回しか実行しないのでこんなもんでいいでしょう :smirk:

# おわりに
これで煩わしいストック通知からおさらばできました。API があると、こういう作業も比較的楽に行えるから良いですね。