BOMの有無を判別し、UTFを読み分ける StreamReader のサンプルコード

2020年9月14日月曜日

技術的備忘録 汎用ソースコード&ツール

t f B! P L


shift-jis と utf-8 の混在問題に関する記事(リンクリスト)に戻る


この記事は情技師(ITエンジニア)向けの記事です。

 

.net の C# の StreamReader を使用してBOM有り、またはBOMなしのテキストファイルを読み込むコードを書こうと検索しても良いサンプルコードが見つからなかったので、ここにサンプルコードを載せておく。

 

テキスト先頭のBOMの有無を判定し、BOMが無ければ shift-jis として読み込み、BOMが有ればBOMの示すエンコーディングで読み込む。

対応文字コードは shift-jis, utf-8, utf-16LE, utf-16BE, utf-32LE, utf-32BE となる。

このコードではBOMなしは全て shift-jis として解釈するが、BOMなしを判定した後の処理を改造すれば、その他の文字コードにも対応できるはずだ。

 

三つのサンプルを載せるが、最初の二つは解説の為に載せているだけで実用には向かない。

三つ目のサンプルが実用的サンプルコードだ。

#define sample03

の値を切り替えてビルドすると好きなコードを有効化できる。

#define sample01

#define sample02

#define sample03

の何れかをコードの先頭に書いてビルドする。

「sample03」 が実用的コードになる。

コードは .net framework 4.0 上でテストしている。

バージョンに依存するようなコードでもないので、他のバージョンや .net core でも使えると思う。

Visual Studio で「コンソールアプリケーション」でプロジェクトファイルを作成してコピーしてビルドする。

<2020-10-29追記>ゼロサイズテキストファイルのテスト完了しました。問題なく動作します。

以下が全サンプルコードだ。

#define sample03

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace StreamReaderForBOM_sample
{
    class Program
    {
        /// <summary>
        /// This is the reading sample of BOM by StreamReader. 
        /// </summary>
        /// <param name="args">[1]:FileName</param>
        static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine("Please input file name !");
            }

            string fileName = args[0];
            string fileContent = null;

#if sample01
            // Simplest StreamReader sample code.
            using (var reader =
                new StreamReader(fileName, 
                    Encoding.GetEncoding("utf-8"), true)
                )
            {
                Console.WriteLine("Before reading content : {0},{1}\n", 
                    reader.CurrentEncoding.ToString(), 
                    reader.CurrentEncoding.CodePage.ToString());

                fileContent = reader.ReadToEnd();

                Console.WriteLine("After reading content : {0},{1}\n", 
                    reader.CurrentEncoding.ToString(), 
                    reader.CurrentEncoding.CodePage.ToString());
            }

            Console.WriteLine(fileContent);
#endif

#if sample02
            // This is a sample code that can identify the BOM.
            // However, I cannot identify that there is no BOM.
            using (var reader =
                new StreamReader(fileName,
                    Encoding.GetEncoding("utf-8"), true)
                )
            {
                Console.WriteLine("Peek().");

                reader.Peek();

                byte[] buffer;
                buffer = reader.CurrentEncoding.GetPreamble().ToArray<byte>();

                Console.WriteLine("GetPreamble().Length : {0}",
                    reader.CurrentEncoding.GetPreamble().Length);

                Console.Write("GetPreamble().ToArray ");

                foreach(var buf in buffer)
                {
                    Console.Write(":{0}", buf.ToString("x2"));
                }
                Console.WriteLine(";");

                Console.WriteLine("Before reading content : {0},{1}\n",
                    reader.CurrentEncoding.ToString(),
                    reader.CurrentEncoding.CodePage.ToString());

                fileContent = reader.ReadToEnd();

                Console.WriteLine("After reading content : {0},{1}\n",
                    reader.CurrentEncoding.ToString(),
                    reader.CurrentEncoding.CodePage.ToString());
            }

            Console.WriteLine(fileContent);
#endif

#if sample03

            // This is sample code where utf and shift-jis can coexist.
            using (FileStream fs =
                new FileStream(fileName, FileMode.Open)
                )
            {
                // I am read BOM of readfile.
                byte[] bom = new byte[4] {0xFF, 0xFF, 0xFF, 0xFF };
                int codepage;
                fs.Read(bom, 0, 4);
                fs.Position = 0;

                if (IsBOM(bom, out codepage))
                {
                    Console.Write("BOM Value ");

                    foreach (var b in bom)
                    {
                        Console.Write(":{0}", b.ToString("x2"));
                    }
                    Console.WriteLine(";");
                }
                else
                {
                    Console.WriteLine("non BOM ! ;");

                }

                // Determine the encoding of StreamReader using FileStream.
                using (var reader =
                    new StreamReader(fs, Encoding.GetEncoding(codepage))
                    )
                {
                    Console.WriteLine("Before reading content : {0},{1}\n",
                        reader.CurrentEncoding.EncodingName,
                        reader.CurrentEncoding.CodePage.ToString());

                    fileContent = reader.ReadToEnd();

                    Console.WriteLine("After reading content : {0},{1}\n",
                        reader.CurrentEncoding.EncodingName,
                        reader.CurrentEncoding.CodePage.ToString());
                }

                Console.WriteLine(fileContent);
            }

#endif
        } //End of Main

