LoginSignup
7
7

Qiitaへ記事を自動投稿するプログラムの作り方

Last updated at Posted at 2023-11-06

プロジェクトの概要

このプロジェクトでは、Qiitaに毎日自動的に記事を投稿するプログラムを作成します。この自動化プロセスを実現するために、GitHubのCI/CDツール、GitHub Actionsを活用します。

GitHubリポジトリ

使用技術

  • C#: .NET 6.0フレームワーク上で動作するアプリケーションを開発するための言語
  • .NET 6.0: クロスプラットフォーム対応のフレームワーク
  • Visual Studio Code (VSCode): 軽量で強力なソースコードエディタ
  • GitHub Actions: コードのビルド、テスト、デプロイを自動化するツール
  • Git: ソースコードのバージョン管理システム
  • Qiita API: Qiitaへの記事投稿を自動化するインターフェース
  • YAML: ワークフローを定義するためのデータ記述言語

前提条件

このプロジェクトを始めるにあたり、以下の条件を満たしている必要があります。

  • GitHubアカウントを持っていること
  • Qiitaアカウントを持っていること
  • Qiitaの個人用アクセストークンを発行していること
  • 基本的なGitの知識

プロジェクト構造

QiitaAutoPostProject/
│
├── .github/
│   └── workflows/
│       └── qiita_auto_post.yml  # GitHub Actionsのワークフロー定義
│
├── articles/
│   ├── unposted/                # 未投稿の記事を保存するディレクトリ
│   │   ├── Azure/
│   │   │   └── Azure1.md        
│   │   └── Git/
│   │       └── Git1.md          
│   └── posted/                  # 投稿済みの記事を保存するディレクトリ
│       ├── Azure/
│       └── Git/
├── src/
│   ├── QiitaAutoPost/
│   │   └── Program.cs           # メインの実行ファイル
│   └── QiitaAutoPost.csproj     # プロジェクトファイル
│
├── .gitignore                   # Gitの無視ファイルリスト
├── Qiita.sln                    
└── README.md                    # プロジェクトの説明や使い方を記述

ワークフロー解説

ステップ1: スケジュールの設定

GitHub Actionsのワークフローは、.github/workflows/qiita_auto_post.ymlに定義されたスケジュールに従って自動的に実行されます。

ステップ2: ワークフローの開始

設定された時間になると、GitHubはワークフローをトリガーし、自動化されたプロセスが開始されます。

ステップ3: 実行環境の準備

ワークフローが開始されると、GitHub Actionsはコードの実行に必要な環境を準備します。これには、.NET環境のセットアップや必要なツールやライブラリのインストールが含まれます。

ステップ4: プログラムのコンパイルと実行

ワークフローは、src/QiitaAutoPost/Program.csに記述されたプログラムをコンパイルし、実行可能な形式に変換します。コンパイルが完了すると、プログラムは実行され、記事の投稿プロセスが開始されます。

ステップ5: 記事の選択と投稿

プログラムはarticles/unposted/ディレクトリから未投稿の記事を選択し、QiitaのAPIを通じて投稿します。このプロセスは自動化されており、プログラムが適切な記事を選出し、Qiitaに投稿するまでの一連の動作を行います。

ステップ6: 投稿結果の確認

記事の投稿後、プログラムはその結果を確認し、成功した場合にはarticles/posted/ディレクトリへ移動します。このステップにより、投稿済みの記事が管理され、重複投稿を防ぐことができます。投稿の成功や失敗は、GitHub Actionsのログを通じて確認することができます。

設定手順

GitHubを通じてQiitaに毎日1記事を自動的に投稿する手順は以下のとおりです。

1. GitHubリポジトリの準備

  • 新しいGitHubリポジトリを作成し、articles/unposted/ディレクトリにQiitaに投稿する記事のMarkdownファイルを保存します。

2. QiitaのトークンをGitHub Secretsに設定

  • GitHubリポジトリの「Settings」にアクセスし、「Secrets」セクションで「New repository secret」をクリックします。
  • 「Name」フィールドにQIITA_TOKENと入力し、「Value」フィールドにQiitaの個人用アクセストークンを入力後、「Add secret」で保存します。

3. GitHub Actionsのワークフローを作成

  • リポジトリのルートに.github/workflowsディレクトリを作成し、qiita_auto_post.ymlという名前のワークフローファイルをこのディレクトリ内に作成します。
  • ワークフローファイルには、Qiitaに自動投稿するための設定を記述します。

