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

2020年11月18日水曜日

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

t f B! P L


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


これまで C# と Java で UNICODEテキストと shift-jisテキストが混在するPC環境で、BOM の有無を識別し UTFテキストと shift-jis テキストを識別する為に必要な、基礎知識とサンプルプログラムのソースコードを提供してきた。

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

 

今回は、PHP7.4 でBOM の有無を識別し UTFテキストと shift-jis テキストを識別するサンプルソースコードを掲載する。

標準ライブラリだけで作成しているので、どのフレームワークを使用していても導入できると思う。

サンプルはコンソールで動かすように作成した。

WEBアプリにはなっていない。

 

テキストファイルを読み込み、コンソールに表示する。

対応している文字エンコーディングは、いつもと同じで以下の物だ。

shift-jis, UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE

(UTFは全てBOM有りのみ)

 

BOMなしの UTF には対応していない。

「BOMなしは shift-jis、BOM有りは UTF」

というルールでプログラムを作成している。

BOMなしの UTF-8 などを読むと、shift-jis と解釈して文字化けするので注意して欲しい。

 

サンプルプログラム作成の中で「UTF-16LEとUTF-32LEのテキストを正攻法で読み込むと文字化けする」というPHPライブラリのバグを発見したので、このサンプルコードはそのバグの回避手段を反映したコードになっている。

UTF-16LEとUTF-32LEのテキストファイルも正常に読み込む事ができる。

 

PHPはバージョン「7.4.11」の「x64」を使用している。

改行コードはWindows と Linux の両方に対応し、

表示する文字エンコーディングは UTF-8 で出力している。

php.ini の mbstring 等の設定解説はしない。通常の日本語設定で PHP を使用して欲しい。

 

PHPライブラリのバグ解説

先にPHPライブラリのバグの解説をする。

これが分からないとサンプルコードの内容が理解できない。

 

PHP のテキスト文字列を扱うライブラリは、UTFエンコーディングを扱う部分にバグがある。

仕様上はPHPは多数の文字エンコーディングに対応しており、UTF系のエンコーディングは全てカバーしているはずだ。

しかし、改行コードを含む UTF-16LE と UTF-32LE のテキストは改行コード以降の文字が文字化けして、正しく読むことができない。

リトルエンディアンの文字エンコーディングが正しく読み取れないのだ。

これはPHPライブラリ全般が「複数バイト構成のUTFの改行コードの解釈を間違えている」から起きる不具合だ。

ビックエンディアンは正しく読み取れるが、偶然動いているだけだ。

 

UTF-16 と UTF-32 の改行コードは1バイトではない

shift-jis や UTF-8 の場合、改行コードは1バイトコードである。

Linux と Mac の場合は LF(ラインフィード) 一つだけ、

Windows の場合は CR(キャリッジリターン) と LF(ラインフィード) の二つで改行を表す。

それぞれの具体的なコード番号は、CR が「0x0D」、LF が「0x0A」(共に16進数)となる。

 

参考として shift-jis や UTF-8 の1バイト文字コードの例をあげると、

J = 0x4A , P = 0x50 , E = 0x45 , N = 0x4E

という文字コードがある。

 

英数字と標準的記号と制御コードは ASCII 7bit コードに定められていて、日本語文字コードでも同じ文字コード番号になる。

改行コードは制御コードに該当する。

shift-jis や UTF-8 では改行コードも1バイト文字コードである。

Windows なら「0x0D」と「0x0A」の 2つの1バイト文字コードで改行を表す。

 

一方、UTF-16 と UTF-32 は LE でも BE でも 2バイトか 4バイト文字コードだけしか存在しない。

特に UTF-32 は全て 4バイト文字コードだ。

1バイト文字コードは存在しないのだ。

 

当然、改行コードも1バイト文字コードではない。

LE と BE の区別なく、CPUのレジスタに読み込まれた値(状態)で改行コードを表すと、以下のようになる。

