0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C# - Qiita と Zenn 記事の統計情報をあわせて閲覧

Posted at

はじめに

現在、記事を Qiita, Zenn にクロスポストしています。
以前「Qiita API v2 でページビューの一覧取得」についての記事を掲載しましたが、Zenn 統計情報とあわせて閲覧したいと思ったので、対象ソースコードを作成してみました。

NuGet 対象、Qiita API、Qiita アクセストークン、および、HttpClient の扱い方については下記記事をご確認ください。

Zenn API については下記記事をご確認ください。

テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。

  • Windows Forms - .NET Framework 4.8
  • Windows Forms - .NET 8
  • WPF - .NET Framework 4.8
  • WPF - .NET 8

記載したソースコードは .NET 8 ベースとしています。
.NET Framework 4.8 の場合は、コメントで記載している null 許容参照型の明示 ? 、および、required を削除してください。

Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。

事前処理

NuGet

詳しくは「はじめに」に記載した「Qiita API v2 でページビューの一覧取得」に記載していますが、NuGet で取得するモジュールを記載しておきます。

  • JSON デシリアライズ用(.NET Framework のみ必要)
    • PM> NuGet\Install-Package System.Text.Json
  • IHttpClientFactory を用いて HttpClient 利用
    • PM> NuGet\Install-Package Microsoft.Extensions.DependencyInjection
    • PM> NuGet\Install-Package Microsoft.Extensions.Http

初期設定

変数、定数、IHttpClientFactory に関するソースコードを掲載します。

// .NET Framework 時は null 許容参照型の明示 `?` を削除してください
private static IServiceProvider? MyServiceProvider = null;
private const string QiitaUrl = "https://qiita.com";
private const string QiitaAccessToken = "<事前準備で取得したアクセストークン>";  // TODO
private const string ZennUrl = "https://zenn.dev";
private const string ZennUsername = "<対象ユーザ名>";  // TODO
// ServiceProvider 初期化
private void ServiceProvider_Initialize()
{
  var serviceCollection = new ServiceCollection();
  serviceCollection.AddHttpClient("QiitaClient", client =>
  {
    client.BaseAddress = new Uri(QiitaUrl);
    client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", QiitaAccessToken);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
  });
  serviceCollection.AddHttpClient("ZennClient", client =>
  {
    client.BaseAddress = new Uri(ZennUrl);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
  });
  MyServiceProvider = serviceCollection.BuildServiceProvider();
}

記事の統計情報

Qiita

「はじめに」に記載した Qiita API v2 元記事をベースとして、「タイトル」「ページビュー」「ライク数」「ストック数」取得に修正しました。

#region Qiita
// Qiita API v2 - /api/v2/authenticated_user/items
// .NET Framework 時は null 許容参照型の明示 `?` を削除してください
private async Task<List<ResponseQiita>?> QiitaGetPageList()
{
  // IHttpClientFactory で QiitaClient という名前の HttpClient 取得
  var factory = MyServiceProvider?.GetService<IHttpClientFactory>();
  var httpClient = factory?.CreateClient("QiitaClient");
  if (httpClient == null)
  {
    // エラー発生 - TODO
    return null;
  }

  string baseUrl = "/api/v2/authenticated_user/items";
  var lstPages = new List<ResponseQiita>();
  int totalCount = 0;  // 全件数
  int limit = 20;      // 1回の取得件数
  int page = 1;        // ループ回数(1~)

  // 1回の処理は limit 件のページ取得なので、ループ処理とする
  while (true)
  {
    string targetUrl = baseUrl + $"?page={page}&per_page={limit}";
    using (var request = new HttpRequestMessage(HttpMethod.Get, targetUrl))
    using (var response = await httpClient.SendAsync(request))
    {
      if (response?.IsSuccessStatusCode == true)
      {
        var content = response.Content.ReadAsStringAsync().Result;
        var obj = System.Text.Json.JsonSerializer
                     .Deserialize<ResponseQiita[]>(content);
        if (obj == null)
        {
          // エラー発生 - TODO
          return null;
        }
        if (totalCount == 0) // 初回
        {
          if (response.Headers.Contains("Total-Count") 
           && int.TryParse(response.Headers.GetValues("Total-Count").First(),
                                                      out int count))
          {
            totalCount = count;
          }
        }
        if (obj.Length > 0)
        {
          lstPages.AddRange(obj);
        }
      }
      else
      {
        // エラー発生 - TODO
        return null;
      }
      // 残りがあるか?
      if (totalCount <= (page++ * limit))
      {
        break;
      }
    }
  }
  return lstPages;
}

