Qiita記事の更新回数を取得するAzure Function App

# この記事は

QiitaAPIを使うと、任意の記事の投稿日時`created_at`や最終更新日時`updated_at`が取得できる

しかし、**その記事が何回更新されたか**まではわからない

https://qiita.com/nkay/items/189496192b328bb2b5e5

> 「記事の更新回数」というデータはないのが残念です。

この記事は、AzureFunctionsを使って、任意の記事の更新回数を取得した記録
巻末に[ソースコード](#ソースコード)も掲載

# 編集履歴の回数をカウントする

記事の編集履歴を開くと・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/73ba58d9-c55c-a0bf-5e1d-4c7890e574b2.png)

その記事が更新された回数がわかる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/768fef9d-3476-0d6c-5439-001b68a6175c.png)

記事のURL末尾に`/revisions`をつけたページを解析すれば、更新回数がわかりそう

# `revisions`ページを調べる

更新されるたびに、`tableList_cell`クラスの`div`が追加されている

⇒その`div`の数がすなわち記事の更新回数になりそう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/9e4189f2-eb63-5d21-f27c-070288a95420.png)

# Azure Functionsで試す

APIっぽくつくるためにAzure Functionsを持ち出す

## 結果

Contentに8とあるのが、更新回数

```powershell:成功例
Invoke-WebRequest "http://localhost:7146/api/QiitaUpdateCounter?user=t13801206&item=481db1a82585483bb1a3"

StatusCode        : 200
StatusDescription : OK
Content           : 8
RawContent        : HTTP/1.1 200 OK
                    Date: Sat, 20 Aug 2022 07:34:40 GMT
                    Server: Kestrel
                    Content-Length: 1
                    Content-Type: application/json; charset=utf-8

                    8
Headers           : {[Date, System.String[]], [Server, System.String[]], [Content-Length, System.String[]], [Content-Ty
                    pe, System.String[]]}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 1
RelationLink      : {}
```

ユーザIDまたは記事IDが正しくないと、ステータス400を返す

```powershell:失敗例1
Invoke-WebRequest "http://localhost:7146/api/QiitaUpdateCounter?user=t13801206&item=481db1a82585483bb1a"
Invoke-WebRequest: Not found t13801206/items/481db1a82585483bb1a
```

クエリに`user`と`item`のどちらかが欠けていると、ステータス400を返す

```powershell:失敗例2
Invoke-WebRequest "http://localhost:7146/api/QiitaUpdateCounter?user=t13801206"
Invoke-WebRequest: This HTTP triggered function executed successfully. Pass a user and item in the query string or in the request body for a personalized response.
```

## ソースコード

:::note warn
攻撃とみなされないために、HTTPリクエストを送る頻度に注意しましょう
:::

```csharp:AzureFuction
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Threading.Tasks;

namespace QiitaUpdateCounterFunction;

public static class QiitaUpdateCounter
{
    [FunctionName("QiitaUpdateCounter")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req, ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        string user = req.Query[nameof(user)];
        string item = req.Query[nameof(item)];

        if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(item))
        {
            return new BadRequestObjectResult(
                "This HTTP triggered function executed successfully. Pass a user and item in the query string or in the request body for a personalized response."
            );
        }

        try
        {
            using var client = new HttpClient();
            HttpResponseMessage response = await client.GetAsync($"https://qiita.com/{user}/items/{item}/revisions");
            response.EnsureSuccessStatusCode();
            string responseBody = await response.Content.ReadAsStringAsync();

            var parser = new AngleSharp.Html.Parser.HtmlParser();
            AngleSharp.Html.Dom.IHtmlDocument doc = parser.ParseDocument(responseBody);
            AngleSharp.Dom.IHtmlCollection<AngleSharp.Dom.IElement> nodes = doc.QuerySelectorAll("div.tableList_cell");

            return new OkObjectResult(nodes.Length);
        }
        catch (HttpRequestException)
        {
            return new BadRequestObjectResult($"Not found {user}/items/{item}");
        }
    }
}
```