Qiita APIとtrocco®で組織の成果を可視化してみる

# はじめに
前回の記事（[「初投稿のPVが爆増したのがtrocco®とQiita APIでよくわかった話」](https://qiita.com/SoySoySoyB/items/c4221a0a000b497448f7)）では、Qiita APIを利用して、自分のアカウントのデータを取得する仕組みについて紹介しました。

その最後で、組織のデータを取るのは難しいという話に触れていたのですが、せっかくなので遊んでみるかということで、データの取得方法を考えてみました。

# 何が難しいのか
Qiita APIで記事データを取得するには、リクエストを投げる際に`page`パラメータを指定する必要があります。自分のデータの取得であれば、

- `api/v2/authenticated_user`で記事数を取得
- 取得した記事数をもとにpageの数値を計算して、`api/v2/authenticated_user/items`から記事データを取得

という流れで処理を行うことができます。

一方、組織のデータを取得しようとすると、APIで組織の記事数を取得できないため、別の工夫をして記事数を特定しなければなりません。記事数が上手く特定できないと、

- 指定した`page`に対象記事がないことで、エラーが発生する
- 記事のある`page`を指定できていないことで、取得漏れが生じる

可能性があります。

# 増減の要因を整理する

対応方法を考えるにあたり、まずは記事数の増減要因を考えてみます。

- 1. 増加要因
    - 1-1. 新規記事の作成
    - 1-2. 既存記事への組織連携の追加付与
- 2. 減少要因
    - 2-1. 既存記事への組織連携の解除
    - 2-2. ユーザーアカウント削除（未検証ではあるのでおそらく）

なお、アカウント単位の組織連携と記事単位の組織連携は独立しているので、アカウントの組織連携を解除しても記事の連携には影響しません。（[「Organizationの設定について」](https://help.qiita.com/ja/articles/qiita-org-setting)）

そこで対応を考えてみると、

- 1-1と1-2：`updated_at`が前回データ取得時よりも後になっているものから拾えそうです。（既存記事への組織連携への追加付与で`updated_at`は更新される気がする／未検証）
- 2-1と2-2：前回データ取得にあったが、今回なくなったものを検知すればよさそうです。

しかし、これをやろうとするとなかなかややこしいですね・・・記事数を取得できるような仕組みがあると助かるのですが。

ちなみにこれは余談ですが、組織ページの記事投稿数と、記事検索での組織連携されている記事数にずれがあり、組織ページの数値をスクレイピングするという裏技も使えずでした。なんでずれているのだろう。

- [組織ページの記事投稿数](https://qiita.com/organizations/primenumber_inc/items)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/5433b25c-b7ca-a5b2-dab7-a99ab28d7ca4.png)

- [記事検索での組織連携されている記事数](https://qiita.com/search?q=org%3Aprimenumber_inc)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/b18f2043-e444-e810-c96d-cb5159d04d99.png)

# 方針を整理する
下記のような流れで進めれば対応できそうです。

- 新たに更新された記事データを取得する
- 前日に取得していた記事データを取得し、上記と合算することで本日該当するであろう記事数を算出
- 記事数をもとに`page`を算出して本日分の記事データを取得
- 本日取得した記事からユーザーIDを特定してユーザーデータを取得
- （2の減少分の考慮はややこしいので諦めました／何かいいやり方があれば教えてください笑）

ワークフローはこのようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/8a3c40ba-80fc-691d-2ede-3770a331d51b.png)

# trocco®で実装してみる
内容はかなり[前回](https://qiita.com/SoySoySoyB/items/c4221a0a000b497448f7)と同じようなものが多いので、異なる部分を中心にご紹介します。

## 新たに更新された記事データを取得する
- APIのURLはhttps://qiita.com/api/v2/items
- カスタム変数で前日の日付を取得して、queryのパラメータに入れることで、前日以降に更新された記事を取得する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/29a0754f-af04-6ba2-dcae-b46feeb323af.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/26e3676e-0eb3-0a8e-8fec-942f12e72bed.png)

```
query=org:primenumber_inc updated:>=$yesterday$
```

## 前日に取得していた記事データを取得し、上記と合算することで本日該当するであろう記事数を算出
この部分はクエリで処理をしてしまいます。下記のクエリでできますね。
```sql
select 
  count(id) as items_count
from
   (
    select distinct id from `project-name.dataset_name.qiita_api_items_primenumber_inc`
    where date(timestamp_add(transferred_at, interval 9 hour)) = date_sub(current_date('Asia/Tokyo'), interval 1 day)
    union distinct
    select id from `project-name.dataset_name.qiita_api_items_primenumber_inc_newly_updated`
   )
```

## 記事数をもとに`page`を算出して本日分の記事データを取得
上記のクエリを踏まえて、前回も登場したBigQueryのクエリ結果でループ実行を利用します。
```sql
with sum_items as (
  select 
    count(id) as items_count
  from
     (
      select distinct id from `project-name.dataset_name.qiita_api_items_primenumber_inc`
      where date(timestamp_add(transferred_at, interval 9 hour)) = date_sub(current_date('Asia/Tokyo'), interval 1 day)
      union distinct
      select id from `project-name.dataset_name.qiita_api_items_primenumber_inc_newly_updated`
     )
)

select
  page
from
  unnest(generate_array(1, (select div(items_count - 1, 100) + 1 from sum_items))) page
```

- 今回は複数のユーザーのデータを取得することになるので、ユーザー情報が扱いやすくなるよう、JSONデータをパースしておきます。ただ`user`以下のパスを記載するだけでできます。便利。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/5e0040c6-4833-3f98-6f14-8516451ee83b.png)

## 本日取得した記事からユーザーIDを特定してユーザーデータを取得
- APIのURLはhttps://qiita.com/api/v2/users/$user_id$
- カスタム変数で`user_id`を作成しておいて、このIDをBigQueryのクエリ結果でループ実行する
```sql
select distinct
  user_id
from
  `project-name.dataset_name.qiita_api_items_primenumber_inc`
where
  date(timestamp_add(transferred_at, interval 9 hour)) = current_date('Asia/Tokyo')
```
- ちなみに、このまま実行するとループを回す分で完了までやや時間がかかるので、該当ジョブとワークフローの両方で並列処理をするよう設定しておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/77cb0dcb-fadb-0424-b314-6e4620e6c3d5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/40839fc9-9dcb-e33e-34a0-ce7e5bb060e1.png)

# 組織の成果を可視化してみる
これでデータが揃ったので、Looker Studioでグラフにしてみます。~~凝ったものを作る前にめげました。~~
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/5b270cde-0f19-b0ce-4527-2b22e464e06b.png)

