shift-jis と utf-8 の混在問題に関する記事(リンクリスト)に戻る
☆2021年11月8日更新: 時々 shift-jisをUTF-8と誤判定する不具合を修正しました。
Java による「BOMなし」のテキストファイルの文字コード(エンコーディング)を自動判定するプログラムを作成した。
サンプルコードとしてここに掲載する。
Java 8 を使用している。
前回、C# で作成したロジックをそのまま Java に移植した。
よってコード解説も C# と同じ文言を記載する。
最初にBOMの有無を判定し、BOMが存在する場合はBOMの文字エンコーディング名を表示する。
BOMが存在しなければ、文字コードのバイナリ値から、文字エンコーディングを推測する。
コマンドラインから、
Java EncodingJudgment.AutoJudg <テキストファイル名>
と入力すると判定結果を表示する。
表示するのは「文字エンコーディング名」と「BOMの有無」である。
推測方法としては、規格の厳しい文字エンコーディングから順に、文字エンコーディングの規格当てはまらないケースが存在しないか確認し、存在すれば「その文字エンコーディングでは無い」と判断して、次のエンコーディングを検査する。
検査は先頭1000byteのテキストコードだけで行う。
検査対象となる文字エンコーディングは、
BOM有りなら、
utf-8, utf-16LE, utf-16BE, utf-32LE, utf-32BE
BOMなしなら、
us-ascii, iso-2022-jp, utf-8, euc-jp, shift_jis
となる。
utf-16LE, utf-16BE, utf-32LE, utf-32BE の場合は、BOMなしでは判別できないので、自動判定の対象にはしていない。
最初にお断りしておくが、文字エンコーディングの自動推測は完全にはできない。
特に shift_jis の判定は難しく、このサンプルでも、他の文字エンコーディングの規格照合を先にやって、最後に shift_jis の判定を行っている。
他の文字エンコーディングは規格が厳しく、規格外判定がやりやすいが、 shift_jis はその判定が難しく、他の文字エンコーディング規格に当てはまらない事が判定基準の重要な要素になっている。
よって確実に動作する事は保証できない。
これは他の文字エンコーディング判定プログラムでも同じである。
ご自分でビルドして動作確認してみる事をお勧めする。
<2020-10-29追記>ゼロサイズテキストファイルに対応しました。
では全てのソースコードを以下に掲載する。
package EncodingJudgment;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class AutoJudg {
public static void main(String[] args) throws IOException, IOException {
//
if(args.length < 1) {
System.out.println("Please enter the file name.");
return;
}
String inputFilename = args[0];
final int BufferSize = 1000;
File file = new File(inputFilename);
try(FileInputStream fis = new FileInputStream(file)) {
byte[] readBuffer = new byte[BufferSize];
readBuffer[2] = (byte)(-1);
readBuffer[3] = (byte)(-1);
int readCount = fis.read(readBuffer, 0, BufferSize);
EncodingInformation ei = EncodingJudgment(readBuffer, readCount);
System.out.println("CharSetName = " + ei.CharsetName + " , BOM Exsist = " + ei.BomExist);
}
}
public static EncodingInformation EncodingJudgment(byte[] buffer, int sizeOfBuffer) {
EncodingInformation encodingInfo = new EncodingInformation();
boolean outOfSpecification;
StringRef charsetName = new StringRef();
// Check BOM.
if(IsBOM(buffer, charsetName)) {
encodingInfo.BomExist = true;
encodingInfo.CharsetName = charsetName.value;
return encodingInfo;
}
else {
encodingInfo.BomExist = false;
encodingInfo.CharsetName = null;
}
// if ISO-2022-JP or ASCII
BooleanRef isJIS = new BooleanRef();
outOfSpecification = JISEncodingJudgment(buffer, sizeOfBuffer, isJIS);
if(outOfSpecification == false)
{
if(isJIS.value == true)
{
//iso-2022-jp : Japanese (JIS)
encodingInfo.CharsetName = EncodingInformation.ISO2022JP;
}
else
{
//us-ascii : US-ASCII
encodingInfo.CharsetName = EncodingInformation.ASCII;
}
return encodingInfo;
}
// else if UTF-8
outOfSpecification = Utf8EncodingJudgment(buffer, sizeOfBuffer);
if (outOfSpecification == false)
{
//utf-8 : Unicode (UTF-8)
encodingInfo.CharsetName = EncodingInformation.UTF8;
return encodingInfo;
}
// else if EUC-JP
outOfSpecification = EUCJPEncodingJudgment(buffer, sizeOfBuffer);
if (outOfSpecification == false)
{
//EUC-JP : Japanese (JIS 0208-1990 and 0212-1990)
encodingInfo.CharsetName = EncodingInformation.EUCJP;
return encodingInfo;
}
// else if Shift_JIS
outOfSpecification = SJISEncodingJudgment(buffer, sizeOfBuffer);
if (outOfSpecification == false)
{
//shift_jis : Japanese (Shift-JIS)
encodingInfo.CharsetName = EncodingInformation.SHIFTJIS;
return encodingInfo;
}
// I do not know.
encodingInfo.CharsetName = null;
return encodingInfo;
}
static boolean IsBOM(byte[] bomByte, StringRef charsetName)
{
boolean result;
final byte[] bomUTF8 = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
final byte[] bomUTF16Little = { (byte) 0xFF, (byte) 0xFE };
final byte[] bomUTF16Big = { (byte) 0xFE, (byte) 0xFF };
final byte[] bomUTF32Little = { (byte) 0xFF, (byte) 0xFE, (byte) 0x00, (byte) 0x00 };
final byte[] bomUTF32Big = { (byte) 0x00, (byte) 0x00, (byte) 0xFE, (byte) 0xFF };
if (IsMatched(bomByte, bomUTF8))
{
result = true;
charsetName.value = EncodingInformation.UTF8; //utf-8,Unicode (UTF-8)
}
else if (IsMatched(bomByte, bomUTF32Little))
{
result = true;
charsetName.value = EncodingInformation.UTF32LE; //utf-32,Unicode (UTF-32)
}
else if (IsMatched(bomByte, bomUTF32Big))
{
result = true;
charsetName.value = EncodingInformation.UTF32BE; //utf-32BE,Unicode (UTF-32 Big-Endian)
}
else if (IsMatched(bomByte, bomUTF16Little))
{
result = true;
charsetName.value = EncodingInformation.UTF16LE; //utf-16,Unicode
}
else if (IsMatched(bomByte, bomUTF16Big))
{
result = true;
charsetName.value = EncodingInformation.UTF16BE; //utf-16BE,Unicode (Big-Endian)
}
else
{
result = false;
//charsetName.value = 0; //non BOM !
charsetName.value = EncodingInformation.SHIFTJIS; //shift_jis,Japanese (Shift-JIS)
}
return result;
}
static boolean IsMatched(byte[] data, byte[] bom)
{
boolean result = true;
for (int i = 0; i < bom.length; i++)
{
if (bom[i] != data[i])
result = false;
}
return result;
}
static boolean JISEncodingJudgment(byte[] buffer, int sizeOfBuffer, BooleanRef isJIS) {
boolean outOfSpecification = false;
boolean esc1 = false;
boolean esc2 = false;
byte[] byteESC1 = { 0x1B, 0x28, 0x42 };
byte[] byteESC2 = { 0x1B, 0x24, 0x42 };
byte[] backESC = { 0, 0, 0 };
// if ISO-2022-JP
for (int i = 0; i < sizeOfBuffer; i++)
{
if (0x80 <= (int)(buffer[i] & 0xFF))
{
outOfSpecification = true;
break;
}
else
{
backESC[0] = backESC[1];
backESC[1] = backESC[2];
backESC[2] = buffer[i];
if (esc1 == false && IsMatched(backESC, byteESC1))
{
esc1 = true;
}
if (esc2 == false && IsMatched(backESC, byteESC2))
{
esc2 = true;
}
}
}
if (esc1 || esc2)
{
isJIS.value = true;
}
else
{
isJIS.value = false;
}
return outOfSpecification;
}
static boolean Utf8EncodingJudgment(byte[] buffer, int sizeOfBuffer) {
boolean outOfSpecification = false;
outOfSpecification = false;
byte[] byteChar = new byte[6];
int byteCharCount = 0;
for (int i = 0; i < sizeOfBuffer; i++)
{
//
int bufferByte = (int)(buffer[i] & 0xFF);
if (0x80 <= (int)(buffer[i] & 0xFF))
{
byte char2byte = (byte)(0b11100000 & bufferByte);
if (char2byte == (byte)0b11000000)
{
outOfSpecification = Utf8OutOfSpecification(byteChar[0], byteCharCount, false);
if (outOfSpecification)
{
break;
}
byteChar[0] = char2byte;
byteCharCount = 1;
continue;
}
byte char3byte = (byte)(0b11110000 & bufferByte);
if (char3byte == (byte)0b11100000)
{
outOfSpecification = Utf8OutOfSpecification(byteChar[0], byteCharCount, false);
if (outOfSpecification)
{
break;
}
byteChar[0] = char3byte;
byteCharCount = 1;
continue;
}
byte char4byte = (byte)(0b11111000 & bufferByte);
if (char4byte == (byte)0b11110000)
{
outOfSpecification = Utf8OutOfSpecification(byteChar[0], byteCharCount, false);
if (outOfSpecification)
{
break;
}
byteChar[0] = char4byte;
byteCharCount = 1;
continue;
}
byte charSecond = (byte)(0b11000000 & bufferByte);
if (charSecond == (byte)0b10000000)
{
if (byteCharCount < 1)
{
outOfSpecification = true;
break;
}
byteChar[byteCharCount] = charSecond;
byteCharCount++;
outOfSpecification = Utf8OutOfSpecification(byteChar[0], byteCharCount, true);
if (outOfSpecification)
{
break;
}
continue;
}
outOfSpecification = true;
break;
}
else
{
// 7bit Char
byteChar[0] = 0;
byteCharCount = 0;
}
}
return outOfSpecification;
}
static boolean Utf8OutOfSpecification(byte topByteChar, int byteCharCount, boolean checkBig) {
boolean outOfSpecification = false;
if (topByteChar == (byte)0b11000000)
{
if(checkBig == true)
{
if(byteCharCount > 2) outOfSpecification = true;
}
else
{
if (byteCharCount < 2) outOfSpecification = true;
}
}
else if (topByteChar == (byte)0b11100000)
{
if (checkBig == true)
{
if (byteCharCount > 3) outOfSpecification = true;
}
else
{
if (byteCharCount < 3) outOfSpecification = true;
}
}
else if (topByteChar == (byte)0b11110000)
{
if (checkBig == true)
{
if (byteCharCount > 4) outOfSpecification = true;
}
else
{
if (byteCharCount < 4) outOfSpecification = true;
}
}
return outOfSpecification;
}
enum BYTECODE{ OneByteCode, TwoByteCode, KanaOneByte }
static boolean EUCJPEncodingJudgment(byte[] buffer, int sizeOfBuffer) {
boolean outOfSpecification = false;
BYTECODE beforeCode = BYTECODE.OneByteCode;
int byteCharCount = 0;
for (int i = 0; i < sizeOfBuffer; i++)
{
// 2 byte code
int bufferByte = (int)(buffer[i] & 0xFF);
if (0xA1 <= bufferByte && bufferByte <= 0xFE)
{
if(beforeCode == BYTECODE.KanaOneByte)
{
if(byteCharCount == 1)
{
byteCharCount = 2;
}
else
{
outOfSpecification = true;
break;
}
}
if (beforeCode == BYTECODE.TwoByteCode)
{
if (byteCharCount == 1)
byteCharCount = 2;
else if(byteCharCount == 2)
byteCharCount = 1;
}
beforeCode = BYTECODE.TwoByteCode;
}
// 1 byte code
else if(bufferByte <= 0x7F)
{
if (beforeCode == BYTECODE.TwoByteCode && byteCharCount == 1)
{
outOfSpecification = true;
break;
}
beforeCode = BYTECODE.OneByteCode;
byteCharCount = 1;
}
// katakana 2 byte code
else if(bufferByte == 0x8E && byteCharCount == 1)
{
beforeCode = BYTECODE.KanaOneByte;
byteCharCount = 1;
}
// impossible.
else
{
outOfSpecification = true;
break;
}
}
return outOfSpecification;
}
enum SJIS_BYTECODE{ OneByteCode, TwoByteCommon, TwoByteBefore, TwoByteAfter, KanaOneByte }
static boolean SJISEncodingJudgment(byte[] buffer, int sizeOfBuffer) {
boolean outOfSpecification = false;
SJIS_BYTECODE sjisByte = SJIS_BYTECODE.OneByteCode;
// if SJIS
outOfSpecification = false;
for (int i = 0; i < sizeOfBuffer; i++)
{
int bufferByte = (int)(buffer[i] & 0xFF);
if (bufferByte <= 0x7F)
{
sjisByte = SJIS_BYTECODE.OneByteCode;
}
else if (0xA1 <= bufferByte && bufferByte <= 0xDF)
{
sjisByte = SJIS_BYTECODE.KanaOneByte;
}
else if (0x81 <= bufferByte && bufferByte <= 0x9F)
{
sjisByte = SJIS_BYTECODE.TwoByteCommon;
}
else if (0xE0 <= bufferByte && bufferByte <= 0xEF)
{
if(sjisByte == SJIS_BYTECODE.TwoByteBefore)
{
outOfSpecification = true;
break;
}
sjisByte = SJIS_BYTECODE.TwoByteBefore;
}
else if (
(0x40 <= bufferByte && bufferByte <= 0x7E) ||
(0x80 <= bufferByte && bufferByte <= 0xFC)
)
{
if (sjisByte == SJIS_BYTECODE.TwoByteAfter)
{
outOfSpecification = true;
break;
}
sjisByte = SJIS_BYTECODE.TwoByteAfter;
}
else
{
outOfSpecification = true;
}
}
return outOfSpecification;
}
}
[StringRef.java]
package EncodingJudgment;
public class StringRef {
String value;
}
[BooleanRef.java]
package EncodingJudgment;
public class BooleanRef {
public boolean value;
}
[EncodingInformation.java]
package EncodingJudgment;
public class EncodingInformation {
public static final String UTF8 = "UTF-8";
public static final String UTF16LE = "UTF-16LE";
public static final String UTF16BE = "UTF-16BE";
public static final String UTF32LE = "UTF-32LE";
public static final String UTF32BE = "UTF-32BE";
public static final String MS932 = "MS932";
public static final String SHIFTJIS = "SJIS";
public static final String ASCII = "US-ASCII";
public static final String ISO2022JP = "ISO-2022-JP";
public static final String EUCJP = "EUC-JP";
public String CharsetName;
public boolean BomExist;
}
コード解説
先に説明したようにこのプログラムは、
先にBOMの有無を判定する。
BOMの判定に使用している関数は、以前掲載したものと同じ「IsBOM」である。これの解説は省略する。
BOMが無ければ、規格の厳しい文字エンコーディングから、規格外になるケースが無いか検査し、規格外のケースが存在すれば、その文字エンコーディングでは無いと判定する。
最初に規格外のケースが存在しない文字エンコーディングが、「その文字エンコーディングである」と判定する。
文字エンコーディングの判定の順番は以下の順になる。
encoding , codename
us-ascii , US-ASCII
iso-2022-jp , Japanese (JIS)
utf-8 , Unicode (UTF-8)
euc-jp , Japanese (EUC)
shift_jis , Japanese (Shift-JIS)
最初に「FileInputStream」でバイナリモードでファイルを1000バイト読み込み、そのバイナリ値で文字エンコーディングを判定する。
判定処理は「EncodingJudgment」関数の中にまとまっている。
「EncodingJudgment」の中でそれぞれの文字エンコーディングの規格外を探す関数を呼び出す。
全て、規格外を検出したら、true を変えす。
false を返したら、全て規格に収まっていることを意味する。
内部の関数は以下になる。
JISEncodingJudgment
ASCII か ISO-2022-JP の規格に全て当てはまるか検査する。
isJIS が true なら ISO-2022-JP を示し、false なら ASCII である事を示す。
文字コードが全て、0x80 未満であるか確認する。
また、JIS のエスケープシーケンスが存在するかどうかも判定している。
Utf8EncodingJudgment
utf-8 の規格に全て当てはまるか検査する。
文字コードが全て、utf-8 の文字コードの「器」を持っているか判定する。
「器」に当てはまらない場合は true を返す。
「器」は二進数で以下の値だ。
1バイト文字(07ビット) : 0-xxxxxxx
2バイト文字(11ビット) : 110-xxxxx,10-xxxxxx
3バイト文字(16ビット) : 1110-xxxx,10-xxxxxx,10-xxxxxx
4バイト文字(21ビット) : 11110-xxx,10-xxxxxx,10-xxxxxx,10-xxxxxx
バイナリ全てにビットマスクを掛け、器と同じ値が取れるか判定する。
一度でも取れなければ true を返す。
EUCJPEncodingJudgment
euc-jp の規格に全て当てはまるか検査する。
2バイトコードが一回しか登場しない、仮名コード「0x8E」の後に2バイトコードが登場しない、時に規格外と判定して true を返す。
SJISEncodingJudgment
shift-jis の規格に全て当てはまるか検査する。
これは厳密な判定が難しく、0xE0 から 0xEF のコードが続いたら true 、
0x40 から 0x7E と 0x80 から 0xFC(0x81 から 0x9F を除く) が続いたら true と判定している。
他の文字エンコーディングに当てはまらない事が重要な判定基準しなる。
要するに消去法で shift-jis か否かを判定している。
このコードは例外的ケースに使うべき
以前から説明しているように、Windows 環境においては「BOM有りはUTF」「BOMなしはshift-jis」というルールで複数の文字エンコーディングのテキストファイルを運用すべきである。
しかし、何かの事情で文字コードの分からないテキストファイルや、BOMなしUTFを作ってしまう場合もある。
Windowsでも標準的に 「BOMなし UTF-8」 を使用するので、「BOMなし UTF-8」の発生を完全に防ぐのは難しい。
そこで、せめてBOMなしの shfit-jis とutf-8 を検出できるようにする必要があると考えこのコードサンプルを作った。
しかし、 shfit-jis の検出が難しく、検出する為には他の文字エンコーディングに当てはまらない事を検出する必要があり、このようなプログラムになった。
決して、 shfit-jis とutf-8 以外の文字エンコーディングも同時運用しろと言っているわけではない。
その点は誤解しないで欲しい。
あくまで、職場での文字エンコーディングは「BOM有りはUTF」「BOMなしはshift-jis」というルールで運用して欲しい。
安全のために。