ファイルのエンコーディングを一括変換
古くから存在するJavaプロジェクトのソースコードがSJISだったのですが、Eclipseではそれが問題になることはありませんでした。
最近になってEclipseのJava環境をVSCodeに変更しようとした際、致命的ではないのですが拡張ライブラリで5C問題が起きることが判明しました。
そこで今回はフォルダと拡張子を指定して、SJISのエンコーディングのファイルをUTF-8にコンバートしたいと思います。
5C問題
5C問題の5Cとは文字コードを指しており、\にあたります。
\はエスケープシーケンスとして扱われることが多い文字ですが、SJISの文字コード上では2バイト目にこの5Cが出現することがあります。
環境によってはそれらの文字に含まれる5Cがエスケープシーケンスと判定されて不具合を引き起こすことがあります。具体的にはコンパイラがソースコードを読もうとするとき正常に読むことができずにエラーとなったりします。
そのような文字はそれほど多く存在しませんが、わりとよく使う文字の中にも存在します。たとえば、「 ソ 」というカタカナはSJISのコードで「 83 5c 」、「 表 」という漢字は「 95 5c 」、となり、これらは一部の環境で5C問題を引き起こす原因となります。
Javaのソースコードにおける文字コード
Javaではコンパイル時に文字列はUTF-16に変換されます。
変換の為には元となるファイルのエンコーディングも判明していないといけないのですが、それはデフォルトだとOSのエンコーディングであるシステムプロパティ(file.encoding) が採用されます。
ですので、SJISで保存していた今までは気にする必要がなかったのですが、UTF-8でソースファイルを保存して運用する場合はコンパイル時に「 javac -encoding UTF-8 ... 」として入力ファイルのエンコーディングを指定するか、「 javac -J-Dfile.encoding=UTF-8 」としてシステム全体で利用するエンコーディングを指定する必要がでてきます。
ちなみに UTF-8 と UTF-16は名前は似ていますがまったく違う文字コードです。UTF-8は1~4バイトで文字を保存しASCII(1バイト)と互換性があります。
まとめると、既存のSJISのコードをUTF-8に変換して、コンパイル時にはエンコーディングにUTF-8を明示するように変更すればいいということになります。
ソースコードの変換
文字コードの変換は、テキストエディタなどで保存し直せばいいのですが、大きなプロジェクトだとその作業だけでも大変ですし、混在している途中はリリースができません。
なので、Python3で指定したディレクトリより下の階層にある拡張子が.javaファイルの文字コードを変換して別のフォルダに生成するスクリプトを組んでみました。
ファイルの文字コードはすべてSJISになっている前提です。たぶんSJIS以外のエンコーディングの場合は読み込み時にエラーになると思いますが、チェックはしていませんのでご了承ください。
丸に数値が入るものなどの(機種依存)文字では、またSJISからUTF-8に変換するの際にエラーになるのでエラーをトラップしてファイル単位で変換を中止します。
エラーが出た場合は、エラー箇所が表示されますので元のソースファイルを修正します。
変換でエラーが出なくなったら、変換後のファイル群を元のプロジェクトのソースへ上書きします。
IDEを利用している場合は、そちらの文字コード設定をを変更する必要があることがあります。
conv-encoding.py
import os
import codecs
input_directory = './input'
output_directory = './output'
for dirpath, dirnames, filenames in os.walk(input_directory):
for filename in filenames:
if filename.endswith('.java'):
# 入力ファイルと出力ファイルのパスを作成します。
input_path = os.path.join(dirpath, filename)
# 出力ディレクトリに同じディレクトリ構造を作成します。
relative_path = os.path.relpath(dirpath, input_directory)
output_dirpath = os.path.join(output_directory, relative_path)
os.makedirs(output_dirpath, exist_ok=True)
output_path = os.path.join(output_dirpath, filename)
try:
# Shift_JISエンコーディングでファイルを開き、その内容を読み込みます。
with codecs.open(input_path, 'r', 'Shift_JIS') as input_file:
content = input_file.read()
# UTF-8エンコーディングで新しいファイルを作成し、変換した内容を書き込みます。
with codecs.open(output_path, 'w', 'UTF-8') as output_file:
output_file.write(content)
except UnicodeDecodeError as e:
# エラーの起きた行を取得
print(f"Error converting file {input_path} at position {e.start}")
with open(input_path, 'rb') as error_file:
data = error_file.read()
linecnt = data[:e.start].decode('Shift_JIS', 'ignore').count('\r\n') + 1
print("Error line may be ", linecnt)
continue
print('End.')
※ 利用時には元のファイルのバックアップを必ず取っておいてください。
筆者はWindows環境のPythonがなんとなく苦手なので、Windows Subsystem for Linux の Ubuntu20.4で実行しました。
Ubuntuでは /mnt/c としてCドライブが利用できますのでc:\conv ディレクトリ内にsrcフォルダを生成して実行します。
# cd /mnt/c/conv # python3 conv-encoding.py ...
PHPでのコード
普段、筆者はあまりPythonを使わないのですが、PHPでやろうとしたところ非効率なコードになってしまった為に先のようにPython3を使うようにしました。
今回のような場合は、変換エラーになる箇所を確認して許容できるか否かを確認する必要があります。
PHPの文字コード変換関数である mb_convert_encoding はエラー時に false を返す仕様になっていますが、変換できない文字があった場合にはエラーにはならず ? が返ってくるようです。
なので、変換エラーを検出したいのなら文字列を1文字ずつ処理をして、元のデータが ? ではない時に変換後のデータが ? だった場合にエラーを検出するという方法になると思います。
この処理は先のPythonのコードに比べて非常に遅いです。また、ソースファイルのエンコーディングが違っても読めてしまうので、環境によってはそのチェックも必要になると思います。
conv-encoding.php
<?php
$inputDirectory = './input';
$outputDirectory = './output';
$directoryIterator = new RecursiveDirectoryIterator($inputDirectory, RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
if ($file->getExtension() === 'java') {
$inputPath = $file->getPathname();
$relativePath = str_replace($inputDirectory, '', $file->getPath());
$outputDirPath = $outputDirectory . $relativePath;
if (!file_exists($outputDirPath)) {
mkdir($outputDirPath, 0777, true);
}
$outputPath = $outputDirPath . '/' . $file->getBasename();
try {
//---- 変換エラーを無視してまとめて処理するなら ----------
//$content = file_get_contents($inputPath);
//$decodedContent = mb_convert_encoding($content, 'UTF-8', 'Shift_JIS');
//file_put_contents($outputPath, $decodedContent);
//---------------------------------------------------------
//一文字ずつチェック
$content = file_get_contents($inputPath);
$decodedContent ="";
for($i = 0, $max=mb_strlen($content,"Shift_JIS"); $i < $max; $i++) {
$s =mb_substr($content,$i,1,"Shift_JIS");
$c =@mb_convert_encoding($s, 'UTF-8', 'Shift_JIS');
if ($c=== false) {
//変換エラー(文字コード指定エラー以外はfalseが返ることはないと思います)
echo "Error converting file " . $inputPath . " at positon ". $i . "\r\n";
$wk = mb_substr_count($decodedContent,"\r\n","UTF-8") + 1;
echo "Error line may be ".$wk ."\r\n";
} else if($c==="?") {
if ($s!="?") {
//変換エラー
echo "Error converting file " . $inputPath . " at positon ". $i . "\r\n";
$wk = mb_substr_count($decodedContent,"\r\n","UTF-8") + 1;
echo "Error line may be ".$wk ."\r\n";
}
}
$decodedContent.=$c;
}
file_put_contents($outputPath, $decodedContent);
} catch (Exception $e) {
echo "Error converting file " . $inputPath . "\r\n";
}
}
}
echo "End.";
?>