EncodingCRLF
UTF-160x00,0x0D0x00,0x0A
UTF-320x00,0x00,0x00,0x0D0x00,0x00,0x00,0x0A

 

Windows では UTF-16 の改行コードは「0x00,0x0D,0x00,0x0A」の 4バイトとなり、

UTF-32 なら「0x00,0x00,0x00,0x0D,0x00,0x00,0x00,0x0A」と 8バイトなる。

 

メモリやファイル上では LE (リトルエンディアン) の場合、バイトの並びが逆になる。

よって、UTF-16LE と UTF-32LE ではファイルの中での改行コードは以下のようになる。

EncodingCRLF
UTF-16LE0x0D,0x000x0A,0x00
UTF-32LE0x0D,0x00,0x00,0x000x0A,0x00,0x00,0x00

Windows では UTF-16LE の改行コードは「0x0D,0x00,0x0A,0x00」の 4バイトとなり、

UTF-32 なら「0x0D,0x00,0x00,0x00,0x0A,0x00,0x00,0x00」と 8バイトなる。

 

fgets のバグ

PHP の fgets 関数は、fopen 関数がオープンしたテキストファイルから、テキストを一行読み込みファイルポインタをその一行分進める。

一行の終わりは「改行コード」で判断する。

 

shift-jis や UTF-8 のテキストファイルを fgets で読む分には、正しく読み取る事ができる。

 

しかし、UTF-16 や UTF-32 のテキストファイルを fgets で読み取ろうとすると、先頭のBOMをプログラムで正しく処理していても、テキストを正しく読むことができない。

正確には二行目以降の文字が文字化けする。

 

なぜ、二行目以降の文字が文字化けするかと言えば、

「改行コードを1バイト文字コードとして読み込んでいる」からである。

 

具体的には、fgets は LF (0x0A) を読むと「そこが行の終わり」と判断する。

 

例えば、以下のような内容の二行の UTF-16LE テキストファイルがあるとする。

JP
EN

これをバイナリエディタで閲覧すると以下のような16進数の内容になる。

FF FE 4A 00 50 00 0D 00 0A 00 45 00 4E 00 0D 00 0A 00

BOM や文字単位に区切ると以下のような内容になる。

(BOM)FF FE 
(J)4A 00 (P)50 00 (CR+LF)0D 00 0A 00 
(E)45 00 (N)4E 00 (CR+LF)0D 00 0A 00 

ご覧のように (LF) 0x0A は行の最後ではない。

 

しかし、fgets 関数はこのテキストを (LF) 0x0A が行の終わりとして一行づつ読み込んでくる。

すると読み込んだテキストの内容はバイナリパターンで以下のようになる。

 

[ 1 行目 ]

(BOM)FF FE (J)4A 00 (P)50 00 (CR+LF)0D 00 0A 

[ 2 行目 ]

00 (E)45 00 (N)4E 00 (CR+LF)0D 00 0A 

[ 3 行目 ]

00 

 

「 1 行目 」の最後の「0x00」が読み込まれず、「 2 行目 」の先頭に現われてしまう。

「 2 行目 」の最後の「0x00」も読み込まれず、「 3 行目 」の先頭に現われてしまう。

PHP はこの行の文字列を先頭から 2バイト文字として解釈するので、「 2 行目 」の先頭文字は「0x00,0x45」として解釈してしまい、それ以降「0x00,0x4E」「0x00,0x0D」と1バイトづつズレて解釈する。

これが文字化けの原因である。

 

UTF-32LE なら改行コードLFは「0x0A,0x00,0x00,0x00」となるので、0x0A の後ろの「0x00,0x00,0x00」が2行目の先頭に来てしまい、次の文字の先頭と合わせて4バイト文字として解釈されて、文字化けする。

 

BE (ビックエンディアン) の場合は「改行の最後」が偶然「0x0A」になっているから、正常に読み込めるが、改行コードを 1バイトコードと解釈しているので、そのロジックが正しく実装されているとは言えないと思う。

 

