概要
- HTML5で定義されている文字実体参照を、タブ区切りファイル(TSV形式)に変換するサンプルコードを紹介します。
- HTML5の文字実体参照はHTML Living Standardの13.5 Named character referencesで定義されています。このページからダウンロードできる文字実体参照の一覧(JSON形式)を変換元(入力ファイル)として使用します。(以後、「文字実体参照JSONファイル」と表記)
- サンプルコードは次の環境で動作確認しています。
OS Windows 10(64ビット) IDE Microsoft Visual Studio Community 2019(16.8.5) + C#(8.0) - サンプルの完全なソースコードはこちらで公開しています。
- 文字実体参照については、こちらをご覧ください。
サンプルコード
前提とする入出力ファイル仕様
- 入力ファイル仕様
想定しているHTML5の文字実体参照JSONファイルのイメージを次に示します。{ "Æ": { "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 Æ 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に対応させています。
| 条件 | 候補例 | 優先候補 |
|---|---|---|
| 同じコードポイントで”;”の有無が異なる文字実体参照がある場合、”;”がある文字実体参照を優先する。 | “<“, “<“ | “<“ |
| 同じコードポイントで大小文字が異なる文字実体参照がある場合、小文字の文字実体参照を優先する。 | “<“, “<“ | “<“ |
| 同じコードポイントで異なる文字実体参照がある場合、短い方を優先する。 | “ “, “ “ | “ “ |
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};");
}