はじめに
現在、記事を 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 時点の情報です。
サンプルソースを以下に掲載しておきます。
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