4. プログラムの作成

  • src/QiitaAutoPost/Program.csにQiitaに投稿するためのプログラムを記述します。
  • QiitaのAPIを使用してMarkdownファイルの内容をQiitaに投稿するようにプログラムを設計します。

5. テストとデバッグ

  • 設定が完了したら、GitHubリポジトリの「Actions」タブからワークフローが正しく動作するかテストします。
  • エラーが発生した場合は、ワークフローのログを確認し、問題を解決します。

注意点

  • QiitaのAPI利用規約を遵守してください。
  • GitHub ActionsのスケジュールはUTC時間で設定されているため、地域に応じて時間を調整する必要があります。

各ディレクトリの解説

  • .github/workflows/: qiita_auto_post.ymlを含む、GitHub Actionsのワークフローを定義するファイルが置かれており、自動化タスクの設定がここで行われます。
qiita_auto_post.ymlの詳細解説

qiita_auto_post.yml

qiita_auto_post.yml
name: Qiita Auto Post

on: # ワークフローがいつ実行されるかを定義します。
  workflow_dispatch:
  schedule:
    - cron: '0 23 * * *'

jobs: # ワークフローで実行するジョブを定義します。
  post_to_qiita:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
        with:
          persist-credentials: false
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: '6.0'

      - name: Install dependencies
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Run Qiita post script
        run: dotnet run --project src/QiitaAutoPost.csproj --configuration Release --no-build --no-restore
        env:
          QIITA_TOKEN: ${{ secrets.QIITA_TOKEN }}

      - name: Commit changes
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add .
          git commit -m "Update articles status" || exit 0
          git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/yamazaki-hidekuni/Qiita.git
          git push
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ワークフローの構造

ワークフローは、onキーワードで始まるトリガーイベント、jobsセクションで定義されるジョブ、そしてジョブ内のstepsによって構成されます。
各ステップは、リポジトリのチェックアウト、環境のセットアップ、依存関係のインストール、ビルド、スクリプトの実行、変更のコミットとプッシュといった一連のタスクを自動的に実行します。
これにより、記事が自動的にQiitaに投稿されるというプロセスを実現できます。

ワークフローのトリガー: onセクション

ワークフローはonキーで定義されたイベントによってトリガーされます。以下の設定では、手動トリガーとスケジュールトリガーの両方が用意されています。

.yml
on:
  workflow_dispatch:
  schedule:
    - cron: '0 23 * * *'
  1. 手動トリガー (workflow_dispatch):
    workflow_dispatchはGitHubのWebインターフェースからワークフローを手動で実行できます。

  2. スケジュールトリガー (schedule):
    scheduleはワークフローを定期的に自動実行するために使用されます。cronスケジュールを使って定期的にワークフローを実行します。

スケジュールの設定

スケジュールはcron形式で設定され、以下のようになります。

schedule:
  - cron: '0 23 * * *'

この設定は、UTCで毎日23時0分にワークフローを実行することを意味します。日本はUTCよりも9時間進んでいるため、日本時間では朝8時0分に実行されることになります。

手動実行の手順

GitHubのWebインターフェースでworkflow_dispatchを使ってワークフローを手動で実行するには、以下の手順に従います。

  1. GitHubで対象のリポジトリにアクセスします。
  2. リポジトリの上部にある「Actions」タブをクリックします。
  3. 左側のサイドバーから実行したいワークフローを選択します。
  4. ワークフローの詳細ページの右上にある「Run workflow」ボタンをクリックします。
  5. 必要に応じてワークフローの入力を提供し、「Run workflow」をクリックして実行します。

ジョブの定義: jobsセクション

GitHub Actionsワークフローでは、jobs セクションを通じて、実行される一連のジョブを定義します。各ジョブは、特定のタスクやアクションの集合を表し、独立してまたは他のジョブと並行して実行されることができます。