他の関数も同様のバグを抱えている

テキストファイルの読み書きを行う PHP の関数は全て同様のバグを抱えている。

 

mb_convert_encoding と、file_get_contents

file_get_contents 関数は第一引数で指定したファイル名のテキストを全て読み込み、文字列で返す。

この時、UTF-16LE や UTF-32LE で読み込んだ文字列は、テキストファイル内のバイナリのまま文字列変数内に格納されている。

これをプログラム中で編集する時に、内部文字コード(mb_internal_encoding)が、もし UTF-8 なら、mb_convert_encoding 関数で UTF-16LE や UTF-32LE から、 UTF-8 へ変換する事になる。

 

[例 : UTF-16LE から UTF-8 へ変換する]

mb_internal_encoding("UTF-8");

$fileName = 'text16le_bom.txt';

$content = file_get_contents($fileName);

$content = mb_convert_encoding($content, "UTF-8", "UTF-16LE");

echo $content;

 

mb_convert_encoding 関数は fgets と同様に、「0x0A」を行の終わりと解釈するため、2行目以降が文字化けしてしまう。

ちなみにこの場合、先頭のBOMは文字化けして表示される。

しかし、1行目は正しく表示される。

 

参考までに、file_get_contents で読み込んだ文字列を、

そのまま変換も編集もしないで file_get_contents で別のファイルに書き込んだ場合は、

正しく書き込まれる。

mb_internal_encoding("UTF-8");

$fileName = 'text16le_bom.txt';

$content = file_get_contents($fileName);

$byteSize = file_put_contents("out_$fileName", $content);

echo "byteSize = $byteSize" . PHP_EOL;

 

readfile($fileName)

この関数は引数で指定したファイル名のファイルを全て読み込み、表示する。

これは読みこんだ文字エンコーディングをそのまま表示するため、文字エンコーディングを変換する事ができない。

UTF-16 や UTF-32 を表示できないため、通常は使用できないと思う。

 

$lines = file($fileName)

file 関数は引数で指定したファイル名のファイルを全て読み込み、行ごとに文字列の配列として $lines に返す。

これも fgets と同様に改行以降が文字化けする。

 

fwrite は問題ない

テキストを書く側の fwrite 関数 には問題はない。

fwrite 関数 はBOMや改行をまったく認識する事なく、与えられた変数バッファの内容をそのままファイルに書き出す。

BOMも改行コードもプログラムで適切に出力すれば、問題なく使用できる。

 

最初に全部のサンプルソースコードを掲載して、後にコードの解説を行う。

 

PHP7.4用の全サンプルソースコード

 

<?php
//--- require_once begin ---
mb_language("Japanese");

//Ecoding Name
define('SJIS', 'Windows-31J');
define('UTF8', 'UTF-8');
define('UTF16LE', 'UTF-16LE');
define('UTF16BE', 'UTF-16BE');
define('UTF32LE', 'UTF-32LE');
define('UTF32BE', 'UTF-32BE');

mb_internal_encoding(UTF8);
//mb_http_output(UTF8);

//preg_match() first argument.
define('PREG_UTF8', '/\A\xef\xbb\xbf/');
define('PREG_UTF16LE', '/\A\xff\xfe/');
define('PREG_UTF16BE', '/\A\xfe\xff/');
define('PREG_UTF32LE', '/\A\xff\xfe\x00\x00/');
define('PREG_UTF32BE', '/\A\x00\x00\xfe\xff/');

//Encoding BOM length.
define('BOMLEN_UTF8', 3);
define('BOMLEN_UTF16LE', 2);
define('BOMLEN_UTF16BE', 2);
define('BOMLEN_UTF32LE', 4);
define('BOMLEN_UTF32BE', 4);

//Encoding CR+LF
define('PHP_EOL_CRLF', "\x0D\x0A");

