C#によるウェブコンテンツ解析技術:正規表現、HTMLパーサー、およびJSON処理の実践

1. 正規表現(Regular Expressions)によるテキスト・HTML解析構造化されていないテキストやHTMLソースから特定のデータを抽出する場合、正規表現は非常に強力なツールとなります。以下では、航空券の予約情報やフライト詳細を抽出するC#の実装例を示します。


// フライト詳細情報を抽出する正規表現パターン
string flightPattern = @"(?<FlightCode>[A-Z]{2}\d{4})\s+(?<CabinClass>[A-Z0-9]{1,2})\s+(?<TravelDate>\d{2}[A-Z]{3}\d{2})\s+(?<RouteCode>[A-Z]{6})\s+(?<BookingStatus>[A-Z]{2}\d)\s+(?<DepartureTime>\d{4})\s+(?<ArrivalTime>\d{4})";

Regex flightRegex = new Regex(flightPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant);

if (flightRegex.IsMatch(rawHtmlContent))
{
    MatchCollection matches = flightRegex.Matches(rawHtmlContent);
    foreach (Match match in matches)
    {
        string flightCode = match.Groups["FlightCode"].Value.Trim();
        string cabinClass = match.Groups["CabinClass"].Value.Trim();
        string travelDate = match.Groups["TravelDate"].Value.Trim();
        string routeCode = match.Groups["RouteCode"].Value.Trim();
        string status = match.Groups["BookingStatus"].Value.Trim();
        string depTime = match.Groups["DepartureTime"].Value.Trim();
        string arrTime = match.Groups["ArrivalTime"].Value.Trim();
        
        // 抽出したデータを使用した処理
    }
}

数値のみで構成されているかの検証や、単純なマッチングループも同様に実装できます。


// 数値かどうかの検証
bool isNumeric = Regex.IsMatch(targetValue, @"^\d+$");

// 基本的なマッチング処理
Regex simpleRegex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
if (simpleRegex.IsMatch(htmlContent))
{
    foreach (Match m in simpleRegex.Matches(htmlContent))
    {
        string extractedValue = m.Value;
    }
}

PNR(Passenger Name Record)テキストの高度な解析

航空会社のRT(Retrieve)エンコーディングテキストのような複雑なフォーマットから、予約コード、フライトセグメント、乗客情報を抽出する実装例です。


public BookingDetails ExtractBookingDetails(string pnrText, out string errorMessage)
{
    errorMessage = string.Empty;
    var result = new BookingDetails();
    var stopwatch = System.Diagnostics.Stopwatch.StartNew();

    // PNRコードの抽出
    Regex pnrRegex = new Regex(@"\s+(?:MR|MS|MRS|MISS)\s+(?<PNR>[A-Z0-9]{6})", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Multiline);
    if (!pnrRegex.IsMatch(pnrText))
    {
        errorMessage = "PNRコードが見つかりません";
        return null;
    }
    result.PnrCode = pnrRegex.Match(pnrText).Groups["PNR"].Value.Trim();

    // フライトセグメントの抽出
    try
    {
        string segPattern = @"(?<FlightCode>[0-9A-Z]{2}\d{1,4})\s+(?<CabinClass>[A-Z0-9]{1,2})\s+(?<TravelDate>[A-Z]{2}\d{2}[A-Z]{3})\s+(?<RouteCode>[A-Z]{6})\s+(?<BookingStatus>[A-Za-z]{2}\d)\s+(?<DepartureTime>\d{4}[+]?[1-9]?)\s+(?<ArrivalTime>\d{4}[+]?[1-9]?)";
        Regex segRegex = new Regex(segPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant);
        
        if (!segRegex.IsMatch(pnrText))
        {
            errorMessage = "フライト情報が見つかりません";
            return null;
        }

        result.Segments = new List<FlightSegment>();
        DateTime baseDate = DateTime.Now;
        int segmentIndex = 1;

        foreach (Match match in segRegex.Matches(pnrText))
        {
            var segment = new FlightSegment
            {
                FlightNumber = match.Groups["FlightCode"].Value.Trim(),
                Cabin = match.Groups["CabinClass"].Value.Trim().Substring(0, 1),
                Carrier = match.Groups["FlightCode"].Value.Trim().Substring(0, 2),
                DepartureAirport = match.Groups["RouteCode"].Value.Trim().Substring(0, 3),
                ArrivalAirport = match.Groups["RouteCode"].Value.Trim().Substring(3),
                Sequence = segmentIndex
            };
            
            // 日付の計算と設定(簡略化)
            segment.DepartureTime = ParseFlightTime(match.Groups["DepartureTime"].Value.Trim(), baseDate);
            segment.ArrivalTime = ParseFlightTime(match.Groups["ArrivalTime"].Value.Trim(), baseDate);
            
            result.Segments.Add(segment);
            segmentIndex++;
        }
    }
    catch (Exception)
    {
        errorMessage = "フライト情報の解析中にエラーが発生しました";
        return null;
    }

    // 乗客情報の抽出
    result.Passengers = new List<PassengerInfo>();
    try
    {
        string paxPattern = @"[.]?(?<FullName>[A-Z]{2,}/[A-Z]{1,}\s?[A-Z]{0,})\s+(?<Gender>MR|MS|MRS|MISS)\s*";
        Regex paxRegex = new Regex(paxPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant);
        
        if (!paxRegex.IsMatch(pnrText))
        {
            errorMessage = "乗客情報が見つかりません";
            return null;
        }

        foreach (Match match in paxRegex.Matches(pnrText))
        {
            var passenger = new PassengerInfo
            {
                Name = match.Groups["FullName"].Value.Trim(),
                Gender = match.Groups["Gender"].Value.Trim() == "MR" ? "M" : "F"
            };
            result.Passengers.Add(passenger);
        }
        
        // SSR DOCS(パスポート情報)の抽出
        string docsPattern = @"SSR DOCS [A-Z0-9]{2} [A-Z0-9]{2}1 P/(?<IssuingCountry>[A-Z]{2})/(?<PassportNo>[0-9A-Z]{2,})/(?<Nationality>[A-Z]{2})/(?<BirthDate>[0-9A-Z]{7})/(?<GenderCode>[A-Z]{1})/(?<ExpiryDate>[0-9A-Z]{7})/(?<FullName>[A-Z]{2,}/[A-Z]{1,}\s?[A-Z]{0,})";
        Regex docsRegex = new Regex(docsPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant);
        
        if (docsRegex.IsMatch(pnrText))
        {
            foreach (Match match in docsRegex.Matches(pnrText))
            {
                string matchedName = match.Groups["FullName"].Value.Trim();
                var targetPax = result.Passengers.FirstOrDefault(p => p.Name == matchedName);
                if (targetPax != null)
                {
                    targetPax.IssuingCountry = match.Groups["IssuingCountry"].Value.Trim();
                    targetPax.PassportNumber = match.Groups["PassportNo"].Value.Trim();
                    targetPax.Nationality = match.Groups["Nationality"].Value.Trim();
                    targetPax.Gender = match.Groups["GenderCode"].Value.Trim();
                    // 日付フォーマットの変換処理は省略
                }
            }
        }
    }
    catch (Exception)
    {
        errorMessage = "乗客情報の解析中にエラーが発生しました";
        return null;
    }

    return result;
}

