C#でAPIがないサイトのスクレイピング

職場で使っているスケジュール管理のWebアプリはAPIとかがない。
その場合はスクレイピングがめんどうと思っていたのですが、割と簡単にできると分かったのでやってみました。
[AngleSharp](https://github.com/AngleSharp/AngleSharp)を使用しました。
これでJavaScriptのようにDOM操作ができます。

>機械翻訳
AngleSharpは、HTML、SVG、MathMLのような角括弧ベースのハイパーテキストを解析する機能を提供する.NETライブラリです。バリデーションのないXMLもサポートされている。AngleSharpの重要な点は、CSSも解析できることです。付属のパーサーは、公式のW3C仕様に基づいて構築されています。これは、与えられたソースコードの完全に移植可能なHTML5 DOM表現を生成し、常用ブラウザでの結果との互換性を保証します。また、querySelector や querySelectorAll のような標準的な DOM 機能も、ツリーの走査のために機能します。

[以前の記事](https://qiita.com/msms/items/8f6066f0500c6ed0145e)のAPI使わないバージョンのようなものです。


# 準備
- .NET9 vscode windows11
- dotnet add package AngleSharp

# スケジュールの取得
以前の[この内容](https://qiita.com/msms/items/8f6066f0500c6ed0145e#%E3%82%B9%E3%82%B1%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B)を変更していきます。
他はだいたいそのままです。

ScheduleAPIクラスの内容です。

## GetTaskTodoAsync
スケジュールを取得してアラート通知していない新たなスケジュールをリストで返します。
このリスト内容がトースト通知の内容になる。

```c#
public static async Task<List<Schedule>?> GetTaskTodoAsync()
{
    if (ScheduleList is null) return null;

    using var client = new HttpClient();
    if (!await LoginAsync(client)) return null;

    var responseBody = await NavigateToPageAsync(client);
    if (string.IsNullOrEmpty(responseBody)) return null;

    var newScheduleList = ParseSchedule(responseBody);
    if (newScheduleList is null) return null;
    var newScheduleListFilter = newScheduleList
            .Where(x => !ScheduleList.Any(y => x.IssueId == y.IssueId
            && x.StartTime == y.StartTime)
            && CheckPeriodFlag(x.StartTime))
            .ToList();
    ScheduleList.AddRange(newScheduleListFilter);
    return newScheduleListFilter;
}
```

## LoginAsync
サイトへのログイン処理。
`userid`と`password`は使用するサイトの`name`を調べます。
だいたい`form`の`input`となっている所。
```c#
    private static async Task<bool> LoginAsync(HttpClient client)
    {
        var loginData = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string?>("userid", ConfigurationManager.AppSettings["Id"]),
            new KeyValuePair<string, string?>("password", ConfigurationManager.AppSettings["Pass"])
        });

        var response = await client.PostAsync(ConfigurationManager.AppSettings["UrlLogin"], loginData);

        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine("ログイン失敗");
            return false;
        }
        return true;
    }
```

## NavigateToPageAsync
スクレイピングしたいページの内容をここでは単に文字列として取得しています。
`http://192.168.xx.xxx/schedule/day?viewDate=2025/02/04`のようなurlなので、日付部分を変更して当日のスケジュールを取得するようにしています。

```c#
    private static async Task<string?> NavigateToPageAsync(HttpClient client)
    {
        var response = await client.GetAsync(ConfigurationManager.AppSettings["UrlPage"] + DateTime.Today.ToString("yyyy/MM/dd"));
        if (!response.IsSuccessStatusCode) return null;

        return await response.Content.ReadAsStringAsync();
    }
```

## ParseSchedule
取得したページの文字列をAngleSharpで解析して必要な情報をリストにして返します。
`IssueId` `StartTime` `Title`がトースト通知で表示する内容。 
`IssueId `はトースト通知の方で、ページ開く時に使うためurlとしています。

```c#
private static List<Schedule>? ParseSchedule(string responseBody)
{
    if (ScheduleList is null) return null;

    var parser = new HtmlParser();
    var doc = parser.ParseDocument(responseBody);
    Console.WriteLine(doc.Title);

    return doc.GetElementsByClassName("aligned inCompany")
                .Select(x => new Schedule()
                {
                    IssueId = ConfigurationManager.AppSettings["UrlIp"] + x.GetAttribute("data-href"),
                    StartTime = ((IHtmlTableRowElement)x).Cells[0].TextContent.Trim().Split("～")[0],
                    Title = ((IHtmlTableRowElement)x).Cells[1].TextContent.Trim()
                })
                .ToList();
}
```

## CheckPeriodFlag
開始時間が指定時間内か確認してboolで返します。

```c#
private static bool CheckPeriodFlag(string? startTime)
{
    if (!DateTime.TryParse(startTime, out DateTime targetTime)) return false;
    TimeSpan timeDifference = targetTime - DateTime.Now;
    return 0 <= timeDifference.TotalMinutes
            && timeDifference.TotalMinutes
            <= (int.TryParse(ConfigurationManager.AppSettings["TargetMinutes"], out var result) ? result : 0);
}
```

## おわり
APIがなくても簡単にスクレイピングできました。
ヘッドレスなのでブラウザがチラつくような事もなく、常駐アプリとしていい感じにできました。

# タスクトレイ
スケジュール取得してトースト通知したいアプリなので、普段はタスクトレイで常駐させておきます。
タスクトレイに入れるにはwinフォームの機能が必要でした。
- [WPFでタスクトレイ常駐アプリを作る](https://qiita.com/TiggeZaki/items/aa17edbef0cc5f4736d9)
- [VSCode＋C#でタスクトレイ常駐アプリを作りたい](https://qiita.com/kyv28v/items/0a5b078d246000cb6781)
- 

# 参考
- [C#でモダンにスクレイピングするならAngleSharp](https://qiita.com/matarillo/items/a92e7efbfd2fdec62595)
- [C# AngleSharp ログイン後の画面遷移を行う](https://qiita.com/okonomiyaki-sushi/items/ba371bf971a64c005513)
- [C# 今更ですが、HttpClientを使う](https://qiita.com/rawr/items/f78a3830d894042f891b)