.yml
jobs:  # ワークフローで実行するジョブを定義します。
  post_to_qiita: 
    runs-on: ubuntu-latest
  • jobs:
    このキーはワークフロー内で実行されるジョブを定義するために使用されます。ワークフローは複数のジョブを含むことができ、それぞれが独自のタスクを実行します。

  • post_to_qiita:
    このジョブの名前はpost_to_qiitaで、特定のタスクを実行するための設定が含まれています。

  • runs-on: ubuntu-latest
    runs-onキーはジョブが実行される仮想環境(ランナー)を指定します。
    ubuntu-latestはGitHubが提供する最新のUbuntuランナーを使用することを意味します。ランナーは、ワークフローのステップを実行するための仮想マシンまたはコンテナです。ubuntu-latestを指定することで、ジョブは最新の安定版Ubuntuオペレーティングシステム上で実行されます。

ステップの定義: steps セクション

ワークフロー内でstepsキーを使用すると、ジョブが実行される際に実行する一連のステップを定義できます。各ステップは、特定のタスクを実行するための命令やコマンドを含んでいます。

リポジトリのチェックアウト

最初のステップは、GitHubリポジトリのコードをGitHub Actionsランナーにチェックアウトすることです。これにはactions/checkout@v2アクションが使用され、persist-credentials: falseの設定を通じて、後続のステップで誤って認証情報が使用されないようにします。

steps:
  - name: Checkout repository
    uses: actions/checkout@v2
    with:
      persist-credentials: false

.NET環境のセットアップ

.NETプロジェクトをビルドする前に、必要な.NET環境をセットアップする必要があります。actions/setup-dotnet@v1アクションを使用して、指定されたバージョンの.NET SDKをインストールします。

  - name: Setup .NET
    uses: actions/setup-dotnet@v1
    with:
      dotnet-version: '6.0'

依存関係のインストール

プロジェクトの依存関係はdotnet restoreコマンドを使用してインストールされます。これにより、プロジェクトが依存する外部ライブラリやフレームワークがダウンロードされ、ビルドや実行に必要な準備が整います。

  - name: Install dependencies
    run: dotnet restore

プロジェクトのビルド

プロジェクトのビルドはdotnet buildコマンドによって行われます。--configuration Releaseオプションを使用して最適化されたコードを生成し、--no-restoreオプションで依存関係の復元をスキップします。

  - name: Build
    run: dotnet build --configuration Release --no-restore

Qiitaへの投稿スクリプトの実行

このステップでは、ビルド済みのアプリケーションをdotnet runコマンドを使って実行し、Qiitaに記事を自動投稿するスクリプトを起動します。src/QiitaAutoPost.csprojプロジェクトを指定し、Release構成で実行することで、最適化された環境下でスクリプトが動作します。--no-build--no-restoreオプションを使用することで、ビルドと依存関係の復元のステップを省略し、効率的に処理を進めます。QIITA_TOKEN環境変数は、QiitaのAPIに安全にアクセスするための認証トークンを提供します。

      - name: Run Qiita post script
        run: dotnet run --project src/QiitaAutoPost.csproj --configuration Release --no-build --no-restore
        env:
          QIITA_TOKEN: ${{ secrets.QIITA_TOKEN }}

ステップの詳細

  • name: Run Qiita post script
    このラベルは、GitHubのUI上でステップを簡単に識別できるように設定します。この名前から、ステップがQiitaへの投稿を行うスクリプトを実行する役割を持っていることが明確になります。

  • run: dotnet run --project src/QiitaAutoPost.csproj --configuration Release --no-build --no-restore
    実行されるコマンドは、ビルド済みの.NET Coreプロジェクトを起動します。ここで指定されたオプションは、プロジェクトのパス、ビルドの構成、そしてビルドと依存関係の復元をスキップするためのものです。

  • env: QIITA_TOKEN: ${{ secrets.QIITA_TOKEN }}
    環境変数QIITA_TOKENを設定し、GitHub Secretsから取得した値を使用してQiitaのAPIに認証されたアクセスを行います。このトークンにより、スクリプトはQiitaに記事を安全に投稿することが可能です。

このステップにより、GitHubリポジトリに保存された内容をQiitaに自動的に投稿するプロセスが完了します。

変更のコミットとプッシュ

ワークフローの最終ステップでは、スクリプトによって生成された変更をコミットし、git pushコマンドを使用して変更をGitHubのリモートリポジトリにプッシュします。

コミットの準備

まず、GitHub Actionsがコミットを行うためのユーザー情報を設定します。これには、メールアドレスとユーザー名が含まれます。

      - name: Commit changes
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"

変更のステージングとコミット