#if sample03

        /// <summary>
        /// Determine if it is BOM
        /// </summary>
        /// <param name="bom">Array to be inspected (4 bytes)</param>
        /// <param name="codepage">output of Encoding.Codepage</param>
        /// <returns>true=BOM.</returns>
        static bool IsBOM(byte[] bomByte, out int codepage)
        {
            bool result;
            byte[] bomUTF8 = { 0xEF, 0xBB, 0xBF };
            byte[] bomUTF16Little = { 0xFF, 0xFE };
            byte[] bomUTF16Big = { 0xFE, 0xFF };
            byte[] bomUTF32Little = { 0xFF, 0xFE, 0x00, 0x00 };
            byte[] bomUTF32Big = { 0x00, 0x00, 0xFE, 0xFF };

            if (IsMatched(bomByte, bomUTF8))
            {
                result = true;
                codepage = 65001; //utf-8,Unicode (UTF-8)
            }

            else if (IsMatched(bomByte, bomUTF32Little))
            {
                result = true;
                codepage = 12000; //utf-32,Unicode (UTF-32)
            }

            else if (IsMatched(bomByte, bomUTF32Big))
            {
                result = true;
                codepage = 12001; //utf-32BE,Unicode (UTF-32 Big-Endian) 
            }

            else if (IsMatched(bomByte, bomUTF16Little))
            {
                result = true;
                codepage = 1200; //utf-16,Unicode
            }

            else if (IsMatched(bomByte, bomUTF16Big))
            {
                result = true;
                codepage = 1201; //utf-16BE,Unicode (Big-Endian) 
            }

            else
            {
                result = false;
                //codepage = 0; //non BOM !
                codepage = 932; //shift_jis,Japanese (Shift-JIS)
            }

            return result;
        }

        /// <summary>
        /// BOM sequence comparison
        /// </summary>
        /// <param name="data">Sequence to be inspected</param>
        /// <param name="bom">BOM array</param>
        /// <returns>true=match</returns>
        static bool IsMatched(byte[] data, byte[] bom)
        {
            bool result = true;

            for (int i = 0; i < bom.Length; i++)
            {
                if (bom[i] != data[i])
                    result = false;
            }

            return result;
        }

#endif

    }
}

「コンソールアプリケーション」なので、コマンドプロンプトからこのコマンドを使用する。

コマンド引数は、ファイル名だけだ。

<コマンド名> <ファイル名>

 

sample01 最も単純な StreamReader の読み込み

最初に、StreamReader の最も単純なサンプルを掲載する。

単純に utf-8 でテキストファイルを読み込み標準出力に出力する。

// Simplest StreamReader sample code.
using (var reader =
    new StreamReader(fileName, 
        Encoding.GetEncoding("utf-8"), true)
    )
{
    Console.WriteLine("Before reading content : {0},{1}\n", 
        reader.CurrentEncoding.ToString(), 
        reader.CurrentEncoding.CodePage.ToString());

    fileContent = reader.ReadToEnd();

    Console.WriteLine("After reading content : {0},{1}\n", 
        reader.CurrentEncoding.ToString(), 
        reader.CurrentEncoding.CodePage.ToString());
}

Console.WriteLine(fileContent);

このサンプルは StreamReader クラスのインスタンスの CurrentEncoding プロパティの値の性質を知ってもらう為に書いた。

reader ストリームは、StreamReader コンストラクタの指定するエンコーディングで CurrentEncoding の値を設定するが、BOMありテキストファイルを読み込むと、そのエンコーディングに変更される。

仮に sample01 をビルドしてPowerShell v7.0 のコンソールから、BOM有り utf-16LE のテキストファイルを読み込むよう、実行すると以下のように表示する。

Before reading content : System.Text.UTF8Encoding,65001

After reading content : System.Text.UnicodeEncoding,1200

TESTストリングaaa日本語

最後の「TESTストリングaaa日本語」はテキストファイルの内容である。

ReadToEnd() を実行する前の CurrentEncoding の値は「UTF8Encoding,65001」だが、実行後は「UnicodeEncoding,1200」になっている。

UnicodeEncoding は utf-16LE のエンコーディング名である。