そしてこれを作ってから気が付いたのですが、限定公開すると検索上は公開日が限定公開を設定した日になっていて、一方で記事上での公開日は一般公開日になっている・・・（すると、限定公開の記事も組織の公開記事数に含まれているということかもしれない）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/db4438e3-5aaa-97ab-d985-b18f5f23dc50.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492462/00dd7963-3bc4-9c42-4161-5fd7ed34445e.png)
※ちなみにクエリ結果でループ実行については、[上記の記事](https://qiita.com/MK0000/items/0630a8611263aaebd5a6)も参考になるのでぜひご覧ください！

# おわりに
これで一通り組織のデータが取得できるようになりました。

- 記事数
- Like数
- ストック数
- コメント数

を蓄積して活用することができます。PV数が取得できないのが難点ですが、記事投稿数やLike数がわかるだけでも少しは役立ちそうです。

# おまけ：タグの加工（`string_agg`が便利だった件）
タグの情報についても、そのままで取得するのはJSON形式になりますが、少し加工してあげるとフィルタ等で利用することができるようになります。データ転送時ではなく、BigQueryに取り込み後のSQLでも加工はできます。下記のようなものです。
```sql
tag_all as (
  select
    article_id,
    transferred_at,
    string_agg(json_value(t.name), ',') as tag_all
  from
    tmp_data  -- ここのもとは省略
  left join
    unnest(json_query_array(parse_json(tags))) t
  group by
    article_id, transferred_at
),
```
この処理でこれが、
```json
[
{"name":"QiitaAPI","versions":[]},
{"name":"BigQuery","versions":[]},
{"name":"trocco","versions":[]}
]
```
こうなります。
```
QiitaAPI,BigQuery,trocco
```
`string_agg`とかどこで使うのかと考えていたら、案外便利そうに思えました。