define('CRLF_BEFORE_SJIS', "\x0D\x0A");
define('CRLF_BEFORE_UTF8', "\x0D\x0A");
define('CRLF_BEFORE_UTF16LE', "\x0D\x00\x0A");
define('CRLF_BEFORE_UTF16BE', "\x00\x0D\x00\x0A");
define('CRLF_BEFORE_UTF32LE', "\x0D\x00\x00\x00\x0A");
define('CRLF_BEFORE_UTF32BE', "\x00\x00\x00\x0D\x00\x00\x00\x0A");

define('CRLF_AFTER_SJIS', '');
define('CRLF_AFTER_UTF8', '');
define('CRLF_AFTER_UTF16LE', '\x00');
define('CRLF_AFTER_UTF16BE', '');
define('CRLF_AFTER_UTF32LE', '\x00\x00\x00');
define('CRLF_AFTER_UTF32BE', '');

//Encoding LF
define('LF_BEFORE_SJIS', "\x0A");
define('LF_BEFORE_UTF8', "\x0A");
define('LF_BEFORE_UTF16LE', "\x0A");
define('LF_BEFORE_UTF16BE', "\x00\x0A");
define('LF_BEFORE_UTF32LE', "\x0A");
define('LF_BEFORE_UTF32BE', "\x00\x00\x00\x0A");

define('LF_AFTER_SJIS', '');
define('LF_AFTER_UTF8', '');
define('LF_AFTER_UTF16LE', '\x00');
define('LF_AFTER_UTF16BE', '');
define('LF_AFTER_UTF32LE', '\x00\x00\x00');
define('LF_AFTER_UTF32BE', '');

define('PREG_BEFORE', '/\A');
define('PREG_AFTER', '/');
//--- require_once end ---