// Qiita 統計情報 JSON
// .NET Framework の場合、title に対する required 指定は不要
public class ResponseQiita
{
  // 利用する項目のみ定義
  public required string title { get; set; }   // タイトル
  public int? page_views_count { get; set; }   // ページビュー
  public int? likes_count { get; set; }        // Like数
  public int? stocks_count { get; set; }       // Skocks数
}
#endregion

Zenn

/api/articles を用いて、「タイトル」「ライク数」「ストック数(ブックマーク数)」を取得します。

#region Zenn
// Zenn API - /api/articles
// .NET Framework 時は null 許容参照型の明示 `?` を削除してください
private async Task<List<ResponseZennStats>?> ZennGetPageList()
{
  // IHttpClientFactory で QiitaClient という名前の HttpClient 取得
  var factory = MyServiceProvider?.GetService<IHttpClientFactory>();
  var httpClient = factory?.CreateClient("ZennClient");
  if (httpClient == null)
  {
    // エラー発生 - TODO
    return null;
  }

  string baseUrl = "/api/articles";
  var lstPages = new List<ResponseZennStats>();
  int? page = 1;        // ループ回数(1~)

  // next_page = null となるまでのループ処理とする
  while (true)
  {
    string targetUrl = baseUrl + $"?username={ZennUsername}&page={page}";
    using (var request = new HttpRequestMessage(HttpMethod.Get, targetUrl))
    using (var response = await httpClient.SendAsync(request))
    {
      if (response?.IsSuccessStatusCode == true)
      {
        var content = response.Content.ReadAsStringAsync().Result;
        var obj = System.Text.Json.JsonSerializer
                     .Deserialize<ResponseZenn>(content);
        if (obj == null)
        {
          // エラー発生 - TODO
          return null;
        }
        if (obj.articles?.Count > 0)
        {
          lstPages.AddRange(obj.articles);
        }
        if (obj.next_page != null)
        {
          page = obj.next_page;
        }
        else
        {
          // End Of Data
          return lstPages;
        }
      }
      else
      {
        // エラー発生 - TODO
        return null;
      }
    }
  }
}

// Zenn 統計情報 JSON
// .NET Framework の場合、articles に対する null 許容参照型の明示 `?` は不要
// .NET Framework の場合、title に対する required 指定は不要
public class ResponseZenn
{
  // 利用する項目のみ定義
  public List<ResponseZennStats>? articles {  get; set; }
  public int? next_page { get; set; }       // 次ページ情報 (null時終端) 
}
public class ResponseZennStats
{
  // 利用する項目のみ定義
  public required string title { get; set; }   // タイトル
  public int? liked_count { get; set; }        // Like数
  public int? bookmarked_count { get; set; }   // Bookmark数
}
#endregion

統計情報の加工

Full Outer Join 結合

Qiita, Zenn 記事の統計情報は「タイトル」で結合します。
基本的にタイトルが一致していることを想定してますが、片方のみに存在するケースを考慮して、Full Outer Join で結合することにします。

#region FullOuterJoin
// Qiita と Zenn 記事統計情報を Full Outer Join
// .NET Framework 時は null 許容参照型の明示 `?` を削除してください
// TODO - DataGird/DataGridView などに出力
private async Task<IEnumerable<StatsJoin>?> FullOuterJoin()
{
  var qiita = await QiitaGetPageList();
  if (qiita == null || qiita.Count <= 0)
  {
    // エラー発生 - TODO
    return null;
  }
  var zenn = await ZennGetPageList();
  if (zenn == null || zenn.Count <= 0)
  {
    // エラー発生 - TODO
    return null;
  }

  // C# の LINQ には直接的な  構文は存在しませんが、
  // 左外部結合と右外部結合を組み合わせることで実現できます。

  // 左外部結合
  var leftJoin = from q in qiita
                 join z in zenn on q.title equals z.title into temp
                 from z in temp.DefaultIfEmpty()
                 select new StatsJoin
                 {
                   Title = q.title,
                   QiitaViews = q.page_views_count ?? 0,
                   QiitaLikes = q.likes_count ?? 0,
                   QiitaStocks = q.stocks_count ?? 0,
                   ZennLikes = z?.liked_count ?? 0,
                   ZennStocks = z?.bookmarked_count ?? 0
                 };
  // 右外部結合(左に存在しないものを抽出)
  var rightJoin = from z in zenn
                  join q in qiita on z.title equals q.title into temp
                  from q in temp.DefaultIfEmpty()
                  where q == null
                  select new StatsJoin
                  {
                    Title = z.title,
                    QiitaViews = 0,
                    QiitaLikes = 0,
                    QiitaStocks = 0,
                    ZennLikes = z.liked_count ?? 0,
                    ZennStocks = z.bookmarked_count ?? 0
                  };

  // 結合
  var fullOuterJoin = leftJoin.Concat(rightJoin);
  return fullOuterJoin;
}