手荷物情報などのシンプルなテキスト抽出にも正規表現が活用できます。


Regex baggageRegex = new Regex(@"(\d+)個");
string itineraryText = "東京-大阪,1個,各20KG,サイズ100*60*40CM;大阪-福岡,2個,各23KG,サイズ100*60*40CM";

if (baggageRegex.IsMatch(itineraryText))
{
    Match baggageMatch = baggageRegex.Match(itineraryText);
    int baggageCount = int.Parse(baggageMatch.Groups[1].Value);
}

2. HtmlAgilityPackとXPathによるDOM解析

HTMLの構造が比較的安定している場合、HtmlAgilityPackライブラリとXPath構文を使用することで、特定のノードを正確に抽出できます。ブラウザの開発者ツール(DevTools)を使用してXPathを取得すると効率的です。


string htmlSource = "";
var htmlDocument = new HtmlAgilityPack.HtmlDocument();
htmlDocument.LoadHtml(htmlSource);

var rootNode = htmlDocument.DocumentNode;

// 特定のクラスと属性を持つリスト項目をXPathで選択
var targetNodes = rootNode.SelectNodes("//div[@id='search-results']//ul[@class='item-list']//li[@data-category='premium']");

if (targetNodes != null)
{
    foreach (var node in targetNodes)
    {
        string itemTitle = node.SelectSingleNode(".//h3[@class='title']").InnerText.Trim();
    }
}

3. Newtonsoft.JsonによるJSONのシリアライズとデシリアライズ

APIからのレスポンスや設定ファイルなど、JSON形式のデータを扱う場合はNewtonsoft.Json(Json.NET)が標準的に利用されます。


// JSON文字列をオブジェクトのリストにデシリアライズ
string jsonPayload = "[...]";
var userList = JsonConvert.DeserializeObject>(jsonPayload);

ストリームからJSONデータを読み込み、特定の配列要素を処理しながらUIを更新する非同期処理の例です。


using (var streamReader = new StreamReader(dataStream))
{
    string rawJson = await streamReader.ReadToEndAsync();
    
    // JSONのルートオブジェクトと配列を解析
    var rootObject = JObject.Parse(rawJson);
    var imageArray = rootObject["images"] as JArray;
    
    int totalImages = imageArray.Count;

    for (int i = 0; i < totalImages; i++)
    {
        var imageNode = imageArray[i] as JObject;
        string imageUrl = (string)imageNode["sourceUrl"];
        
        try
        {
            // 画像のダウンロード処理
            await DownloadAssetAsync(imageUrl);
            
            // UIスレッドでプログレスバーとログを更新
            UpdateProgressSafely((i + 1) * 100 / totalImages);
            AppendLogSafely($"ダウンロード完了: {imageUrl}");
        }
        catch (Exception ex)
        {
            AppendLogSafely($"エラー発生: {ex.Message}");
        }
    }
}

JSONからC#クラスへの自動変換

大規模なJSONレスポンスを扱う際、手動でC#のエンティティクラスを記述するのは時間がかかります。Visual Studioの「特別な貼り付け(JSONからのクラス)」機能や、json2csharp などのオンラインツール、あるいはRiderなどのIDEに組み込まれているジェネレーター機能を活用することで、JSONの構造から正確なC#クラス定義を瞬時に生成できます。

タグ: csharp RegularExpressions HtmlAgilityPack NewtonsoftJson webscraping

10月3日 16:47 投稿