function ReadBomTextFile($readFileName) {
    
    $resultText = '';
    $internalEncoding = mb_internal_encoding();
    $first = TRUE;
    $encoding = '';
    $bomLen = 0;
    $enableEcho = TRUE;
    $crlf_before = '';
    $crlf_after = '';
    
    //Check CR+LF.
    if(PHP_EOL == PHP_EOL_CRLF){
        $isCrlf = TRUE;
    }
    else{
        $isCrlf = FALSE;
    }
    
    //Open File.
    $fp = fopen($readFileName, 'r');
    if($fp == FALSE){
        echo "Error fopen($readFileName) !" . PHP_EOL;
        return FALSE;
    }
    
    //Loop Of Row.
    while (!feof($fp)) {
        
        //Read File.
        $text = fgets($fp);
        if($fp == FALSE){
            echo "Error fgets() !" . PHP_EOL;
            fclose($fp);
            return FALSE;
        }
        
        //Check BOM.
        if($first == TRUE){
            if (preg_match(PREG_UTF8, $text)) {
                //UTF-8.
                $bomLen = BOMLEN_UTF8;
                $encoding = UTF8;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF8;
                    $crlf_after = CRLF_AFTER_UTF8;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF8;
                    $crlf_after = LF_AFTER_UTF8;
                }
            }
            else if(preg_match(PREG_UTF32LE, $text)){
                //UTF-32LE.
                $bomLen = BOMLEN_UTF32LE;
                $encoding = UTF32LE;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF32LE;
                    $crlf_after = CRLF_AFTER_UTF32LE;
                    //$crlf_after = PREG_CRLF_AFTER_UTF32LE;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF32LE;
                    $crlf_after = LF_AFTER_UTF32LE;
                    //$crlf_after = PREG_LF_AFTER_UTF32LE;
                }
            }
            else if(preg_match(PREG_UTF32BE, $text)){
                //UTF-32BE.
                $bomLen = BOMLEN_UTF32BE;
                $encoding = UTF32BE;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF32BE;
                    $crlf_after = CRLF_AFTER_UTF32BE;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF32BE;
                    $crlf_after = LF_AFTER_UTF32BE;
                }
            }
            else if(preg_match(PREG_UTF16LE, $text)){
                //UTF-16LE.
                $bomLen = BOMLEN_UTF16LE;
                $encoding = UTF16LE;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF16LE;
                    $crlf_after = CRLF_AFTER_UTF16LE;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF16LE;
                    $crlf_after = LF_AFTER_UTF16LE;
                }
            }
            else if(preg_match(PREG_UTF16BE, $text)){
                //UTF-16BE.
                $bomLen = BOMLEN_UTF16BE;
                $encoding = UTF16BE;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF16BE;
                    $crlf_after = CRLF_AFTER_UTF16BE;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF16BE;
                    $crlf_after = LF_AFTER_UTF16BE;
                }
            }
            else{
                //Windows Shift-JIS.
                $bomLen = 0;
                $encoding = SJIS;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_SJIS;
                    $crlf_after = CRLF_AFTER_SJIS;
                }
                else {
                    $crlf_before = LF_BEFORE_SJIS;
                    $crlf_after = LF_AFTER_SJIS;
                }
            }
            
            //Change to preg_match() first parameter. 
            $preg_crlf_after = PREG_BEFORE . $crlf_after . PREG_AFTER;
            
            //Delete BOM.
            if($bomLen != 0){
                $text = substr($text, $bomLen);
            }
            
            $first = FALSE;
            
            //Output EncodingName to the Console.
            if($enableEcho == TRUE) 
                echo $encoding . PHP_EOL ;
        }
        
        //Delete ZERO CODE for CR+LF fragments (Only LE).
        if($encoding == UTF16LE && preg_match($preg_crlf_after, $text)){
            $text = substr($text, 1);
        }
        else if($encoding == UTF32LE && preg_match($preg_crlf_after, $text)){
            $text = substr($text, 3);
        }
        
        //Delete CR+LF CODE.
        $text = str_replace($crlf_before, '', $text);
        
        //Convert Encoding.
        $text = mb_convert_encoding($text, $internalEncoding, $encoding);
        
        //Append to result text.
        $resultText .= $text . PHP_EOL;
        
        //Output Text to the Console.
        if($enableEcho == TRUE){
            echo $text . PHP_EOL;
            //echo bin2hex($text) . PHP_EOL;
        }
    }
        
    //Close File.
    fclose($fp);
    
    return $resultText;
}


//Init parameters.

//$fileName = 'textsjis.txt';
//$fileName = 'text8.txt';
//$fileName = 'text8_bom.txt';
$fileName = 'text16le_bom.txt';
//$fileName = 'text16be_bom.txt';
//$fileName = 'text32le_bom.txt';
//$fileName = 'text32be_bom.txt';
//$fileName = 'notExsit.txt';

//Call Function.
$resultText = ReadBomTextFile($fileName);

echo "---Result---." . PHP_EOL;
echo $resultText;

//All End.

 

サンプルコードの解説

ソースの始めの部分の「//--- require_once begin ---」から「//--- require_once end ---」で囲まれた部分は、別のソースファイルに移動して「require_once()」関数などでインクルードして使用できるように、コードを書いている。

 

ヘッダー定数

「//Ecoding Name」の部分は、文字エンコーディングの名前を定義している。

プログラム中では、define の右側の値を使用する。

他の define でも同様。

 

「//preg_match() first argument.」の部分は、テキストファイルの先頭のBOMがそれぞれのUTFのBOMと一致するから判定する為の、正規表現を定義している。

 

「//Encoding BOM length.」は、それぞれのUTFのBOMのバイト長。

「//Encoding CR+LF」は、改行コードが WIndows 環境か、Linux や Mac 環境になるか、を判定する為に使用する。PHP_EOL とこれを比較して判定する。

//Encoding CR+LF
define('PHP_EOL_CRLF', "\x0D\x0A");

その下は Windows環境の改行コードの「0x0A」より前のバイナリパターンを定義している。

