AI を活用した RSS フィード アグリゲーターの構築
Microsoft MVP でありテクノロジー愛好家である私は、Microsoft の DevBlog で公開されている素晴らしいコンテンツの海に常に溺れていることに気づきます。 .NET の発表から Visual Studio の更新、Azure のイノベーションからセマンティック カーネルの詳細まで、Microsoft エコシステムでは常に新しくてエキサイティングなことが起こっています。
問題? すべてを維持し続けることはほぼ不可能です。
最新の発表を常に把握してネットワークと共有したいと考えていましたが、7 つの異なる RSS フィードを手動でチェックし、記事を読み、魅力的なソーシャル メディア投稿を作成し、すでに共有した内容を追跡すること自体がフルタイムの仕事になりつつありました。毎朝、ブラウザの複数のタブを開いて何十もの記事に目を通し、どの記事をすでに共有したかを思い出し、注意を引いた記事についての投稿を書くのに貴重な時間を費やしていました。
そこで私は、開発者であれば誰もが行うことと同じことを行いました。それを自動化しました。
この包括的なガイドでは、新しいコンテンツの複数の Microsoft DevBlog RSS フィードを監視し、Azure OpenAI とセマンティック カーネルを使用して記事を分析して魅力的な投稿を生成し、分析された記事ごとに詳細なマークダウン ドキュメントを作成し、コンテンツを確認して共有できるように Telegram 経由で通知を送信し、投稿の重複を避けるためにすべてを追跡し、GitHub Actions 経由で自動的に実行する、AI を活用した RSS フィード アグリゲーターをどのように構築したかについて説明します。
このソリューションのあらゆる側面を詳しく見てみましょう。
このプロジェクトの裏話
情報過多と共存する
このツールを作成する前の私の典型的な朝の様子を描いてみましょう。私は朝起きてコーヒーを飲み、ラップトップを開いて Microsoft 開発者エコシステムの最新情報をチェックしました。まず、メインの DevBlogs サイトに移動して、重大な発表があるかどうかを確認します。次に、.NET ブログを特にチェックします。それが私の主要なテクノロジ スタックだからです。その後、AI の重要性がますます高まっているため、セマンティック カーネルのブログに移動します。 IDE の更新は日常のワークフローに大きな影響を与える可能性があるため、Visual Studio ブログがリストの次に挙げられました。次に、CI/CD および GitHub 関連のニュースに関する DevOps ブログ、クラウド インフラストラクチャの更新に関する All Things Azure ブログ、最後にデータベースのイノベーションに関する Azure SQL ブログが続きました。
チェックすべきフィードは 7 つあります。これらの各ブログは、週に複数の記事を公開します。.NET Conf や Build などの主要な発表期間には、1 日に複数の記事を公開することもあります。追跡し、読み、共有する必要がある記事は数十件になる可能性があります。ここで重要なのは、コミュニティと知識を共有することを重視する人間として、これらの記事をただ読むことだけを望んでいたわけではありません。最も価値のあるものを LinkedIn のネットワークと共有し、他の開発者も最新情報を入手できるようにしたいと考えました。しかし、LinkedIn で優れた投稿を作成するには時間がかかります。記事を徹底的に読み、重要なポイントを理解し、それが読者にとってなぜ重要なのかを考え、魅力的なフックを作成し、すべてを適切にフォーマットする必要があります。これに週に数本の記事を掛けると、作業時間になります。
私が本当に欲しかったもの
何か月もこの問題に対処した後、私は腰を据えて、理想的な解決策とはどのようなものかを考えました。何よりもまず、重要な発表を二度と見逃したくありませんでした。新しい記事が公開されるとすぐに、システムが自動的にそれをキャッチする必要があります。また、AI に魅力的な投稿の作成を支援してもらい、コンテンツ作成の時間を節約したいと考えていました。これは、私の声を完全に置き換えるのではなく、カスタマイズできる確かな出発点を提供するためでした。
一貫性も大きな要素でした。毎日手動で行うことを忘れずに、コンテンツを定期的に共有したいと考えていました。追跡の側面も重要でした。重複投稿やフォロワーの迷惑を避けるために、自分がすでに共有した内容を知る方法が必要でした。最後に、処理したすべての内容を永久に記録して整理整頓し、どのようなトピックを扱ったかを振り返って確認できるようにしたいと思いました。
ソリューションが形になる
私が思い描いていたソリューションは、GitHub Actions を使用して完全にハンズフリーでスケジュールに従って実行されるものです。ブラウザのタブを 1 つも開かなくても、7 つのフィードすべてが自動的に取得されます。 AI コンポーネントは実際にコンテンツを読んで理解し、視聴者にとって役立つ方法で要約します。投稿を一から書く必要がなくなり、すぐに共有できるソーシャル メディア コンテンツが作成され、必要に応じて調整できるようになります。すべてはレビューのために私の Telegram に送信されるので、携帯電話をざっと見て、何を共有するかを決めることができました。そしてもちろん、将来の参照のためにすべてを永久に記録します。
構築を始める前に
マシンに必要なもの
このチュートリアルを進めるには、開発マシンにいくつかのものがインストールされている必要があります。最も重要なものは、.NET SDK バージョン 9.0 以降です。これは私たちのランタイムであり、必要なすべてのビルド ツールを提供します。インストールしていない場合は、dot.net にアクセスして最新バージョンをダウンロードしてください。 Windows、macOS、または Linux ではインストールは簡単です。
バージョン管理のために Git もインストールする必要があります。コードを GitHub にプッシュし、自動化に GitHub Actions を使用するため、Git をローカルにセットアップすることが不可欠です。最新のバージョンであれば問題なく動作します。
開発環境としては、Visual Studio または VS Code をお勧めします。個人的には、最近ほとんどの仕事に VS Code を使用しています。VS Code は軽量であり、C# Dev Kit 拡張機能による優れた C# サポートを備えているからです。ただし、完全な Visual Studio の方が使い慣れている場合は、それも完璧に機能します。
必要なサービスとアカウントローカル ツール以外にも、いくつかのサービスのアカウントが必要になります。最も重要なものは、AI 分析を強化する Azure OpenAI です。これは従量課金制のサービスですが、この使用例ではコストは最小限で、分析される記事あたり数セントです。 Azure アカウントをお持ちでない場合は、クレジットを含む無料試用版にサインアップして開始できます。
通知には Telegram Bot を使用します。 Telegram の素晴らしい点は、ボット API が完全に無料で使用できることです。必要なだけボットを作成し、無制限にメッセージを送信できます。セットアッププロセスについては、このガイドの後半で説明します。
最後に、コードをホストし、GitHub Actions を実行するために GitHub アカウントが必要になります。このプロジェクトには無料利用枠で十分です。 GitHub では、プライベート リポジトリでは月あたり 2,000 分のアクション ランタイムが提供され、パブリック リポジトリでは無制限のアクション ランタイムが提供されます。
これを可能にするライブラリ
私たちのプロジェクトは 3 つの主要な NuGet パッケージに依存しており、それぞれが特定の目的を果たします。
1 つ目は HtmlAgilityPack で、.NET での HTML 解析のゴールド スタンダードです。ブログから記事を取得すると、ナビゲーション メニュー、フッター、広告、その他私たちが気にしないあらゆる種類の要素を含む、ページの完全な HTML が返されます。 HtmlAgilityPack を使用すると、その HTML を解析して、必要な記事コンテンツだけを抽出できます。
2 番目のパッケージは Microsoft.SemanticKernel で、AI モデルをアプリケーションに統合するための Microsoft の SDK です。これは、.NET コードと GPT-4 などの大規模な言語モデルの間の橋渡しであると考えてください。 API 呼び出し、トークン管理、応答解析の複雑さをすべて処理するため、AI に実際に実行してもらいたいことに集中できます。
3 番目のパッケージは System.ServiceModel.Syndication で、RSS フィードと Atom フィードを解析するための組み込みサポートを提供します。 RSS は古いテクノロジーのように思えるかもしれませんが、ブログやニュース サイトから構造化された更新情報を取得するための最良の方法であることに変わりはありません。このパッケージは、生の XML フィードを、扱いやすい厳密に型指定された C# オブジェクトに変換します。
アーキテクチャを理解する
ピースがどのように組み合わされるか
コードの説明に入る前に、すべてのコンポーネントがどのように連携して動作するかを説明しましょう。全体像を理解すると、実装の詳細がより明確になります。
最上位レベルには、オーケストレーターとして機能するメインの Program.cs ファイルがあります。これはアプリケーションのエントリ ポイントであり、他のすべてのコンポーネントを調整します。アプリケーションが実行されると、まず環境変数 (API キーや Telegram 認証情報など) から設定が読み込まれます。次に、7 つの Microsoft DevBlog ソースすべてから RSS フィードを取得します。これらのフィードを処理するときに、同じ記事が複数のフィードに表示される場合に対処するために記事の重複が排除されます。各記事を追跡ファイルと照合して、すでに処理されているかどうかを確認します。新しい記事の場合は、処理のために AI アナライザーに渡します。ArticleAnalyzer クラスは、AI の魔法が起こる場所です。このコンポーネントは記事を受け取り、それに対していくつかの処理を実行します。まず、記事の URL から完全な HTML コンテンツを取得します。次に、その HTML からクリーンなテキストを抽出し、不要なナビゲーション要素、スクリプト、スタイルをすべて削除します。クリーン テキストを取得すると、慎重に作成されたプロンプトとともにセマンティック カーネルを介してそれを Azure OpenAI に送信します。 AI は記事を分析し、概要、重要なトピック、関連性の説明、そして最も重要なことに、すぐに使用できる LinkedIn 投稿を含む構造化された応答を返します。アナライザーはこの応答を解析し、このすべての情報を含む ArticleAnalysis オブジェクトを返します。
MarkdownGenerator クラスは、ArticleAnalysis オブジェクトを取得し、その永続的なレコードを作成します。すべての記事のメタデータ、AI の分析、生成された投稿を含む、適切にフォーマットされたマークダウン ファイルが生成されます。これらのファイルは generated-posts ディレクトリに保存され、処理したすべてのものを検索可能なアーカイブとして保存できます。
最後に、Telegram 統合により、生成された投稿コンテンツが携帯電話に送信されます。これは、人間として AI の作業をレビューし、それを共有するかどうかを決定するポイントです。ボットは投稿コンテンツを含むメッセージを送信します。それを LinkedIn に直接コピーするか、最初に変更することができます。
データの流れ
新しい記事が .NET ブログに公開されると何が起こるかを説明します。ワークフローは、GitHub Actions がスケジュールに従って (たとえば 6 時間ごとに) アプリケーションをトリガーすると開始されます。アプリケーションが起動し、7 つすべての RSS フィードの取得を開始します。各フィードは、そのブログの最新の記事を含む XML ドキュメントを返します。
各フィードを解析すると、個々の記事が抽出され、リストに保存されます。ただし、ここには注意が必要です。DevBlog のメイン フィードには、個々のカテゴリ フィードにも表示される記事が含まれることがよくあります。したがって、「.NET 10」に関する記事は、メイン フィードと .NET 固有のフィードの両方に表示される可能性があります。これは、HashSet 内の URL を追跡することで処理され、重複が自動的に防止されます。
重複を除去した記事のリストを取得したら、フィルタリングして最近の記事のみに絞ります。通常は、ここ 1 日ほどに公開された記事です。以前の実行ですでに処理した古い記事は処理したくありません。次に、最近の記事を追跡ファイルと照合してチェックします。記事についてすでに処理して投稿している場合は、その記事をスキップします。
新しい記事ごとに、AI 分析パイプラインが開始されます。アナライザーは記事全体の HTML を取得し、クリーンアップして、プロンプトとともに GPT-4 に送信します。 AI は記事を読み取り、LinkedIn の投稿とともに包括的な分析を生成します。この分析を記録のためにマークダウン ファイルに保存します。分析が完了したら、メッセージをフォーマットし、テレグラム経由で送信します。メッセージには、URL とハッシュタグが追加された生成された投稿コンテンツが含まれます。携帯電話で通知を受け取り、投稿を確認し、気に入ったものがあれば、数回タップするだけでコピーして LinkedIn で共有できます。
最後に、追跡ファイルを更新してこの記事を処理済みとしてマークします。これにより、今後の実行では再び処理されなくなります。ファイルが作成または変更された場合、GitHub Actions はこれらの変更をリポジトリにコミットして、すべての同期を維持します。
プロジェクトを最初からセットアップする
ソリューション構造の作成
構築を始めましょう。ターミナルを開き、プロジェクトを作成する場所に移動します。私はプロジェクトを Development フォルダーに整理しておくのが好きですが、自分にとって都合の良い場所にどこにでも置くことができます。
まず、新しいソリューション ファイルを作成します。 .NET では、ソリューションは複数のプロジェクトを保持できるコンテナです。今のところプロジェクトは 1 つだけですが、ソリューションから始めると、後で必要に応じてプロジェクトを追加するのが簡単になります。コマンド dotnet new sln -n vs-feed-linkedin を実行して、vs-feed-linkedin という名前のソリューションを作成します。
次に、コンソール アプリケーション プロジェクトを作成する必要があります。整理しておくために、これを src サブディレクトリに置きます。 dotnet new console -n VsFeedLinkedin -o src を実行して、src フォルダーに VsFeedLinkedin という名前のコンソール プロジェクトを作成します。次に、dotnet sln add src/VsFeedLinkedin.csproj を使用してこのプロジェクトをソリューションに追加します。
cd src を使用して src ディレクトリに移動します。ここで NuGet パッケージを追加し、開発のほとんどを行います。
必要なパッケージの追加
プロジェクトを作成したら、前に述べた 3 つの NuGet パッケージを追加する必要があります。これらの各コマンドを順番に実行します。
dotnet add package System.ServiceModel.Syndication --version 9.0.9
dotnet add package Microsoft.SemanticKernel --version 1.30.0
dotnet add package HtmlAgilityPack --version 1.11.72
これらのコマンドを実行すると、プロジェクト ファイルは次のようになります。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.30.0" />
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.9" />
</ItemGroup>
</Project>
プロジェクト ファイルは、.NET 9.0 をターゲットとして、暗黙的な using や null 許容参照型などの最新の C# 機能を使用して、実行可能ファイル (OutputType は Exe) を構築していることを .NET に伝えます。 ItemGroup セクションには、3 つのパッケージの依存関係とその正確なバージョンがリストされています。
RSS フィードを詳しく見る
RSS とは何ですか?
フィードを取得するコードを書き始める前に、何を扱うのかを必ず理解してください。 RSS は Really Simple Syndication の略で、コンテンツ更新を配布するための標準化された XML 形式です。アイデアはシンプルです。ユーザーに新しいコンテンツがあるかどうかを確認するために Web サイトにアクセスするよう要求する代わりに、最近のコンテンツをリストした機械可読ファイルを公開します。アプリケーションはこのファイルを定期的にポーリングして、新しい記事を検出できます。
RSS は 1990 年代後半から 2000 年代初頭にかけて存在しました。時代遅れのテクノロジーだと思うかもしれませんが、実際には今でも、特にブログ、ニュース サイト、ポッドキャストなどで広く使用されています。 RSS の利点はそのシンプルさです。これは定義された構造を持つ単なる XML であり、どのアプリケーションでも解析できます。
DevBlogs フィードの構造Microsoft DevBlog から RSS フィードをフェッチすると、特定の構造に従った XML ドキュメントが返されます。最上位には、単一のチャネル要素を含む rss 要素があります。チャネルはブログ自体を表し、ブログのタイトル、URL、説明などのメタデータが含まれます。
チャネル内には複数の item 要素があり、それぞれが個々のブログ投稿を表します。各項目には、タイトル (記事の見出し)、リンク (記事全文を読める URL)、pubDate (記事の公開日)、dc:creator 要素 (著者の名前)、1 つ以上の category 要素 (記事のタグ)、説明 (通常は記事の概要または抜粋) が含まれます。
これがどのようなものかを単純化した例を次に示します。
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>.NET Blog</title>
<link>https://devblogs.microsoft.com/dotnet</link>
<description>The latest news about .NET</description>
<item>
<title>Announcing .NET 10</title>
<link>https://devblogs.microsoft.com/dotnet/announcing-dotnet-10</link>
<pubDate>Mon, 10 Dec 2025 12:00:00 GMT</pubDate>
<dc:creator>Microsoft</dc:creator>
<category>Announcements</category>
<category>.NET</category>
<description>Article summary...</description>
</item>
</channel>
</rss>
.NET の System.ServiceModel.Syndication パッケージの優れた点は、これらすべてを解析してくれることです。 XML ノードを手動で移動したり、RSS のバージョンの違いを心配したりする必要はありません。フィードをロードして、厳密に型指定されたオブジェクトを取得するだけです。
私たちが監視する 7 つのフィード
私の実装では、7 つの異なる Microsoft DevBlogs フィードを監視しています。 devblogs.microsoft.com/feed にあるメインの DevBlogs フィードでは、Microsoft がすべての開発者ブログで公開しているすべての情報を幅広く見ることができます。 devblogs.microsoft.com/dotnet/feed にある .NET 固有のフィードは、特に .NET のリリース、機能、ベスト プラクティスに焦点を当てています。 devblogs.microsoft.com/semantic-kernel/feed のセマンティック カーネル フィードでは、AI のオーケストレーションと統合について説明しています。これは、AI が現代の開発の中心となるにつれてますます重要になっています。
Visual Studio フィード (devblogs.microsoft.com/visualstudio/feed) では、IDE の改善点と生産性機能に関する最新情報を入手できます。 devblogs.microsoft.com/devops/feed の DevOps フィードでは、Azure DevOps、GitHub、CI/CD のトピックがカバーされています。 devblogs.microsoft.com/all-things-azure/feed にある All Things Azure フィードは、クラウド サービスとアーキテクチャ パターンに焦点を当てています。最後に、devblogs.microsoft.com/azure-sql/feed にある Azure SQL フィードでは、データベースの革新と機能について説明しています。
なぜメイン フィードと個々のカテゴリ フィードの両方をチェックするのか疑問に思われるかもしれません。メイン フィードは幅を与えてくれます。私が知らないものも含め、Microsoft 開発者ブログの記事が表示されます。カテゴリ フィードは私に深みを与えてくれます。たとえ新しいコンテンツによって記事がメイン フィードから追い出されてしまったとしても、カテゴリ フィードにより、私の関心の中核となる分野で重要なものを見逃すことがなくなります。
RSS フェッチ ロジックの構築
コアフェッチ関数
それでは、コードを書いてみましょう。私たちのアプリケーションの基礎は、RSS フィードを取得して解析する機能です。これを処理する関数は次のとおりです。
static async Task<SyndicationFeed?> FetchRssFeedAsync(string url)
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", "VsFeedLinkedin/1.0");
var response = await httpClient.GetStringAsync(url);
using var stringReader = new StringReader(response);
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Parse,
MaxCharactersFromEntities = 1024
};
using var xmlReader = XmlReader.Create(stringReader, settings);
return SyndicationFeed.Load(xmlReader);
}
このコードが何をするのかを見ていきましょう。まず、HTTP リクエストを行うための .NET の組み込みクラスである HttpClient を作成します。一部のサーバーは自身を識別しないリクエストをブロックするため、User-Agent ヘッダーを設定します。サーバーが必要としない場合でも、これを設定することをお勧めします。次に、フィード URL に対して GET リクエストを実行し、応答を文字列として受け取ります。この文字列には、RSS フィードの生の XML が含まれています。
この XML を解析するには、応答文字列をラップする StringReader を作成し、いくつかの XmlReaderSettings を構成します。 DtdProcessing 設定は重要です。RSS フィードには、処理が必要な DTD (Document Type Definition) 宣言が含まれる場合があります。 MaxCharactersFromEntities 設定は、発生するエンティティ拡張の量を制限することで XML 爆弾攻撃を防ぐセキュリティ対策です。
最後に、これらの設定で XmlReader を作成し、SyndicationFeed.Load を使用して XML を解析して、厳密に型指定された SyndicationFeed オブジェクトにします。これにより、生の XML ナビゲーションではなく、優れた C# プロパティを通じてフィードのメタデータとそのすべてのアイテムにアクセスできるようになります。
エラー処理を使用した複数のフィードの取得
現実の世界では、ネットワーク リクエストは失敗します。サーバーがダウンし、接続がタイムアウトになり、XML の形式が不正になる可能性があります。私たちはこうしたケースを丁寧に処理する必要があります。障害に強く、すべてのフィードを取得する方法は次のとおりです。
var allArticles = new List<(SyndicationItem item, string feedUrl)>();
var seenUrls = new HashSet<string>();
foreach (var feedUrl in feedUrls)
{
try
{
Console.WriteLine($" 📡 Fetching {feedUrl}...");
var feed = await FetchRssFeedAsync(feedUrl);
if (feed?.Items != null && feed.Items.Any())
{
foreach (var item in feed.Items)
{
var itemUrl = item.Links.FirstOrDefault()?.Uri.ToString() ?? "";
if (!string.IsNullOrEmpty(itemUrl) && seenUrls.Add(itemUrl))
{
allArticles.Add((item, feedUrl));
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($" ⚠️ Failed to fetch {feedUrl}: {ex.Message}");
}
}
ここでは 2 つのコレクションを管理しています。 allArticles リストには、見つかったすべての記事が、どのフィードから来たのかとともに保持されます。 seenUrls HashSet は、すでに見た記事の URL を追跡し、重複を避けるのに役立ちます。
各フィード URL をループし、取得操作を try-catch ブロックでラップします。特定のフィードの取得が失敗した場合 (おそらくサーバーが一時的にダウンしている可能性があります)、警告をログに記録し、次のフィードを続行します。このようにすると、1 つのフィードに問題があっても、他のフィードの処理が妨げられることはありません。
正常にフェッチされたフィードごとに、その項目を反復処理します。アイテムのリンク コレクションから記事の URL を抽出します。 URL がすでにセット内にある場合、HashSet.Add メソッドは false を返します。これは重複排除ロジックに最適です。記事が新しい場合にのみリストに追加します。
各記事の横にフィード URL を保存します。これは、この情報が後で役立つ可能性があるためです。たとえば、デバッグやログ記録の目的で、記事がどの特定のフィードから来たのかを知りたい場合があります。
重複の処理と状態の追跡
重複排除の課題
前に述べたように、Microsoft DevBlogs には階層的なフィード構造があり、これが興味深い課題を生み出しています。 .NET チームのメンバーが、たとえば .NET 10 のパフォーマンス向上に関する記事を公開すると、その記事はメインの DevBlogs フィードと .NET 固有のフィードの両方に表示される可能性があります。場合によっては、IDE 機能に関連する場合、Visual Studio フィードに表示されることもあります。
すべてのフィードのすべての記事を単純に処理すると、同じ記事を何度も分析して投稿することになります。これにより、Azure OpenAI への API 呼び出しが無駄になり、Telegram に重複した通知がスパム送信され、重複した通知を投稿するとフォロワーが迷惑する可能性があります。解決策は URL ベースの重複排除です。各記事には固有の URL があるため、それを識別子として使用できます。 HashSet データ構造は、O(1) の検索時間を提供し、重複を自動的に防止するため、これに最適です。すでにセット内にある URL を追加しようとすると、Add メソッドは単に false を返し、その記事をスキップする必要があることを知らせます。
マークダウンを使用した永続的な状態
重複排除は 1 回の実行内で重複を処理しますが、複数の実行ではどうなるでしょうか?アプリケーションが 6 時間ごとに実行される場合、どの記事がすでに処理されたかを記憶し、再度処理しないようにする必要があります。
この状態を posted-articles.md というマークダウン ファイルに保存することにしました。なぜマークダウンなのか?理由はいくつかあります。まず、人間が判読できるという点です。ファイルを開いて、どの記事を共有したかをすぐに確認できます。 2 番目に、バージョン管理されています。このファイルは Git リポジトリに存在するため、記事が処理されたときの完全な履歴がわかります。第三に、ドキュメントとしての役割を果たします。リポジトリを見れば誰でもアプリケーションが何をしたかを確認できます。
このファイルの形式は単純です。これには、ヘッダー、アプリケーションが最後に実行された時間を示すタイムスタンプ、そしてマークダウン リンク形式の記事のリストが含まれています。
# Posted Articles
*Last run: 2025-12-10 15:30:00*
List of articles posted to LinkedIn:
- [Announcing .NET 10](https://devblogs.microsoft.com/dotnet/announcing-dotnet-10?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-10 15:30:00 (Published: 2025-12-10)
- [Visual Studio 2026 Preview](https://devblogs.microsoft.com/visualstudio/vs-2026-preview?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-09 10:15:00 (Published: 2025-12-09)
トラッキング ファイルのロードと解析
記事がすでに処理されているかどうかを確認するには、このファイルをロードして URL を抽出する必要があります。これを行う関数は次のとおりです。
static HashSet<string> LoadPostedArticles(string filePath)
{
var postedUrls = new HashSet<string>();
if (!File.Exists(filePath))
{
return postedUrls;
}
var lines = File.ReadAllLines(filePath);
foreach (var line in lines)
{
var match = System.Text.RegularExpressions.Regex.Match(line, @"\(([^)]+)\)");
if (match.Success)
{
var url = match.Groups[1].Value;
if (url.Contains("?wt.mc_id="))
{
url = url.Substring(0, url.IndexOf("?wt.mc_id="));
}
else if (url.Contains("?"))
{
url = url.Substring(0, url.IndexOf("?"));
}
url = url.TrimEnd('/');
postedUrls.Add(url);
}
}
return postedUrls;
}
この関数は、すでに処理したすべての URL を含む HashSet を返します。まずファイルが存在するかどうかを確認します。最初の実行では存在しないため、空のセットを返します。
ファイル内の各行について、正規表現を使用してマークダウン リンク形式から URL を抽出します。正規表現 \(([^)]+)\) は、マークダウン リンクが URL を保存する括弧内のすべてのものと一致します。
次に、URL の正規化という重要なステップが始まります。同じ記事の URL の形式が異なる場合があります。 RSS フィードには https://devblogs.microsoft.com/dotnet/article が表示される場合がありますが、保存されたバージョンには追跡パラメーターが追加されています: https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972。 URL には末尾にスラッシュが付いているものと、付いていないものがあります。
これを処理するには、クエリ パラメータ (? 以降のすべて) を削除し、末尾のスラッシュを削除します。この正規化により、URL が表面的に異なる場合でも記事が重複していると認識されるようになります。
新しい記事を保存する
記事が正常に処理されたら、それを追跡ファイルに追加する必要があります。
static void SavePostedArticle(string filePath, string url, string title, DateTimeOffset publishDate)
{
var markdownEntry = $"- [{title}]({url}) - Posted on {DateTime.Now:yyyy-MM-dd HH:mm:ss} (Published: {publishDate:yyyy-MM-dd})\n";
if (!File.Exists(filePath))
{
File.WriteAllText(filePath, "# Posted Articles\n\n*Last run: {DateTime.Now:yyyy-MM-dd HH:mm:ss}*\n\nList of articles posted:\n\n");
}
File.AppendAllText(filePath, markdownEntry);
}
この関数は、記事のタイトルをリンクとして付けたマークダウン形式のエントリを作成し、その後に記事を投稿したときと最初に公開したときを示すタイムスタンプが続きます。ファイルがまだ存在しない場合は、最初にヘッダーを付けてファイルを作成します。
AI 分析エンジン
セマンティック カーネルを理解するここで、アプリケーションの最もエキサイティングな部分である AI 分析に進みます。 Semantic Kernel は、大規模な言語モデルをアプリケーションに統合するための Microsoft のオープンソース SDK です。これは単なる API 呼び出しのラッパーではありません。プラグイン、プランナー、メモリなどの機能を備えた高度な AI アプリケーションを構築するためのフレームワークを提供します。
このユースケースでは、セマンティック カーネルのチャット完了機能を使用しています。 Azure OpenAI にプロンプトを送信すると、モデルが記事を分析して応答を生成します。セマンティック カーネルは、複雑な API 認証、リクエストのフォーマット、レスポンスの解析をすべて処理します。
記事アナライザーのセットアップ
アナライザー クラスを設定する方法を見てみましょう。
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using HtmlAgilityPack;
namespace VsFeedLinkedin.Services;
public class ArticleAnalyzer
{
private readonly Kernel _kernel;
private readonly IChatCompletionService _chatService;
public ArticleAnalyzer(string endpoint, string apiKey, string deploymentName)
{
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: deploymentName,
endpoint: endpoint,
apiKey: apiKey
);
_kernel = builder.Build();
_chatService = _kernel.GetRequiredService<IChatCompletionService>();
}
セマンティック カーネルは、構成にビルダー パターンを使用します。 KernelBuilder を作成し、必要な資格情報を備えた Azure OpenAI チャット完了サービスを追加して、カーネルを構築します。構築されたカーネルから、プロンプトの送信と応答の受信に使用する IChatCompletionService インターフェイスを取得します。
コンストラクターは、Azure OpenAI エンドポイント (https://your-resource.openai.azure.com/ など)、API キー、デプロイメント名 (gpt-4o など) の 3 つのパラメーターを取ります。これらは環境変数から渡され、認証情報を安全に保ちます。
完璧なプロンプトを作成する
AI に送信するプロンプトは非常に重要です。適切に作成されたプロンプトにより、一貫した高品質の出力が生成されます。プロンプトが曖昧だったり、構造が不十分だったりすると、一貫性のない平凡な結果が生じます。満足のいく出力を得るために、このプロンプトを繰り返すのにかなりの時間を費やしました。
var prompt = $"""
You are a professional tech content analyst and LinkedIn content creator.
Analyze the following Microsoft DevBlogs article and create an engaging LinkedIn post.
Article Title: {title}
Author: {author}
URL: {url}
Tags: {string.Join(", ", tags)}
Article Content:
{cleanContent}
Please provide:
1. A brief summary (2-3 sentences) of the key points
2. The main technologies or topics covered
3. Why this is relevant for developers/tech professionals
4. An engaging LinkedIn post (max 1300 characters) that:
- Starts with a hook or attention-grabbing statement
- Highlights the key value for readers
- Includes a call to action
- Uses appropriate emojis (but not too many)
- Maintains a professional yet approachable tone
- DO NOT include hashtags in the post (they will be added separately)
- DO NOT include the URL in the post (it will be added separately)
Format your response as follows:
## Summary
[Your summary here]
## Key Topics
[List of main topics/technologies]
## Relevance
[Why this matters]
## LinkedIn Post
[Your engaging LinkedIn post here]
""";
ここで設計上の決定について説明しましょう。まずは AI に「あなたはプロのテクノロジー コンテンツ アナリストであり、LinkedIn コンテンツ クリエイターです」という明確な役割を与えることから始めます。これにより、モデルは適切なスタイルと音声で応答できるようになります。
AI が必要とするすべてのコンテキスト (記事のタイトル、著者、URL、RSS フィードのタグ、記事の全コンテンツ) を提供します。提供するコンテキストが多ければ多いほど、分析はより適切になります。
次に、何を返したいかを正確に指定します。要約、重要なトピック、関連性の説明、LinkedIn への投稿の 4 つをお願いします。特に LinkedIn の投稿については、良い投稿の条件について詳しく説明します。フックがあり、価値が強調され、行動喚起が含まれ、絵文字が適切に使用され、プロフェッショナルな口調が維持される必要があります。
否定的な指示も同様に重要です。投稿にハッシュタグや URL を含めないよう AI に明示的に指示します。なぜ?これらを個別に追加するため、AI がそれらを含めると重複が発生するためです。この種の明示的な指示により、よくある間違いを防ぐことができます。
最後に、正確な出力形式を指定します。 ## ヘッダーでマークされたセクションを要求することで、プログラムによる応答の解析が容易になります。 AI はフォーマット指示に従うのが非常に得意で、この一貫性により解析コードがよりシンプルかつ信頼性の高いものになります。
分析の実行
すべてをまとめると次のようになります。
public async Task<ArticleAnalysis> AnalyzeArticleAsync(
string title,
string url,
string htmlContent,
string author,
List<string> tags)
{
var cleanContent = ExtractTextFromHtml(htmlContent);
if (cleanContent.Length > 8000)
{
cleanContent = cleanContent.Substring(0, 8000) + "...";
}
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage(prompt);
var response = await _chatService.GetChatMessageContentAsync(chatHistory);
var responseText = response.Content ?? "";
return ParseAnalysisResponse(responseText, title, url, author, tags);
}
```まず、HTML コンテンツからクリーン テキストを抽出します (これについては次のセクションで説明します)。次に、コンテンツが長すぎる場合は切り捨てます。大規模な言語モデルにはトークン制限があり、非常に長い記事はトークン制限を超える可能性があります。文字数を 8,000 文字に制限することで、実質的なコンテキストを提供しながら制限内に収まることを保証します。
ChatHistory オブジェクトを作成し、プロンプトをユーザー メッセージとして追加します。これは、チャットベースの対話のためのセマンティック カーネルの抽象化です。これをチャット完了サービスに送信し、応答を返します。最後に、応答を解析して個々のセクションを抽出します。
### AI 応答の解析
AI は、要求された構造でフォーマットされたテキストとして応答を返します。これを個々のフィールドに解析する必要があります。
```csharp
private static ArticleAnalysis ParseAnalysisResponse(
string response,
string title,
string url,
string author,
List<string> tags)
{
var analysis = new ArticleAnalysis
{
Title = title,
Url = url,
Author = author,
Tags = tags,
RawAnalysis = response
};
var sections = response.Split("##", StringSplitOptions.RemoveEmptyEntries);
foreach (var section in sections)
{
var lines = section.Trim().Split('\n', 2);
if (lines.Length < 2) continue;
var sectionTitle = lines[0].Trim().ToLower();
var sectionContent = lines[1].Trim();
switch (sectionTitle)
{
case "summary":
analysis.Summary = sectionContent;
break;
case "key topics":
analysis.KeyTopics = sectionContent;
break;
case "relevance":
analysis.Relevance = sectionContent;
break;
case "linkedin post":
analysis.LinkedInPost = sectionContent;
break;
}
}
return analysis;
}
応答を ## マーカーで分割し、各セクションを取得します。各セクションは改行で分割され、ヘッダーとコンテンツが分離されます。次に、switch ステートメントを使用して、各セクションのコンテンツを適切なプロパティに割り当てます。
解析されていない生の応答も保存します。これはデバッグに役立ちます。解析で何か問題が発生した場合、AI が実際に何を返したかを確認できます。
HTML からのコンテンツの抽出
HTML をクリーンアップする必要がある理由
ブログから記事を取得すると、ページの完全な HTML が取得されます。これには記事のコンテンツだけではなく、ナビゲーション メニュー、ヘッダー、フッター、サイドバー、関連記事ウィジェット、コメント セクション、分析と追跡用のスクリプト、スタイルシート、その他あらゆる種類の要素が含まれます。
これらすべてを AI に送信すると、いくつかの悪いことが起こるでしょう。 AI は大量の無関係なテキストを処理する必要があり、トークンが無駄になり、分析が混乱する可能性があります。ナビゲーションとフッターのテキストが概要に含まれる場合があります。スクリプトと CSS はコンテンツとして扱われるため、分析がさらに汚染されてしまいます。
記事のコンテンツ、つまり人間の読者が実際に読む部分だけを抽出する必要があります。
HtmlAgilityPack の使用
HtmlAgilityPack は、.NET 用の堅牢な HTML 解析ライブラリです。 XML とは異なり、HTML は不正な形式であることがよくあります。タグが適切に閉じられていなかったり、属性が正しく引用されていなかったりする可能性があります。 HtmlAgilityPack はこれらすべてを適切に処理し、クエリと操作が可能な DOM のような構造を提供します。
抽出関数は次のとおりです。
private static string ExtractTextFromHtml(string html)
{
if (string.IsNullOrWhiteSpace(html))
return string.Empty;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var nodesToRemove = doc.DocumentNode.SelectNodes("//script|//style|//nav|//footer|//header");
if (nodesToRemove != null)
{
foreach (var node in nodesToRemove)
{
node.Remove();
}
}
var text = doc.DocumentNode.InnerText;
text = System.Text.RegularExpressions.Regex.Replace(text, @"\s+", " ");
return text.Trim();
}
HTML を HtmlDocument にロードし、HTML をツリー構造に解析します。次に、XPath を使用して、削除するすべてのノードを選択します。 XPath 式 //script|//style|//nav|//footer|//header は、すべてのスクリプト要素 (不要な JavaScript コード)、スタイル要素 (不要な CSS)、nav 要素 (ナビゲーション メニュー)、フッター要素、およびヘッダー要素を選択します。
これらのノードを削除すると、HTML タグを削除しながらすべてのテキスト コンテンツを抽出する InnerText プロパティを取得します。これにより、記事のプレーンテキストが得られます。最後に、空白を削除します。 HTML には、複数のスペース、タブ、改行など、書式設定のための余分な空白が多く含まれていることがよくあります。正規表現を使用して一連の空白文字を単一のスペースに置き換え、結果をトリミングします。
記事全文の取得
RSS フィードでは概要のみが提供され、記事の全内容は提供されません。完全なテキストを取得するには、記事の Web ページを取得する必要があります。
public static async Task<string> FetchArticleContentAsync(string url)
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", "VsFeedLinkedin/1.0");
try
{
return await httpClient.GetStringAsync(url);
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ Failed to fetch article content: {ex.Message}");
return string.Empty;
}
}
これは簡単です。記事の URL に対して HTTP GET リクエストを作成し、HTML レスポンスを返します。ネットワークリクエストは失敗する可能性があり、アプリケーション全体をクラッシュさせるよりは空の文字列を返したいため、これを try-catch でラップします。
永続的なドキュメントの作成
マークダウン ファイルを生成する理由
記事を分析するたびに、その分析を文書化した詳細なマークダウン ファイルが生成されます。これにはいくつかの目的があります。
まず、検索可能なアーカイブを作成します。時間が経つにつれて、分析された記事のコレクションが構築されます。これらのファイルを検索して、特定のトピックに関する過去のコンテンツを見つけることができます。
第二に、透明性が得られます。完全な分析や LinkedIn の投稿など、AI が各記事に対して何を生成したかを正確に確認できます。
第三に、デバッグに役立ちます。投稿で何か問題が発生した場合は、マークダウン ファイルを調べて何が起こったのかを理解できます。
マークダウン ジェネレーター クラス
public class MarkdownGenerator
{
private readonly string _outputDirectory;
public MarkdownGenerator(string outputDirectory)
{
_outputDirectory = outputDirectory;
if (!Directory.Exists(_outputDirectory))
{
Directory.CreateDirectory(_outputDirectory);
}
}
public string GenerateMarkdownFile(ArticleAnalysis analysis)
{
var sb = new StringBuilder();
var safeTitle = GenerateSafeFileName(analysis.Title);
var fileName = $"{analysis.AnalyzedAt:yyyy-MM-dd}_{safeTitle}.md";
var filePath = Path.Combine(_outputDirectory, fileName);
sb.AppendLine($"# {analysis.Title}");
sb.AppendLine();
sb.AppendLine("## Article Information");
sb.AppendLine();
sb.AppendLine($"- **Author:** {analysis.Author}");
sb.AppendLine($"- **URL:** [{analysis.Url}]({analysis.Url})");
sb.AppendLine($"- **Published:** {analysis.PublishDate:yyyy-MM-dd}");
sb.AppendLine($"- **Analyzed:** {analysis.AnalyzedAt:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"- **Tags:** {string.Join(", ", analysis.Tags)}");
sb.AppendLine();
sb.AppendLine("");
sb.AppendLine();
sb.AppendLine("## AI Analysis");
sb.AppendLine();
sb.AppendLine("### Summary");
sb.AppendLine();
sb.AppendLine(analysis.Summary);
sb.AppendLine();
sb.AppendLine("### Key Topics");
sb.AppendLine();
sb.AppendLine(analysis.KeyTopics);
sb.AppendLine();
sb.AppendLine("### Relevance for Developers");
sb.AppendLine();
sb.AppendLine(analysis.Relevance);
sb.AppendLine();
sb.AppendLine("");
sb.AppendLine();
sb.AppendLine("## Generated LinkedIn Post");
sb.AppendLine();
sb.AppendLine("```");
sb.AppendLine(分析.LinkedInPost);
sb.AppendLine("```");
sb.AppendLine();
sb.AppendLine("");
sb.AppendLine();
sb.AppendLine("*This analysis was generated using AI (Semantic Kernel with Azure OpenAI)*");
File.WriteAllText(filePath, sb.ToString());
return filePath;
}
コンストラクターは出力ディレクトリのパスを取得し、存在しない場合はそれを作成します。 GenerateMarkdownFile メソッドは ArticleAnalysis オブジェクトを受け取り、適切にフォーマットされたマークダウン ドキュメントを生成します。
ファイル名には、日付とタイトルのサニタイズされたバージョンが含まれます。これにより、ファイルを時系列に簡単に並べ替えて、一目で識別できるようになります。
安全でないファイル名の処理
記事のタイトルには、ファイル名に使用できない文字 (コロン、スラッシュ、疑問符、引用符など) を含めることができます。これらをサニタイズする必要があります。
private static string GenerateSafeFileName(string title)
{
var invalidChars = Path.GetInvalidFileNameChars();
var safeTitle = new string(title
.Where(c => !invalidChars.Contains(c))
.ToArray());
safeTitle = safeTitle.Replace(" ", "-").Replace("--", "-");
if (safeTitle.Length > 50)
{
safeTitle = safeTitle.Substring(0, 50);
}
return safeTitle.TrimEnd('-').ToLowerInvariant();
}
Path.GetInvalidFileNameChars() を使用して、現在のオペレーティング システムでファイル名に使用できない文字のリストを取得します。これらをフィルターで除外し、読みやすくするためにスペースをハイフンに置き換え、長さを 50 文字に制限し、一貫性を保つために小文字に変換します。
テレグラム通知の設定
私がテレグラムを選んだ理由
通知コンポーネントについては、電子メール、SMS、Slack、Discord、Telegram などのいくつかのオプションを検討しました。私が最終的に Telegram を選んだ理由はいくつかあります。
API は完全に無料で、適切な使用であればレート制限はありません。多くの通知サービスでは無料で送信できるメッセージの数に制限がありますが、Telegram はボット メッセージを個々のユーザーに制限しません。
ボット API は非常にシンプルです。これは JSON ペイロードを含む単なる HTTP リクエストです。複雑な認証フローや、基本的な機能に必要な Webhook はありません。Telegram は、携帯電話、デスクトップ、Web ブラウザなど、どこでも機能します。どこにいても通知を受信し、すぐに応答できます。
メッセージはリッチフォーマットをサポートします。太字テキスト、斜体、さらにはコード ブロックを使用して、通知を読みやすくすることができます。
Telegram ボットの作成
Telegram ボットのセットアップは驚くほど簡単です。 Telegram を開いて @BotFather を検索します。これは、ボットを作成および管理するための Telegram の公式ボットです。 BotFather との会話を開始し、コマンド /newbot を送信します。 BotFather は、ボットの名前 (これは表示名です) とユーザー名 (これは一意であり、「bot」で終わる必要があります) を尋ねます。これらを指定すると、BotFather がボットを作成し、API トークンを提供します。このトークンはパスワードのようなものです。秘密にして、パブリック リポジトリにコミットしないでください。
ボットがメッセージの送信先を認識できるようにチャット ID を見つけるには、チャット ID を検索して [開始] を押して、新しいボットとの会話を開始します。次に、ブラウザまたはカールを使用して URL https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates にアクセスします。応答で chat オブジェクトを探します。id フィールドはチャット ID です。
API 経由でメッセージを送信する
Telegram メッセージを送信するための関数は次のとおりです。
static async Task SendToTelegramAsync(string botToken, string chatId, string message)
{
using var httpClient = new HttpClient();
var telegramApiUrl = $"https://api.telegram.org/bot{botToken}/sendMessage";
var payload = new
{
chat_id = chatId,
text = message,
parse_mode = "HTML"
};
var jsonContent = JsonSerializer.Serialize(payload);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(telegramApiUrl, content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"Telegram API error: {response.StatusCode} - {errorContent}");
}
}
Telegram Bot API は REST ベースです。チャット ID (送信先)、メッセージ テキスト (送信内容)、およびオプションで解析モード (フォーマット用) を含む JSON 本文を使用して、sendMessage エンドポイントに対して POST リクエストを作成します。
parse_mode を「HTML」に設定すると、<b>bold</b> や <i>italic</i> などの基本的な HTML タグをメッセージ内で使用できるようになります。これにより通知が読みやすくなりますが、現在の使用例ではプレーン テキストを送信します。
リクエストが失敗した場合は、何が問題だったかの詳細を示す例外がスローされます。これは、何かが動作しない場合のデバッグに役立ちます。
アプリケーションの構成
環境変数
私たちのアプリケーションには、API キー、ボット トークン、エンドポイント URL など、いくつかの機密情報が必要です。これらをハードコーディングしたり、バージョン管理にコミットしたりしないでください。代わりに、アプリケーションが実行される各環境で安全に設定できる環境変数を使用します。
Telegram の場合、TELEGRAM_BOT_TOKEN (BotFather から与えられたトークン) と TELEGRAM_CHAT_ID (メッセージが送信されるチャット ID) が必要です。
Azure OpenAI の場合、AZURE_OPENAI_ENDPOINT (リソースの URL)、AZURE_OPENAI_API_KEY (API キー)、および AZURE_OPENAI_DEPLOYMENT (「gpt-4o」など、デプロイされたモデルの名前) が必要です。
コードで構成をロードする
アプリケーションの起動時にこれらの値をロードする方法は次のとおりです。
var telegramBotToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN");
var telegramChatId = Environment.GetEnvironmentVariable("TELEGRAM_CHAT_ID");
var azureOpenAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var azureOpenAiDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o";
var aiAnalysisEnabled = !string.IsNullOrWhiteSpace(azureOpenAiEndpoint) &&
!string.IsNullOrWhiteSpace(azureOpenAiKey);
各値を読み取るには、Environment.GetEnvironmentVariable を使用します。デプロイメント名には、値が設定されていない場合、デフォルトの「gpt-4o」が提供されます。
次に、エンドポイントと API キーの両方があることを確認して、AI 分析を有効にするかどうかを確認します。これにより、Azure OpenAI が構成されていない場合でも、アプリケーションは縮退モードで実行できます。AI 分析を行わずに、フィードを取得して記事を追跡します。### グレースフル デグラデーション
このグレースフル デグラデーションの概念は重要です。 1 つのオプション機能が構成されていないという理由だけでアプリケーションがクラッシュすることは望ましくありません。
ArticleAnalyzer? articleAnalyzer = null;
MarkdownGenerator? markdownGenerator = null;
if (aiAnalysisEnabled)
{
Console.WriteLine("🤖 AI Analysis enabled - Using Azure OpenAI with Semantic Kernel");
articleAnalyzer = new ArticleAnalyzer(azureOpenAiEndpoint!, azureOpenAiKey!, azureOpenAiDeployment);
markdownGenerator = new MarkdownGenerator(articlesOutputDir);
}
else
{
Console.WriteLine("ℹ️ AI Analysis disabled - Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY to enable");
}
AI が有効な場合は、アナライザーとマークダウン ジェネレーターを作成します。そうでない場合は、それらを null のままにし、処理中の AI 関連のステップをスキップします。 AI の機能強化がなくても、アプリケーションはフィードを取得し、基本的な通知を送信することで価値を提供します。
GitHub アクションによる自動化
GitHub アクションを使用する理由
このソリューションの真の力は自動化によってもたらされます。アプリケーションを数時間ごとに手動で実行するのではなく、バックグラウンドで自動的に実行したいのです。
GitHub Actions はこれに最適です。 GitHub に組み込まれているため、追加のサービスを設定する必要はありません。パブリック リポジトリは無料で、プライベート リポジトリには十分な無料時間が含まれています。スケジュールに基づいて実行でき、定期的にアプリケーションをトリガーします。 API キーを安全に保存するためのシークレット管理が組み込まれています。また、変更をリポジトリにコミットして、追跡ファイルを最新の状態に保つことができます。
ワークフロー ファイル
次の内容のファイルを .github/workflows/fetch-and-notify.yml に作成します。
name: Fetch DevBlogs and Notify
on:
schedule:
- cron: '0 */6 * * *'
workflow_dispatch:
jobs:
fetch-and-notify:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Restore dependencies
run: dotnet restore src/VsFeedLinkedin.csproj
- name: Build
run: dotnet build src/VsFeedLinkedin.csproj --no-restore
- name: Run application
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
run: dotnet run --project src/VsFeedLinkedin.csproj
- name: Commit and push changes
run: |
git config user.name "GitHub Actions Bot"
git config user.email "[email protected]"
if [[ -n $(git status --porcelain posted-articles.md generated-posts/) ]]; then
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
git add posted-articles.md generated-posts/
git commit -m "chore($TIMESTAMP): processed new articles"
git push
else
echo "No changes to commit"
fi
それぞれの部分について説明していきます。 on セクションでは、ワークフローをいつ実行するかを定義します。スケジュール トリガーは cron 構文を使用します。0 */6 * * * は「6 時間ごとの 0 分に」を意味します。したがって、ワークフローは UTC の午前 0 時、午前 6 時、正午、午後 6 時に実行されます。 workflow_dispatch トリガーを使用すると、GitHub UI から手動で実行できるため、テストに役立ちます。
ジョブは、Linux 仮想マシンである ubuntu-latest 上で実行されます。リポジトリをチェックアウトし、.NET 9 をセットアップし、NuGet パッケージを復元し、プロジェクトをビルドします。
アプリケーションの実行ステップでは魔法が起こります。 ${{ Secrets.SECRET_NAME }} 構文を使用して、シークレットを環境変数として渡します。これらのシークレットは GitHub に安全に保存され、ログに公開されることはありません。
最後に、変更をリポジトリにコミットします。ボット ID を使用して Git を構成し、追跡ファイルまたは生成された投稿ディレクトリに変更があるかどうかを確認し、変更がある場合はコミットを作成してプッシュします。
シークレットの設定
GitHub リポジトリにシークレットを追加するには、リポジトリの [設定]、[シークレットと変数]、[アクション] の順に移動します。 [新しいリポジトリ シークレット] をクリックし、各環境変数を追加します。名前は、ワークフロー ファイルで参照するものと正確に一致する必要があります。
まとめ
私たちが築いてきたもの
これまで取り上げてきたすべてを振り返ると、これまで面倒な手動プロセスであったものを自動化する、AI を活用した包括的な RSS フィード アグリゲーターを構築しました。このアプリケーションは 7 つの Microsoft DevBlog フィードを自動的に監視し、新しい記事が公開されるとすぐにそれをキャッチします。重複排除の複雑さを処理し、同じ記事が複数のフィードに表示されることを認識します。Semantic Kernel と Azure OpenAI を活用した AI 分析は、記事の内容を読んで理解し、要約の生成、重要なトピックの特定、関連性の説明をすべて自動的に行います。最も重要なことは、最小限の編集で共有できる魅力的な LinkedIn 投稿を作成できることです。
Telegram の統合により、レビューする新しいコンテンツがあるたびに携帯電話に通知が届きます。メッセージをざっと見て、共有するかどうかを決定し、すぐに行動することができます。
また、スケジュールに基づいて GitHub Actions 上で実行されるため、何もすることを覚えておく必要はありません。システムはバックグラウンドで動作しており、私は共有する価値のあるものがある場合にのみ関与します。
それを可能にしたテクノロジー
このプロジェクトでは、それぞれが重要な役割を果たすいくつかのテクノロジーを統合しました。 .NET 9 は、最新の言語機能と優れたパフォーマンスによる強固な基盤を提供しました。セマンティック カーネルは AI の統合を簡単にし、複雑な API 呼び出しと応答管理をすべて処理します。 Azure OpenAI は、技術的な内容を実際に理解して分析する機能であるインテリジェンスを提供しました。 HtmlAgilityPack は、Web ページからクリーン テキストを抽出するという厄介な問題を解決しました。 System.ServiceModel.Syndication により、RSS の解析が簡単になりました。 Telegram Bot API は無料で信頼性の高い通知を提供してくれました。そして、GitHub Actions は、自動化されたスケジュールされた実行によってすべてを結び付けました。
コストについて考える
あなたが抱く疑問の 1 つは、「これを実行するのにどれくらいのコストがかかるのか?」ということです。答えは「あまり多くはない」です。
Telegram は完全に無料です。ボットを介してメッセージを送信するのに料金はかかりません。
GitHub Actions はパブリック リポジトリでは無料です。プライベート リポジトリの場合、無料枠で毎月 2,000 分を利用できますが、これはこのユースケースには十分以上です。
Azure OpenAI は唯一の有料コンポーネントであり、コストは最小限です。 GPT-4o を使用すると、一般的なブログ記事の分析にかかる費用は 1 ~ 3 セントになります。たとえ月に数十の記事を処理しているとしても、AI コストは 1 ドル未満です。
次はどこに行けばよいでしょうか
このソリューションは私のニーズにはうまく機能しますが、拡張する方法はたくさんあります。複数のソーシャル プラットフォームのサポートを追加できます。LinkedIn に加えて Twitter/X、Mastodon、または Bluesky に投稿することもできます。センチメント分析を実装すると、長期にわたる記事の論調を追跡し、傾向を特定できます。フィードごとに異なるプロンプト テンプレートを許可し、トピックごとに異なるスタイルの投稿を生成できます。 Telegram を使用する代わりに、投稿を確認および管理するための Web ダッシュボードを構築することもできます。投稿されたコンテンツのエンゲージメント指標を追跡して、どのトピックが視聴者に最も共感を呼んだかを確認できます。
最終的な考えこのプロジェクトで私が最も気に入っている点は、私が強く信じている哲学を体現していることです。つまり、退屈な部分は自動化で処理し、創造的で意思決定の部分は人間に任せるべきです。システムは、取得、解析、分析、生成などの面倒な作業をすべて実行しますが、共有する前にすべてを確認します。 AI によって生成された投稿は、カスタマイズおよびパーソナライズできる出発点となります。
.NET、セマンティック カーネル、Azure OpenAI の力を組み合わせることで、品質と一貫性を維持しながら毎週何時間もの手作業を節約できるツールを作成しました。これは、日常生活に大きな変化をもたらす実用的な自動化です。
同様のものを作成したり、改善のアイデアがある場合は、ぜひお知らせください。 LinkedIn でお気軽にご連絡ください。
コーディングを楽しんでください、そしてメリークリスマス! 🎄