// 結合した統計情報
// .NET Framework の場合、Title に対する required 指定は不要
public class StatsJoin
{
  public required string Title { get; set; }
  // Qiita 統計
  public int QiitaViews { get; set; }
  public int QiitaLikes { get; set; }
  public int QiitaStocks { get; set; }
  // Zenn 統計
  public int ZennLikes { get; set; }
  public int ZennStocks { get; set; }
}
#endregion

ツール仕立て

ここまでは、「テスト環境」に記載した環境で動作確認しましたが、ツール仕立ては、それぞれの好みもあるので、特定の環境で構築したツールのキャプションとソースのみを掲載します。
ここでは、Windows Forms - .Net 8 で、QiitaLikes, ZennLikes, QiitaViews でソートした結果を DataGirdView で表示するツールとして構築してみました。
※ chai0917 - 2025/10/01 時点の情報です。

Stats.png

サンプルソースを以下に掲載しておきます。

public Form1()
{
  InitializeComponent();

  // DataGridView 初期化
  DataGridView_Initialize();

  // ServiceProvider 初期化
  ServiceProvider_Initialize();

  btnAction.Click += btnAction_Click;
}
#region DataGirdView
// DataGridView 初期化
private void DataGridView_Initialize()
{
  dataGridView1.AutoGenerateColumns = false;
  dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;  // 行選択モード
  dataGridView1.MultiSelect = false;             // 複数選択無効
  dataGridView1.ColumnHeadersVisible = true;     // 列ヘッダ表示
  dataGridView1.RowHeadersVisible = false;       // 行ヘッダ非表示
  dataGridView1.AllowUserToDeleteRows = false;   // 削除キーで削除を無効
  dataGridView1.AllowUserToAddRows = false;      // 末尾行での行追加を無効
  dataGridView1.ScrollBars = ScrollBars.Both;
  dataGridView1.ReadOnly = true;

  dataGridView1.Columns.AddRange(new DataGridViewColumn[]
  {
    new DataGridViewTextBoxColumn
    {
      Name = "Title", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
    },
    new DataGridViewTextBoxColumn
    {
      Name = "QiitaViews", AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
      DefaultCellStyle = new DataGridViewCellStyle {
          Alignment = DataGridViewContentAlignment.MiddleRight }
    },
    new DataGridViewTextBoxColumn
    {
      Name = "QiitaLikes", AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
      DefaultCellStyle = new DataGridViewCellStyle {
          Alignment = DataGridViewContentAlignment.MiddleRight }
    },
    new DataGridViewTextBoxColumn
    {
      Name = "QiitaStocks", AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
      DefaultCellStyle = new DataGridViewCellStyle {
          Alignment = DataGridViewContentAlignment.MiddleRight }
    },
    new DataGridViewTextBoxColumn
    {
      Name = "ZennLikes", AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
      DefaultCellStyle = new DataGridViewCellStyle {
          Alignment = DataGridViewContentAlignment.MiddleRight }
    },
    new DataGridViewTextBoxColumn
    {
      Name = "ZennStocks", AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader,
      DefaultCellStyle = new DataGridViewCellStyle {
          Alignment = DataGridViewContentAlignment.MiddleRight }
    }
  });
}
#endregion

#region DataGirDivew 表示
// .NET Framework 時は null 許容参照型の明示 `?` を削除してください
private async void btnAction_Click(object? sender, EventArgs e)
{
  dataGridView1.Rows.Clear();

  var stats = await FullOuterJoin();
  if (stats != null)
  {
    var sorted = stats.OrderByDescending(x => x.QiitaLikes)
                      .ThenByDescending(x => x.ZennLikes)
                      .ThenByDescending(x => x.QiitaViews);

    foreach (var page in sorted)
    {
      var row = new DataGridViewRow();
      row.CreateCells(dataGridView1);

      row.Cells[0].Value = page.Title;
      row.Cells[1].Value = page.QiitaViews;
      row.Cells[2].Value = page.QiitaLikes;
      row.Cells[3].Value = page.QiitaStocks;
      row.Cells[4].Value = page.ZennLikes;
      row.Cells[5].Value = page.ZennStocks;

      dataGridView1.Rows.Add(row);
    }
    MessageBox.Show("データ出力しました。");
  }
}
#endregion
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?