これを比較照合して、文字列中の改行コードを検出する。

コードパターンに展開するので二重引用符で定義している。

define('CRLF_BEFORE_SJIS', "\x0D\x0A");
define('CRLF_BEFORE_UTF8', "\x0D\x0A");
define('CRLF_BEFORE_UTF16LE', "\x0D\x00\x0A");
define('CRLF_BEFORE_UTF16BE', "\x00\x0D\x00\x0A");
define('CRLF_BEFORE_UTF32LE', "\x0D\x00\x00\x00\x0A");
define('CRLF_BEFORE_UTF32BE', "\x00\x00\x00\x0D\x00\x00\x00\x0A");

その後ろは、改行コードの「0x0A」より後ろのバイナリパターンを定義している。

2行目以降の読み取り文字列の先頭にこのバイナリパターンが付いているので、これを使用して検出削除する。

使用しているのは「CRLF_AFTER_UTF16LE」と「CRLF_AFTER_UTF32LE」だけである。

preg_match 関数で正規表現として使用するので、シングル引用符で定義している。

define('CRLF_AFTER_SJIS', '');
define('CRLF_AFTER_UTF8', '');
define('CRLF_AFTER_UTF16LE', '\x00');
define('CRLF_AFTER_UTF16BE', '');
define('CRLF_AFTER_UTF32LE', '\x00\x00\x00');
define('CRLF_AFTER_UTF32BE', '');

 

「//Encoding LF」の部分は、LInux と Mac 環境の改行コードのバイナリパターンを定義している。

//Encoding LF
define('LF_BEFORE_SJIS', "\x0A");
define('LF_BEFORE_UTF8', "\x0A");
define('LF_BEFORE_UTF16LE', "\x0A");
define('LF_BEFORE_UTF16BE', "\x00\x0A");
define('LF_BEFORE_UTF32LE', "\x0A");
define('LF_BEFORE_UTF32BE', "\x00\x00\x00\x0A");

define('LF_AFTER_SJIS', '');
define('LF_AFTER_UTF8', '');
define('LF_AFTER_UTF16LE', '\x00');
define('LF_AFTER_UTF16BE', '');
define('LF_AFTER_UTF32LE', '\x00\x00\x00');
define('LF_AFTER_UTF32BE', '');

 

その次は、preg_match() でバイナリパターンを検出する時に使用する初めと終わりの区切り記号である。

define('PREG_BEFORE', '/\A');
define('PREG_AFTER', '/');

 

メイン処理 ReadBomTextFile 関数

メイン処理の ReadBomTextFile関数を定義している。

引数で指定したファイルを読み、返値に行単位の文字列配列を返す。

例外処理は書いていない。

一部、エラーの時はメッセージを表示して終了するようにしてはいる。