65001 や 1200 はコードページと言って、エンコーディングを区別する番号である。

StreamReader はBOMが存在する場合は、自動でエンコーディングを区別する事ができる。

その判断は最初にテキストファイルを読み込んだタイミングで行われる。

しかし、BOMの無いテキストファイルは判別できない。

最初に読むまでエンコーディングが分からないので、文字コードが複数混在する現場では実用的では無い。

BOMなしで分からない時は指定が無ければ、utf-8 になる。

 

sample02 読み込む前にエンコーディングを区別する

では、sample01 を改良して、テキストを読み込む前にエンコーディングを区別するようにしてみよう。

// This is a sample code that can identify the BOM.
// However, I cannot identify that there is no BOM.
using (var reader =
    new StreamReader(fileName,
        Encoding.GetEncoding("utf-8"), true)
    )
{
    Console.WriteLine("Peek().");

    reader.Peek();

    byte[] buffer;
    buffer = reader.CurrentEncoding.GetPreamble().ToArray<byte>();

    Console.WriteLine("GetPreamble().Length : {0}",
        reader.CurrentEncoding.GetPreamble().Length);

    Console.Write("GetPreamble().ToArray ");

    foreach(var buf in buffer)
    {
        Console.Write(":{0}", buf.ToString("x2"));
    }
    Console.WriteLine(";");

    Console.WriteLine("Before reading content : {0},{1}\n",
        reader.CurrentEncoding.ToString(),
        reader.CurrentEncoding.CodePage.ToString());

    fileContent = reader.ReadToEnd();

    Console.WriteLine("After reading content : {0},{1}\n",
        reader.CurrentEncoding.ToString(),
        reader.CurrentEncoding.CodePage.ToString());
}

Console.WriteLine(fileContent);

StreamReader でファイルを開いてから、Peek() を実行する。

これはテキストファイルの最初の1文字を読み込むがファイルポインタは進めないメソッドである。

これで、BOMだけ読み込む。

BOMが無ければ何もしないと考えて良い。

Peek() を実行後、 CurrentEncoding.GetPreamble() の中にBOMの内容が読み込まれるので、その内容を標準出力に出力している。

これで、BOM有り utf-16LE のテキストファイルを読み込むよう、実行すると以下のように表示する。

Peek().
GetPreamble().Length : 2
GetPreamble().ToArray :ff:fe;
Before reading content : System.Text.UnicodeEncoding,1200

After reading content : System.Text.UnicodeEncoding,1200

TESTストリングaaa日本語

ReadToEnd() を実行する前の段階の「Before reading content」で、 CurrentEncoding の値が「UnicodeEncoding,1200」と utf-16LE である事が認識できている。

もし全てのテキストファイルがBOM有りのテキストファイルの現場ならば、このコードで対処できる。

 

しかし、このコードは一つ欠点があり、「BOMなしテキストファイル」を認識できない。

「BOMが存在しない」事がわからないのだ。

もし、shift-jis と BOM有り utf-8 が混在する現場ではこのコードは使用できない。

 

sample03 shift-jis と utf が共存できるサンプルコード

「BOMが存在しない」事を認識するには、StreamReader の自動判定に任せる事はできない。

BOMの判定処理を手作りしなければならない。

そして、BOMの値を取り出すために 一度 FileStream でファイルを開き、そのインスタンスを StreamReader に渡して、テキストファイルを処理する必要がある。

sample03 がそのコードである。

まずBOM判定処理のコードである。

/// <summary>
/// Determine if it is BOM
/// </summary>
/// <param name="bom">Array to be inspected (4 bytes)</param>
/// <param name="codepage">output of Encoding.Codepage</param>
/// <returns>true=BOM.</returns>
static bool IsBOM(byte[] bomByte, out int codepage)
{
    bool result;
    byte[] bomUTF8 = { 0xEF, 0xBB, 0xBF };
    byte[] bomUTF16Little = { 0xFF, 0xFE };
    byte[] bomUTF16Big = { 0xFE, 0xFF };
    byte[] bomUTF32Little = { 0xFF, 0xFE, 0x00, 0x00 };
    byte[] bomUTF32Big = { 0x00, 0x00, 0xFE, 0xFF };

    if (IsMatched(bomByte, bomUTF8))
    {
        result = true;
        codepage = 65001; //utf-8,Unicode (UTF-8)
    }

    else if (IsMatched(bomByte, bomUTF32Little))
    {
        result = true;
        codepage = 12000; //utf-32,Unicode (UTF-32)
    }

    else if (IsMatched(bomByte, bomUTF32Big))
    {
        result = true;
        codepage = 12001; //utf-32BE,Unicode (UTF-32 Big-Endian) 
    }

    else if (IsMatched(bomByte, bomUTF16Little))
    {
        result = true;
        codepage = 1200; //utf-16,Unicode
    }

    else if (IsMatched(bomByte, bomUTF16Big))
    {
        result = true;
        codepage = 1201; //utf-16BE,Unicode (Big-Endian) 
    }

    else
    {
        result = false;
        //codepage = 0; //non BOM !
        codepage = 932; //shift_jis,Japanese (Shift-JIS)
    }

    return result;
}

