NDW

アプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

.NET Core 1. システムエンジニアリング html/jquery 実装技術

HTML5文字実体参照をタブ区切りファイルに変換

投稿日:

概要

  • HTML5で定義されている文字実体参照を、タブ区切りファイル(TSV形式)に変換するサンプルコードを紹介します。
  • HTML5の文字実体参照はHTML Living Standard13.5 Named character referencesで定義されています。このページからダウンロードできる文字実体参照の一覧(JSON形式)を変換元(入力ファイル)として使用します。(以後、「文字実体参照JSONファイル」と表記)
  • HTML5の文字実体参照については、こちらをご覧ください。
  • サンプルコードは次の環境で動作確認しています。
    OS Windows 10(64ビット)
    IDE Microsoft Visual Studio Community 2019(16.8.5) + C#(8.0)
  • サンプルの完全なソースコードはこちらで公開しています。

サンプルコード

前提とする入出力ファイル仕様

  • 入力ファイル仕様
    想定しているHTML5の文字実体参照JSONファイルのイメージを次に示します。
    {
      "&AElig": { "codepoints": [198], "characters": "\u00C6" },
      "Æ": { "codepoints": [198], "characters": "\u00C6" },
    ...
      "⊁": { "codepoints": [8833], "characters": "\u2281" },
      "⪰̸": { "codepoints": [10928, 824], "characters": "\u2AB0\u0338" },
      "⋡": { "codepoints": [8929], "characters": "\u22E1" },
      "≿̸": { "codepoints": [8831, 824], "characters": "\u227F\u0338" },
      "⊃⃒": { "codepoints": [8835, 8402], "characters": "\u2283\u20D2" },
      "⊉": { "codepoints": [8841], "characters": "\u2289" },
    ...
    
  • 出力ファイル仕様

    ここで説明するサンプルコードでは、変換した内容をタブ区切りファイルに出力します。
    このファイルに出力する項目は次の通りです。

    項番 項目名 説明 サンプル値
    1 No. 1から始まる連番 376
    2 文字実体参照名 特定文字を表現するための名称 ⊃⃒
    3 コードポイント(1/2) 文字実体参照に対応するコードポイント値。
    文字実体参照が2文字に対応する場合、2文字目のコードポイント値を「コードポイント(1/2)」に出力する。
    8835
    4 コードポイント(2/2) 8402
    5 数値文字参照(10進数)(1/2) 文字実体参照に対応する10進数形式の数値文字参照。
    文字実体参照が2文字に対応する場合、2文字目の数値文字参照を「数値文字参照(10進数)(2/2)」に出力する。
    ⊃
    6 数値文字参照(10進数)(2/2) ⃒
    7 数値文字参照(16進数)(1/2) 文字実体参照に対応する16進数形式の数値文字参照。
    文字実体参照が2文字に対応する場合、2文字目の数値文字参照を「数値文字参照(16進数)(2/2)」に出力する。
    ⊃
    8 数値文字参照(16進数)(2/2) ⃒

    出力イメージを次に示します。
    (ここでは見やすいようタブではなく任意長の空白を使用しています。前述のサンプル値をハイライトで表示しています。)

    1    &AElig                      198             Æ             Æ    
    2    Æ                     198             Æ             Æ    
    ...
    372  ⊁              8833            ⊁             ⊁    
    373  ⪰̸        10928     824   ⪰    ̸   ⪰    ̸
    374  ⋡    8929            ⋡             ⋡    
    375  ≿̸         8831     824    ≿    ̸   ≿    ̸
    376  ⊃⃒              8835    8402    ⊃   ⃒   ⊃    ⃒
    377  ⊉         8841            ⊉             ⊉    
    ...
    

変換サンプルコード(1:N)

文字実体参照JSONファイルを単純にタブ区切りファイルに変換するサンプルコードです。
文字実体参照JSONファイルは、コードポイントに対して複数の文字実体参照が対応(1:N)する場合がありますが、そのままファイルに出力します。

private static void OutputAsTsv(string inputFilename, string outputFilename)
{
    using var inputStream = File.OpenRead(inputFilename);
    using var outputWriter = new StreamWriter(outputFilename, false);

    int no = 1;
    var document = JsonDocument.Parse(inputStream);
    foreach (var jsonProperty in document.RootElement.EnumerateObject())
    {
        // 文字実体参照名とコードポイント配列の取得
        var cer = jsonProperty.Name;
        var value = jsonProperty.Value;
        var codepoints = value.GetProperty("codepoints")
            .EnumerateArray().Select(e => e.GetInt32()).ToArray();

        // 出力データの編集
        WriteRecord(outputWriter, no++, codepoints, cer);
    }
}

private static void WriteRecord(StreamWriter writer, int no, int[] codepoints, string cer)
{
    int cp1 = codepoints[0];
    int? cp2 = codepoints.Length > 1 ? codepoints[1] : (int?)null;

    var cp1str = cp1.ToString();
    var cp2str = cp2?.ToString() ?? string.Empty;
    (var ncrDec1, var ncrHex1) = ToNcr(cp1);
    (var ncrDec2, var ncrHex2) = ToNcr(cp2);

    writer.WriteLine($"{no}\t{cer}\t{cp1str}\t{cp2str}" +
        $"\t{ncrDec1}\t{ncrDec2}\t{ncrHex1}\t{ncrHex2}");
}

private static (string dec, string hex) ToNcr(int? codepoint)
{
    if (!codepoint.HasValue) return (string.Empty, string.Empty);
    return ($"&#{codepoint.Value};" , $"&#x{codepoint.Value:X5};");
}

変換サンプルコード(1:1)

前述の「変換サンプルコード(1:N)」と同様にタブ区切りファイルに変換するサンプルコードです。
こちらのサンプルでは、次のルールに従ってコードポイントに対する文字実体参照を1:1に対応させています。

条件 候補例 優先候補
同じコードポイントで”;”の有無が異なる文字実体参照がある場合、”;”がある文字実体参照を優先する。 &LT“, “< <
同じコードポイントで大小文字が異なる文字実体参照がある場合、小文字の文字実体参照を優先する。 <“, “< <
同じコードポイントで異なる文字実体参照がある場合、短い方を優先する。  “, “   
private static void OutputUniqueCodepointAsTsv(string inputFilename, string outputFilename)
{
    // コードポイント配列・文字実体参照ディクショナリ
    var dic = new Dictionary<int[], string>(new IntArrayEqualityComparer());

    // 重複するコードポイントを除外
    using var inputStream = File.OpenRead(inputFilename);
    var document = JsonDocument.Parse(inputStream);
    foreach (var jsonProperty in document.RootElement.EnumerateObject())
    {
        // 文字実体参照名とコードポイント配列の取得
        var newCer = jsonProperty.Name;
        var value = jsonProperty.Value;
        var codepoints = value.GetProperty("codepoints")
            .EnumerateArray().Select(e => e.GetInt32()).ToArray();

        if (!dic.ContainsKey(codepoints))
        {
            dic[codepoints] = newCer;
            continue;
        }

        // 同一のコードポイントが存在する場合、ルールに基づいて更新
        var oldCer = dic[codepoints];
        var oldCerLower = oldCer.ToLower();
        var newCerLower = newCer.ToLower();

        bool priorNew;
        // "&Xyz;" vs "&xyz" → 左辺を優先
        if (oldCerLower == newCerLower + ";") priorNew = false;
        // "&Xyz" vs "&xyz;" → 右辺を優先
        else if (oldCerLower + ";" == newCerLower) priorNew = true;
        // 大小文字無視で一致の場合、小文字側を優先
        else if (oldCerLower == newCerLower) priorNew = (newCer == newCerLower);
        // 大小文字無視で不一致の場合、桁数が少ない側を優先
        else priorNew = oldCer.Length > newCer.Length;

        // debug
        //Console.WriteLine($"{string.Join(",", codepoints)}: " +
        //    (priorNew ? newCer : oldCer ) + $" <- [ {oldCer} / {newCer} ]");

        if (priorNew) dic[codepoints] = newCer;
    }

    // データ出力
    using var outputWriter = new StreamWriter(outputFilename, false);
    var kvlist = dic.ToList();
    for (var i = 0; i < kvlist.Count; i++)
    {
        var codepoints = kvlist[i].Key;
        var cer = kvlist[i].Value;
        WriteRecord(outputWriter, i + 1, codepoints, cer);
    }
}

class IntArrayEqualityComparer : IEqualityComparer<int[]>
{
    public bool Equals([AllowNull] int[] x, [AllowNull] int[] y)
        => Enumerable.SequenceEqual(x, y);
    public int GetHashCode([DisallowNull] int[] obj)
    {
        var hash = new HashCode();
        foreach (var v in obj) hash.Add(obj[0]);
        return hash.ToHashCode();
    }
}

private static void WriteRecord(StreamWriter writer, int no, int[] codepoints, string cer)
{
    int cp1 = codepoints[0];
    int? cp2 = codepoints.Length > 1 ? codepoints[1] : (int?)null;

    var cp1str = cp1.ToString();
    var cp2str = cp2?.ToString() ?? string.Empty;
    (var ncrDec1, var ncrHex1) = ToNcr(cp1);
    (var ncrDec2, var ncrHex2) = ToNcr(cp2);

    writer.WriteLine($"{no}\t{cer}\t{cp1str}\t{cp2str}" +
        $"\t{ncrDec1}\t{ncrDec2}\t{ncrHex1}\t{ncrHex2}");
}

private static (string dec, string hex) ToNcr(int? codepoint)
{
    if (!codepoint.HasValue) return (string.Empty, string.Empty);
    return ($"&#{codepoint.Value};" , $"&#x{codepoint.Value:X5};");
}






-.NET Core, 1. システムエンジニアリング, html/jquery, 実装技術

関連記事

.NET Core(C#): JsonSerializer実践オプションとコンバータ

業務アプリの開発を想定したJsonSerializerの使い方とサンプルです。 概要 .NET Core 3.1の標準パッケージSystem.Text.Jsonに含まれるJsonSerializerを …

.NET Core(C#): 配列/List/Dictionary/HashSet変換方法

はじめに 次の環境を使用して動作確認しています。 OS Windows 10(64ビット) IDE Microsoft Visual Studio Community 2019(16.8.5) + C …

EAP7のインストールとパッチ適用

仕事でredhat社のEnterprise Application Server7(EAP7)を扱うことが多いので備忘録として記載します。 なお、このサイトでWebアプリを公開したいと考えていますが、 …

chromeのjavascriptを一時的に無効化

Webアプリの開発やテストで、クライアント側のバリデーションを無効にしてサーバ側バリデーションの動作確認する、等のように一時的にjavascriptを無効にしたい場合があります。これを実現するための方 …

Windows Updateが終わらない問題

ふと会社で使っているPCのWindows Updateがほとんど当たっていないことに気づいた。 手動でWindows Updateを実行すると、30分、1時間たっても終わらない… これは何 …