function ReadBomTextFile($readFileName) {

基本的な処理の枠組みは以下のような処理になる。

ファイルを開いて、ファイルの最後までループで回し、一行づつ読み込み、BOM判定して、文字エンコーディングを内部文字エンコーディングに変換して、返値にセットする。

最後まで読んだらファイルを閉じて、返値を返し終了する。


$fp = fopen($readFileName, 'r');

while (!feof($fp)) {

	$text = fgets($fp);
	
	//文字エンコーディング判定
	if(preg_match('BOM',$text)){
		$encoding = 文字エンコーディング;
	}
	
	//BOM削除
	$text = substr($text, $bomLen);
	
	//文字エンコーディング変換
	$text = mb_convert_encoding($text, $内部文字エンコーディング, $encoding);
	
	//返値へ追加
	$resultText .= $text . PHP_EOL;
}

fclose($fp);
    
return $resultText;

 

詳細の解説をする。

    $internalEncoding = mb_internal_encoding();

内部文字エンコーディングを取得する。

ソースの先頭で「mb_internal_encoding(UTF8);」と宣言しているので、この値は「UTF8」である。

その他は、必要な変数の初期値である。

$first は最初の行だけBOMの判定をするため TRUE にしている。

最初の判定後、FALSE になる。以後変わらない。

$enableEcho = TRUE; は、処理の途中で読み込んだ値をコンソールに表示するフラグである。

これを FALSE にすると処理の途中で値を表示しない。

邪魔ならば FALSE にする事をお勧めする。

 

    //Check CR+LF.
    if(PHP_EOL == PHP_EOL_CRLF){
        $isCrlf = TRUE;
    }
    else{
        $isCrlf = FALSE;
    }

現在実行している環境が、Windowsの改行か、Linux や Mac の改行が判定している。

 

    //Open File.
    $fp = fopen($readFileName, 'r');
    if($fp == FALSE){
        echo "Error fopen($readFileName) !" . PHP_EOL;
        return FALSE;
    }

ファイルを開いている。開けなければ終了する。

 

    //Loop Of Row.
    while (!feof($fp)) {
        
        //Read File.
        $text = fgets($fp);
        if($fp == FALSE){
            echo "Error fgets() !" . PHP_EOL;
            fclose($fp);
            return FALSE;
        }

ファイルの最後までループして、一行づつテキストファイルの読み込みを行う。

読めなければ終了する。

 

        //Check BOM.
        if($first == TRUE){
            if (preg_match(PREG_UTF8, $text)) {
                //UTF-8.
                $bomLen = BOMLEN_UTF8;
                $encoding = UTF8;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF8;
                    $crlf_after = CRLF_AFTER_UTF8;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF8;
                    $crlf_after = LF_AFTER_UTF8;
                }
            }
            else if(preg_match(PREG_UTF32LE, $text)){
                //UTF-32LE.
                $bomLen = BOMLEN_UTF32LE;
                $encoding = UTF32LE;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF32LE;
                    $crlf_after = CRLF_AFTER_UTF32LE;
                    //$crlf_after = PREG_CRLF_AFTER_UTF32LE;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF32LE;
                    $crlf_after = LF_AFTER_UTF32LE;
                    //$crlf_after = PREG_LF_AFTER_UTF32LE;
                }
            }
            else if(preg_match(PREG_UTF32BE, $text)){
                //UTF-32BE.
                $bomLen = BOMLEN_UTF32BE;
                $encoding = UTF32BE;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF32BE;
                    $crlf_after = CRLF_AFTER_UTF32BE;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF32BE;
                    $crlf_after = LF_AFTER_UTF32BE;
                }
            }
            else if(preg_match(PREG_UTF16LE, $text)){
                //UTF-16LE.
                $bomLen = BOMLEN_UTF16LE;
                $encoding = UTF16LE;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF16LE;
                    $crlf_after = CRLF_AFTER_UTF16LE;
                    //$crlf_after = PREG_CRLF_AFTER_UTF16LE;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF16LE;
                    $crlf_after = LF_AFTER_UTF16LE;
                    //$crlf_after = PREG_LF_AFTER_UTF16LE;
                }
            }
            else if(preg_match(PREG_UTF16BE, $text)){
                //UTF-16BE.
                $bomLen = BOMLEN_UTF16BE;
                $encoding = UTF16BE;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_UTF16BE;
                    $crlf_after = CRLF_AFTER_UTF16BE;
                }
                else {
                    $crlf_before = LF_BEFORE_UTF16BE;
                    $crlf_after = LF_AFTER_UTF16BE;
                }
            }
            else{
                //Windows Shift-JIS.
                $bomLen = 0;
                $encoding = SJIS;
                if($isCrlf) {
                    $crlf_before = CRLF_BEFORE_SJIS;
                    $crlf_after = CRLF_AFTER_SJIS;
                }
                else {
                    $crlf_before = LF_BEFORE_SJIS;
                    $crlf_after = LF_AFTER_SJIS;
                }
            }

これは、fgets で読み込んだ文字列の先頭の BOM 判定を行う。