/// <summary>
/// BOM sequence comparison
/// </summary>
/// <param name="data">Sequence to be inspected</param>
/// <param name="bom">BOM array</param>
/// <returns>true=match</returns>
static bool IsMatched(byte[] data, byte[] bom)
{
    bool result = true;

    for (int i = 0; i < bom.Length; i++)
    {
        if (bom[i] != data[i])
            result = false;
    }

    return result;
}

IsBOM は返値で、BOMが有れば true を返し、なければ false を返す。

BOMが有れば第二出力引数でコードページを返す。

BOMのない場合、codepage = 932 (shift_jis) の値を返しているが、この部分のコードを「codepage = 0; //non BOM !」に変えれば、BOMなしを呼び元で認識できる。

好きなように改造すると良い。

 

次が、FileStream でファイルを開き、そのインスタンスを StreamReader に渡して ReadToEnd() で読み込む処理だ。

// This is sample code where utf and shift-jis can coexist.
using (FileStream fs =
    new FileStream(fileName, FileMode.Open)
    )
{
    // I am read BOM of readfile.
    byte[] bom = new byte[4] {0xFF, 0xFF, 0xFF, 0xFF };
    int codepage;
    fs.Read(bom, 0, 4);
    fs.Position = 0;

    if (IsBOM(bom, out codepage))
    {
        Console.Write("BOM Value ");

        foreach (var b in bom)
        {
            Console.Write(":{0}", b.ToString("x2"));
        }
        Console.WriteLine(";");
    }
    else
    {
        Console.WriteLine("non BOM ! ;");
    }

    // Determine the encoding of StreamReader using FileStream.
    using (var reader =
        new StreamReader(fs, Encoding.GetEncoding(codepage))
        )
    {
        Console.WriteLine("Before reading content : {0},{1}\n",
            reader.CurrentEncoding.EncodingName,
            reader.CurrentEncoding.CodePage.ToString());

        fileContent = reader.ReadToEnd();

        Console.WriteLine("After reading content : {0},{1}\n",
            reader.CurrentEncoding.EncodingName,
            reader.CurrentEncoding.CodePage.ToString());
    }

    Console.WriteLine(fileContent);
}

FileStream でファイルを開いた直後、IsBOM でBOMの判定をしてから、StreamReader にコードページでエンコーディングを指定して、StreamReader インスタンスを作り、ReadToEnd() を呼び出す。

このコードはBOMが無い場合は、shift-jis で StreamReader インスタンスを作る。

 

これで、BOM有り utf-16LE のテキストファイルを読み込むように、実行すると以下のように表示する。

BOM Value :ff:fe:54:00;
Before reading content : Unicode,1200

After reading content : Unicode,1200

TESTストリングaaa日本語

shift-jis のテキストファイルを読み込むように、実行すると以下のように表示する。

non BOM ! ;
Before reading content : 日本語 (シフト JIS),932

After reading content : 日本語 (シフト JIS),932

TESTストリングaaa日本語

このコードなら、shift-jis と utf-8 が混在する現場でも使用できると思う。

ただし、「BOMなし utf-8」は使用できない。

「BOMなしは shift-jis テキストファイル」

「BOM有りは utf (UNICODE) テキストファイル」

というルールが成立している現場では、このコードが使用できるという意味だ。

 

「BOMなし utf-8」を使用できるようにするには、テキストファイル全体を読み込んで、文字コードを判定する処理を作らなければならない。

かなり複雑になる。

また、確実に判定できる保証もない。

業務の現場で「BOMなし utf-8」を使用するのは避ける事をお勧めする。

無駄な苦労をする必要は無い。

システムを作りやすいルールを定めるべきだ。

その方がコストを節約できる。

 

以上、このサンプルがお役に立てば幸いだ。


shift-jis と utf-8 の混在問題に関する記事(リンクリスト)に戻る


このブログを検索

Translate

人気の投稿

自己紹介

自分の写真
オッサンです。実務経験は Windows環境にて C#,VB.NET ,SQL Server T-SQL,Oracle PL/SQL,PostgreSQL,MariaDB。昔はDelphi,C,C++ など。 趣味はUbuntu,PHP,PostgreSQL,MariaDBかな ?基本無料のやつ。

QooQ