リポジトリ内の全ての変更をステージングし、コミットメッセージと共にコミットします。変更がない場合は、エラーを返さずに正常に処理を終了させます。

          git add .
          git commit -m "Update articles status" || exit 0

リモートリポジトリへのプッシュ

GitHubの認証トークンを使用してリモートリポジトリのURLを設定し、ローカルのコミットをリモートリポジトリにプッシュします。

          git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/yamazaki-hidekuni/Qiita.git
          git push

環境変数の設定

GitHub Actionsがリポジトリに対して認証された操作を行うために必要なGITHUB_TOKENを環境変数として設定します。

        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

このステップにより、Qiitaへの投稿が完了した後、リポジトリの変更をコミットし、それらをリモートリポジトリにプッシュすることで、リポジトリの状態を常に最新に保つことができます。

  • articles/unposted/: トピック別に分類された、Qiitaにまだ投稿されていない記事のMarkdownファイルが収められています。

  • articles/posted/: Qiitaに投稿後の記事をアーカイブするためのディレクトリで、投稿済みの記事がここに移されます。

  • src/QiitaAutoPost/: アプリケーションの主要な実行コードを含むProgram.csが配置されており、記事投稿のための主要なロジックがここに組み込まれています。

Program.csの詳細解説

Program.cs

Program.cs
// プログラム内で使用する.NETの標準ライブラリの名前空間を宣言しています。
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;

class Program
{
    private static readonly HttpClient httpClient = new HttpClient();

    static async Task Main(string[] args)
    {
        try
        {
            string unpostedDirectory = "articles/unposted";
            string postedDirectory = "articles/posted";

            var articleFile = FindOldestUnpostedArticle(unpostedDirectory);
            if (articleFile == null)
            {
                Console.WriteLine("未投稿の記事が見つかりませんでした。");
                return;
            }

            string articleContent = await File.ReadAllTextAsync(articleFile.FullName);
            string title = Path.GetFileNameWithoutExtension(articleFile.Name);
            string tag = articleFile.Directory.Name;

            bool isPosted = await PostArticleToQiita(title, tag, articleContent);
            if (!isPosted)
            {
                Console.WriteLine("記事の投稿に失敗しました。");
                return;
            }

            // 投稿済みのディレクトリに移動
            string newDirectoryPath = Path.Combine(postedDirectory, tag);
            if (!Directory.Exists(newDirectoryPath))
            {
                Directory.CreateDirectory(newDirectoryPath);
            }
            string newPath = Path.Combine(newDirectoryPath, articleFile.Name);
            File.Move(articleFile.FullName, newPath);

            // unpostedとpostedディレクトリを比較し、同名のファイルがあれば削除
            string unpostedFileName = Path.GetFileName(articleFile.FullName);
            string postedFileName = Path.GetFileName(newPath);

            if (postedFileName == unpostedFileName)
            {
                try
                {
                    File.Delete(articleFile.FullName);
                    Console.WriteLine($"削除成功: {articleFile.FullName}");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"削除失敗: {articleFile.FullName}, エラー: {ex.Message}");
                }
            }
            else
            {
                Console.WriteLine($"削除対象なし: {articleFile.FullName}");
            }

            Console.WriteLine("記事をQiitaに投稿しました。");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"エラーが発生しました: {ex.Message}");
        }
    }

    static FileInfo FindOldestUnpostedArticle(string directoryPath)
    {
        var directoryInfo = new DirectoryInfo(directoryPath);
        FileInfo oldestFile = null;
        DateTime oldestDate = DateTime.MaxValue;

        foreach (var subDirectory in directoryInfo.GetDirectories())
        {
            foreach (var file in subDirectory.GetFiles("*.md"))
            {
                if (file.CreationTime < oldestDate)
                {
                    oldestFile = file;
                    oldestDate = file.CreationTime;
                }
            }
        }
        return oldestFile;
    }

    static async Task<bool> PostArticleToQiita(string title, string tag, string content)
    {
        var requestUri = "https://qiita.com/api/v2/items";

        var postData = new
        {
            body = content,
            coediting = false,
            group_url_name = "jpt-qiita",
            @private = false,
            tags = new[] { new { name = tag } },
            title = title,
            tweet = false,
        };

        var jsonContent = JsonSerializer.Serialize(postData);
        var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");

        var accessToken = Environment.GetEnvironmentVariable("QIITA_TOKEN");
        httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

        var response = await httpClient.PostAsync(requestUri, httpContent);

        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine($"Qiitaへの投稿に失敗しました。ステータスコード: {response.StatusCode}");
            return false;
        }

        var responseBody = await response.Content.ReadAsStringAsync();
        var responseJson = JsonSerializer.Deserialize<Dictionary<string, object>>(responseBody);
        if (responseJson.ContainsKey("error"))
        {
            Console.WriteLine($"Qiitaへの投稿に失敗しました。エラーメッセージ: {responseJson["error"]}");
            return false;
        }

        return true;
    }
}