BOMがなければ shift-jis と判定する。

BOMを検出した場合、

$bomLen (BOMのバイト長)、

$encoding (文字エンコーディング名)、

$crlf_before (0x0A以前の改行コード)、

$crlf_after (0x0A以降の改行コード)、

を設定する。

 

            //Change to preg_match() first parameter. 
            $preg_crlf_after = PREG_BEFORE . $crlf_after . PREG_AFTER;

後の「2行目以降の先頭の改行コードの0x00の値」を検出する為の、正規表現の条件を作成している。

 

            //Delete BOM.
            if($bomLen != 0){
                $text = substr($text, $bomLen);
            }

shift-jis 以外 (BOM有) の場合、BOMを削除している。

 

            $first = FALSE;

BOM判定は一度だけで良いので、この if スコープに次は入らない。

$first フラグを FALSE にして if 文を抜ける。

 

            //Output EncodingName to the Console.
            if($enableEcho == TRUE) 
                echo $encoding . PHP_EOL ;

コンソールに検出した文字エンコーディング名を表示している。

これは削除しても構わない。

 

        }

「 if($first == TRUE) 」文のBOM判定処理を抜ける。

 

        //Delete ZERO CODE for CR+LF fragments (Only LE).
        if($encoding == UTF16LE && preg_match($preg_crlf_after, $text)){
            $text = substr($text, 1);
        }
        else if($encoding == UTF32LE && preg_match($preg_crlf_after, $text)){
            $text = substr($text, 3);
        }

LEの文字エンコーディングの場合、2行目以降の文字列先頭に改行コードの欠片「0x00」が、登場する。

これを検出して削除している。

UTF16LE か UTF32LE の時しか動作しない。

 

        //Delete CR+LF CODE.
        $text = str_replace($crlf_before, '', $text);

改行コードの「0x0A」以前のバイナリパターンを削除している。

 

        //Convert Encoding.
        $text = mb_convert_encoding($text, $internalEncoding, $encoding);
        
        //Append to result text.
        $resultText .= $text . PHP_EOL;

文字エンコーディングを内部文字エンコーディングに変換して、返り値に追加設定している。

返り値には内部の改行コードを再設定している。

テキストファイルと実行環境の改行コードが異なる場合への対処でもある。

 

        //Output Text to the Console.
        if($enableEcho == TRUE){
            echo $text . PHP_EOL;
            //echo bin2hex($text) . PHP_EOL;
        }
    }

返り値にセットした一行の値をコンソールに表示して、ループの最後となる。

bin2hex はバイナリを16進数で表示したい時に使用する。

このコンソールに表示する処理は削除しても良い。

$enableEcho が FALSE なら表示しない。

 

    //Close File.
    fclose($fp);
    
    return $resultText;
}

ファイルを閉じて、返り値を返す。

function ReadBomTextFile の終了。

 

//Init parameters.

//$fileName = 'textsjis.txt';
//$fileName = 'text8.txt';
//$fileName = 'text8_bom.txt';
$fileName = 'text16le_bom.txt';
//$fileName = 'text16be_bom.txt';
//$fileName = 'text32le_bom.txt';
//$fileName = 'text32be_bom.txt';
//$fileName = 'notExsit.txt';

//Call Function.
$resultText = ReadBomTextFile($fileName);

echo "---Result---." . PHP_EOL;
echo $resultText;

//All End.

ReadBomTextFile 関数を呼び出す。

$fileName = 'text16le_bom.txt'; で、好きなファイル名を指定する。

echo $resultText; で返り値を表示しているので、コンソールに表示している場合は、ファイルの内容を二回表示する。

 

テスト用のデータはご自分でご用意ください

テスト用のテキストファイルは「メモ帳」でも「nkf」コマンドでも、一般のフリーソフトでも作成できますので、ご自分でご用意ください。

 

このコードがお役に立てば、幸いです。

 


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