文件編碼不一致是亂碼高發區。本文實現一個健壯的 EncodingConverter 工具類:自動檢測源文件編碼 (借助 juniversalchardet 或 CPDetector)、按行讀取轉碼、原子性寫入 (臨時文件 + 重命名)、完整異常回滾機制。
<dependency>
<groupId>com.googlecode.juniversalchardet</groupId>
<artifactId>juniversalchardet</artifactId>
<version>1.0.3</version>
</dependency>
<!-- 或使用 Mozilla 通用檢測庫 -->
import org.mozilla.universalchardet.UniversalDetector;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
public class EncodingConverter {
/** 檢測文件編碼 */
public static String detectEncoding(File file) throws IOException {
byte[] buf = new byte[4096];
UniversalDetector detector = new UniversalDetector(null);
try (FileInputStream fis = new FileInputStream(file)) {
int nread;
while ((nread = fis.read(buf)) > 0 && !detector.isDone()) {
detector.handleData(buf, 0, nread);
}
}
detector.dataEnd();
String encoding = detector.getDetectedCharset();
detector.reset();
return encoding != null ? encoding : "UTF-8"; // 默認兜底
}
/** 轉換文件編碼至目標編碼 (默認 UTF-8) */
public static void convertTo(File srcFile, String targetEncoding) throws IOException {
String srcEncoding = detectEncoding(srcFile);
if (srcEncoding.equalsIgnoreCase(targetEncoding)) {
System.out.println("編碼一致,無需轉換: " + srcFile.getName());
return;
}
File tmpFile = File.createTempFile("conv_", ".tmp", srcFile.getParentFile());
try (
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(srcFile), srcEncoding));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(tmpFile), targetEncoding))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine(); // 保持系統換行符
}
writer.flush();
}
// 原子性替換
Files.move(tmpFile.toPath(), srcFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
System.out.println("轉換完成: " + srcFile.getName() + " (" + srcEncoding + " -> " + targetEncoding + ")");
}
/** 讀取文件內容並按目標編碼返回字符串 (不修改原文件) */
public static String readAsString(File file, String targetEncoding) throws IOException {
String srcEncoding = detectEncoding(file);
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(file), srcEncoding))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append(System.lineSeparator());
}
}
// 重新編碼為目標編碼字節再解碼 (雙重保險)
return new String(sb.toString().getBytes(targetEncoding), targetEncoding);
}
public static void main(String[] args) {
try {
File testFile = new File("D:/data/gbk_file.txt");
convertTo(testFile, "UTF-8");
// 或僅讀取
String content = readAsString(testFile, "UTF-8");
System.out.println(content.substring(0, Math.min(200, content.length())));
} catch (IOException e) {
e.printStackTrace();
}
}
}
UniversalDetector 基於 Mozilla 算法,支援 GBK/Big5/Shift_JIS/UTF-8/UTF-16 等主流編碼,準確率極高。BufferedReader/Writer 按行處理,常數內存,支援超大文件 (GB 級)。Files.move(REPLACE_EXISTING) 原子替換,避免寫入中斷損壞源文件。writer.newLine() 自動使用系統默認換行符 (Windows \r\n, Linux \n),或可顯式指定。| 問題 | 原因 | 解決 |
|---|---|---|
| 檢測結果為 null | 文件過短/純 ASCII | 默認 UTF-8 或嘗試指定常見編碼 |
| 轉換後中文亂碼 | 源編碼檢測錯誤 (如 GBK 誤判為 ISO-8859-1) | 強制指定源編碼;或檢查文件是否混合編碼 |
| 大文件 OOM | 一次性 readAllBytes | 必須使用 BufferedReader 流式讀寫 |
| 權限拒絕 | 目標文件被佔用/只讀 | 檢查 file.canWrite();關閉佔用進程 |
public static void convertDirectory(File dir, String targetEncoding) throws IOException {
File[] files = dir.listFiles((d, name) -> name.endsWith(".txt") || name.endsWith(".java") || name.endsWith(".xml"));
if (files == null) return;
for (File f : files) {
if (f.isFile()) convertTo(f, targetEncoding);
else convertDirectory(f, targetEncoding);
}
}
文件編碼轉換看似簡單,實則暗藏「檢測準確性、內存安全性、寫入原子性」三大挑戰。採用 juniversalchardet 檢測 + BufferedReader/Writer 流式轉碼 + Files.move 原子替換的組合拳,可構建生產級可靠轉換工具。原則:不信任扩展名,不信任默認編碼,不原地覆蓋源文件。