必要な名前空間の宣言

プログラミングにおいて、名前空間はコードの整理と管理に不可欠な要素です。C#では、名前空間を通じてクラスやメソッドなどのコード要素に対して一意のスコープを提供し、名前の衝突を防ぎます。これにより、異なるライブラリやモジュール間で同じ名前のクラスや関数があっても、それぞれが独立して扱われるため、コードの可読性とメンテナンス性が向上します。

C#で名前空間を宣言するには、usingディレクティブを使用します。これにより、プログラム内で特定のライブラリの機能に簡単にアクセスできるようになります。以下のコードスニペットは、一般的な.NETアプリケーションでよく使用される標準ライブラリの名前空間を宣言しています。

using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;

それぞれの名前空間が提供する機能は以下の通りです:

  • System: 基本的なデータ型(StringInt32など)、例外クラス、コンソール入出力など、基本的な機能を含む基礎的な名前空間です。
  • System.IO: ファイルやストリームの読み書きを行うための入出力関連のクラスを提供します。
  • System.Linq: データのクエリや操作を簡潔に記述できる、LINQ(Language Integrated Query)をサポートするクラスとインターフェースを含みます。
  • System.Net.Http: HTTP通信を行うためのクラスを提供し、Web APIなどのHTTPサービスとの通信を可能にします。
  • System.Text: 文字エンコーディング関連のクラスを含み、テキストデータのエンコードやデコードに使用します。
  • System.Text.Json: JSONデータのシリアライズとデシリアライズを行うためのクラスを含みます。
  • System.Threading.Tasks: 非同期プログラミングをサポートし、非同期操作を簡単に実装できるクラスを含みます。
  • System.Collections.Generic: 型安全なジェネリックコレクション(リスト、キュー、スタックなど)を提供します。

例えば、System.Linq名前空間を使用すると、以下のようにコレクションに対して簡潔なクエリを書くことができます:

using System.Linq;
...
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();

System.Linqを使用しない場合、同じ操作を行うにはより多くのコードを書く必要があります:

...
List<int> evenNumbers = new List<int>();
foreach (var n in numbers)
{
    if (n % 2 == 0)
    {
        evenNumbers.Add(n);
    }
}

このように名前空間を宣言することで、開発者は.NETライブラリが提供する豊富な機能にアクセスし、効率的かつ安全にコードを書くことができます。

ProgramクラスとMainメソッドの定義

C#プログラムの実行を開始するためには、エントリーポイントとなるMainメソッドが必要です。このメソッドは、プログラムが開始されるときに最初に呼び出されるメソッドであり、通常はProgramという名前のクラス内に定義されます。以下はその基本的な構造を示しています。

class Program
{
    static async Task Main(string[] args)
    {
        // ここにプログラムの実行コードが入ります...

Programクラスについて

Programクラスは、C#アプリケーションの中核を成すクラスです。クラス名は任意で変更可能ですが、慣例としてProgramがよく使用されます。このクラスは、プログラムの実行に必要なメソッドや変数を含んでおり、特にMainメソッドがその起点となります。

Mainメソッドの役割

Mainメソッドは、プログラムが実行される際に.NETランタイムによって呼び出される特別なメソッドです。このメソッドのシグネチャは、プログラムの実行方法によっていくつかの形式がありますが、最も一般的なのはstatic void Main(string[] args)またはstatic Task Main(string[] args)(非同期実行をサポートする場合)です。

非同期実行のサポート

上記のコード例では、Mainメソッドがasync Taskを返すように定義されています。これは、C# 7.1以降でサポートされている機能で、メソッド内で非同期操作を行うことができることを意味します。asyncキーワードは、メソッド内でawaitを使用することを可能にし、非同期プログラミングを行う際に非常に便利です。

argsパラメータ

Mainメソッドのargsパラメータは、コマンドライン引数を受け取るために使用されます。これにより、プログラム実行時に外部からパラメータを渡すことができ、プログラムの挙動を動的に変更することが可能になります。

HttpClientのインスタンス化

プログラミングにおいて「インスタンス化」とは、クラスからオブジェクトを生成するプロセスを指します。C#におけるHttpClientクラスも例外ではなく、Webリソースと通信するためには、まずこのクラスのインスタンスを作成する必要があります。

なぜインスタンス化が必要なのか?

HttpClientクラスは、Webサーバーとの間でHTTPリクエストとレスポンスをやり取りするための機能を提供します。しかし、これらの機能を使用する前に、HttpClientクラスの実体、つまりインスタンスをメモリ上に作り出す必要があります。これにより、プログラム内で実際にHttpClientの機能を利用できるようになります。

インスタンス化の方法

HttpClientのインスタンスを作成するには、以下のように記述します。

private static readonly HttpClient httpClient = new HttpClient();

この行のコードは、HttpClientクラスの新しいインスタンスを作成し、それをhttpClientという名前の変数に割り当てています。privateはこの変数がクラス内でのみアクセス可能であることを意味し、staticはこのインスタンスがクラスに属し、個々のオブジェクトに属さないことを示しています。readonlyはこの変数が初期化後に変更できないことを意味し、つまりhttpClientは一度作成されると、プログラムの実行中はその状態が保持されることを保証します。

未投稿の記事を検索するFindOldestUnpostedArticleメソッド

static FileInfo FindOldestUnpostedArticle(string directoryPath)
{
    var directoryInfo = new DirectoryInfo(directoryPath);
    FileInfo oldestFile = null;
    DateTime oldestDate = DateTime.MaxValue;

    foreach (var subDirectory in directoryInfo.GetDirectories())
    {
        foreach (var file in subDirectory.GetFiles("*.md"))
        {
            if (file.CreationTime < oldestDate)
            {
                oldestFile = file;
                oldestDate = file.CreationTime;
            }
        }
    }
    return oldestFile;
}

FindOldestUnpostedArticleメソッドは、指定されたディレクトリ内で最も古いMarkdown形式の記事ファイル(.mdファイル)を検索し、そのFileInfoオブジェクトを返す自作のメソッドです。このメソッドは、ブログの投稿システムなどで、次に投稿するべき古い記事を効率的に見つけ出すために使用されます。

メソッドの処理は以下の手順で行われます:

  1. new DirectoryInfo(directoryPath)を使用して、検索対象のディレクトリに関する情報を取得します。
  2. directoryInfo.GetDirectories()を呼び出し、対象ディレクトリ内のすべてのサブディレクトリを取得します。
  3. 各サブディレクトリに対してGetFiles("*.md")を使用し、Markdown形式のファイルのみを検索します。
  4. 検索された各ファイルについて、CreationTimeプロパティをチェックし、最も古い日時を持つファイルを特定します。
  5. 最も古いファイルが見つかれば、そのFileInfoオブジェクトを返します。見つからなければnullを返します。

このメソッドは、ファイルの作成日時を基に最も古いファイルを選択するため、記事の投稿順序を管理する際に役立ちます。例えば、記事の草稿が多数ある場合に、最も古いものから順に公開していくようなシナリオに適しています。

以下に、メソッドの使用例を示します:

string directoryPath = "articles/unposted";
FileInfo oldestArticle = FindOldestUnpostedArticle(directoryPath);
if (oldestArticle != null)
{
    Console.WriteLine($"最も古い未投稿の記事: {oldestArticle.FullName}");
}
else
{
    Console.WriteLine("未投稿の記事はありません。");
}

このコードは、articles/unpostedディレクトリ内の未投稿記事の中で最も古いものを探し出し、そのファイルのパスをコンソールに出力します。ファイルが見つからない場合は、それに応じたメッセージを表示します。

Qiitaに記事を投稿するPostArticleToQiitaメソッド

Qiitaでは、APIを通じてプログラムから直接記事を投稿することが可能です。この機能を利用して、C#で書かれたPostArticleToQiitaメソッドは、Qiitaに記事をプログラム的に投稿するための自作メソッドです。以下にその詳細な動作を解説します。

static async Task<bool> PostArticleToQiita(string title, string tag, string content)
{
    ...
}

メソッドの概要

このメソッドは、記事のタイトル(title)、タグ(tag)、そして記事の内容(content)を引数として受け取り、非同期的にQiitaのAPIにHTTP POSTリクエストを送信します。メソッドの戻り値はTask<bool>で、記事の投稿が成功したかどうかの結果を非同期的に返します。

HTTPリクエストの構築

まず、QiitaのAPIエンドポイントへのURIを指定します。次に、投稿する記事のデータを匿名オブジェクトとして構築し、JSON形式にシリアライズします。

var requestUri = "https://qiita.com/api/v2/items";
...
var jsonContent = JsonSerializer.Serialize(postData);

HTTPリクエストの準備

メソッドではまず、QiitaのAPIエンドポイント https://qiita.com/api/v2/items に対してPOSTリクエストを送信する準備を行います。このリクエストには、記事の本文、タイトル、タグなどがJSON形式で含まれます。JSONオブジェクトの作成は、C#の匿名型を利用して行われ、JsonSerializer によって文字列化されます。

var postData = new
{
    body = content, // 記事の本文
    coediting = false, // 共同編集の設定
    group_url_name = "jpt-qiita", // 投稿するグループの指定
    @private = false, // 非公開投稿かどうか
    tags = new[] { new { name = tag } }, // 記事に付けるタグ
    title = title, // 記事のタイトル
    tweet = false, // 投稿と同時にツイートするかどうか
};

var jsonContent = JsonSerializer.Serialize(postData);
var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");

このJSONは、HTTPリクエストのボディとして設定され、QiitaのAPIが解釈できる形式で送信されます。これにより、Qiita側で記事が適切に作成されるための情報が提供されます。

環境変数からのAPIトークンの取得

セキュリティを確保するために、APIトークンは環境変数から取得するのが一般的です。これにより、トークンがソースコードにハードコードされることを避け、トークンの漏洩リスクを減らすことができます。PostArticleToQiitaメソッドでは、Environment.GetEnvironmentVariableメソッドを使用して、環境変数QIITA_TOKENからAPIトークンを取得しています。

var accessToken = Environment.GetEnvironmentVariable("QIITA_TOKEN");
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

上記のコードは、環境変数QIITA_TOKENから取得したトークンをhttpClientの認証ヘッダーに設定しています。これにより、QiitaのAPIに対するリクエストが認証され、記事の投稿が可能になります。

HTTP POSTリクエストの送信

記事をQiitaに投稿するためには、HTTP POSTリクエストを送信する必要があります。このメソッドでは、HttpClientクラスのPostAsyncメソッドを使用して、QiitaのAPIエンドポイントにリクエストを送信しています。

var response = await httpClient.PostAsync(requestUri, httpContent);

ここでrequestUriはQiitaのAPIエンドポイントのURLを指し、httpContentは送信する記事の内容を含むHTTPコンテンツです。awaitキーワードはこの操作が非同期であることを示しており、リクエストが完了するまでメソッドの実行を一時停止します。

レスポンスのステータスコードの処理

リクエストが送信された後、応答として受け取ったHTTPステータスコードを確認することで、リクエストが成功したかどうかを判断します。ステータスコードが成功を示す範囲(200-299)にあるかどうかをチェックすることで、記事の投稿が成功したかを評価します。

if (!response.IsSuccessStatusCode)
{
    Console.WriteLine($"Qiitaへの投稿に失敗しました。ステータスコード: {response.StatusCode}");
    return false;
}

このコードは、HTTPレスポンスのIsSuccessStatusCodeプロパティをチェックしています。このプロパティがfalseを返す場合、コンソールにエラーメッセージを出力し、メソッドはfalseを返して失敗を示します。これにより、呼び出し元のコードは投稿の成功または失敗を適切に処理することができます。

エラーハンドリング

レスポンスにエラー情報が含まれている場合、それを取り出してコンソールに表示します。

if (responseJson.ContainsKey("error"))
{
    Console.WriteLine($"Qiitaへの投稿に失敗しました。エラーメッセージ: {responseJson["error"]}");
    ...
}

投稿後のファイル操作(移動と削除)

C#プログラムでは、System.IO名前空間を利用して、ファイルの移動や削除などの操作を簡単に行うことができます。

ステップ1: 投稿済みディレクトリの準備

まず、投稿済みの記事を保存するためのディレクトリを設定します。このステップでは、記事のタグに基づいて新しいディレクトリパスを作成し、そのディレクトリが存在しない場合は新たに作成します。

string newDirectoryPath = Path.Combine(postedDirectory, tag);
if (!Directory.Exists(newDirectoryPath))
{
    Directory.CreateDirectory(newDirectoryPath);
}

Path.Combineを使用することで、異なるパスのセグメントを結合して新しいパスを形成します。これにより、異なるオペレーティングシステム間でのパスの不一致を防ぎます。Directory.Existsでディレクトリの存在を確認し、なければDirectory.CreateDirectoryで新しいディレクトリを作成します。

ステップ2: 記事のファイル移動

次に、記事のファイルを新しく作成した投稿済みディレクトリに移動します。

string newPath = Path.Combine(newDirectoryPath, articleFile.Name);
File.Move(articleFile.FullName, newPath);

File.Moveメソッドは、ファイルを新しい場所に移動し、必要であればファイル名も変更します。

ステップ3: 重複ファイルの削除

最後に、元の未投稿記事のディレクトリと投稿済み記事のディレクトリを比較し、同名のファイルが存在する場合は削除します。

string unpostedFileName = Path.GetFileName(articleFile.FullName);
string postedFileName = Path.GetFileName(newPath);

if (postedFileName == unpostedFileName)
{
    try
    {
        File.Delete(articleFile.FullName);
        Console.WriteLine($"削除成功: {articleFile.FullName}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"削除失敗: {articleFile.FullName}, エラー: {ex.Message}");
    }
}
else
{
    Console.WriteLine($"削除対象なし: {articleFile.FullName}");
}

Path.GetFileNameでファイル名を取得し、移動後と移動前のファイルが同じ名前であるかを確認します。同じであれば、File.Deleteで元のファイルを削除し、成功したかどうかのメッセージをコンソールに表示します。この処理はtry-catchブロックで囲まれており、何か問題が発生した場合にはエラーメッセージを表示します。

  • src/: プロジェクトの設定ファイルQiitaAutoPost.csprojがあり、プロジェクトの依存関係や設定がここで管理されます。
QiitaAutoPost.csprojの詳細解説

QiitaAutoPost.csproj

QiitaAutoPost.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

</Project>

C#プロジェクトファイル

C#でプログラムを作成する際、コードだけではなく、そのコードがどのようにコンパイルされ実行されるかを指定するプロジェクトファイルが必要です。この.csprojファイルは、プロジェクトの設計図のようなもので、プロジェクトの設定全体を包括しています。

プロジェクトの設定を包むProject タグ

<Project>タグは、プロジェクトの設定を全て包み込む大きなカッコのような役割を果たしています。

<Project Sdk="Microsoft.NET.Sdk">
  <!-- ここに他の設定が入ります -->
</Project>

Sdk属性は、プロジェクトがどの開発キットを使用しているかを示しており、プログラムのビルドに必要なツールやライブラリを定義します。

プロジェクトの基本性質を定義するPropertyGroup

<PropertyGroup>はプロジェクトの基本的な特性を設定する場所です。例えば、プログラムが単独で実行されるアプリケーションなのか、それとも他のプログラムに組み込まれるライブラリなのかを決めるOutputTypeがここにあります。

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

Exeは実行可能なアプリケーションを意味し、この設定によりビルド時に実行ファイルが生成されます。

プログラムが動作するフレームワーク:TargetFramework

<TargetFramework>はプログラムが動作する.NETのバージョンを指定します。

    <TargetFramework>net6.0</TargetFramework>

この設定は、プログラムが実行される際の環境、つまりランタイムを決定します。

プロジェクトのビルドと実行の影響

これらの設定は、プロジェクトがどのようにビルドされ、どのように実行されるかに直接関わってきます。OutputTypeTargetFrameworkを適切に設定することで、プログラムがどのようにユーザーに届けられるかを決定できます。

  • .gitignore: Gitの追跡対象から除外するファイルやディレクトリを指定するためのファイルです。

  • Qiita.sln: Visual Studioのソリューションファイルで、プロジェクトの構成やビルドオプションをIDEが参照するためのものです。

  • README.md: プロジェクトの概要、目的、使用方法などを説明するためのドキュメントです。

参考リンク

7
7
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
7
7