import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileEncodingConverter {
/**
* 轉換文件編碼
* @param sourcePath 源文件路徑
* @param targetCharset 目標編碼 (如 "UTF-8", "GBK")
* @param overwrite 是否覆蓋原文件
* @return 轉換後內容 (供預覽)
*/
public static String convert(String sourcePath, String targetCharset, boolean overwrite)
throws IOException {
Path source = Paths.get(sourcePath);
if (!Files.exists(source)) {
throw new FileNotFoundException("文件不存在: " + sourcePath);
}
// 1. 自動檢測源編碼
String detectedCharset = detectEncoding(source);
System.out.println("檢測到源編碼: " + detectedCharset);
// 2. 編碼相同,無需轉換
if (detectedCharset.equalsIgnoreCase(targetCharset)) {
System.out.println("編碼一致,無需轉換");
return Files.readString(source, Charset.forName(detectedCharset));
}
// 3. 讀取源文件內容 (使用檢測到的編碼)
String content = Files.readString(source, Charset.forName(detectedCharset));
// 4. 寫入目標文件 (使用目標編碼)
Path target = overwrite ? source : source.resolveSibling(source.getFileName() + ".converted");
Files.writeString(target, content, Charset.forName(targetCharset));
System.out.println("轉換完成: " + detectedCharset + " → " + targetCharset);
System.out.println("輸出文件: " + target.toAbsolutePath());
return content;
}
/**
* 編碼檢測:BOM 優先,無 BOM 時啟發式分析
*/
private static String detectEncoding(Path file) throws IOException {
try (InputStream is = Files.newInputStream(file)) {
// 讀取前 4 字節檢測 BOM
byte[] bom = new byte[4];
int read = is.read(bom);
if (read < 2) return StandardCharsets.ISO_8859_1.name(); // 空文件或極小
// BOM 檢測
if (bom[0] == (byte)0xEF && bom[1] == (byte)0xBB && bom[2] == (byte)0xBF) {
return "UTF-8";
}
if (bom[0] == (byte)0xFF && bom[1] == (byte)0xFE) {
return "UTF-16LE";
}
if (bom[0] == (byte)0xFE && bom[1] == (byte)0xFF) {
return "UTF-16BE";
}
if (bom[0] == (byte)0x00 && bom[1] == (byte)0x00 && bom[2] == (byte)0xFE && bom[3] == (byte)0xFF) {
return "UTF-32BE";
}
if (bom[0] == (byte)0xFF && bom[1] == (byte)0xFE && bom[2] == (byte)0x00 && bom[3] == (byte)0x00) {
return "UTF-32LE";
}
// 無 BOM:啟發式檢測 (讀取前 8KB 樣本)
return heuristicDetect(file);
}
}
/**
* 啟發式編碼檢測 (簡化版,生產建議用 ICU4J/jchardet)
*/
private static String heuristicDetect(Path file) throws IOException {
// 讀取樣本
byte[] sample = Files.readAllBytes(file);
if (sample.length > 8192) {
sample = java.util.Arrays.copyOf(sample, 8192);
}
// 統計特徵
int utf8Score = 0;
int gbkScore = 0;
for (int i = 0; i < sample.length; i++) {
int b = sample[i] & 0xFF;
// UTF-8 多字節序列特徵
if (b >= 0xC0 && b <= 0xDF) { // 2 字節首位
if (i + 1 < sample.length && (sample[i+1] & 0xFF) >= 0x80 && (sample[i+1] & 0xFF) <= 0xBF) {
utf8Score += 2;
i++; // 跳過續位
}
} else if (b >= 0xE0 && b <= 0xEF) { // 3 字節首位
if (i + 2 < sample.length
&& (sample[i+1] & 0xFF) >= 0x80 && (sample[i+1] & 0xFF) <= 0xBF
&& (sample[i+2] & 0xFF) >= 0x80 && (sample[i+2] & 0xFF) <= 0xBF) {
utf8Score += 3;
i += 2;
}
} else if (b >= 0xF0 && b <= 0xF4) { // 4 字節首位 (罕見)
utf8Score += 4;
i += 3;
} else if (b >= 0x80) { // 孤立高位字節,傾向 GBK
gbkScore++;
}
}
// 純 ASCII 文本無法區分,默認 UTF-8
if (utf8Score == 0 && gbkScore == 0) return "UTF-8";
return utf8Score > gbkScore ? "UTF-8" : "GBK";
}
// 測試主方法
public static void main(String[] args) {
try {
// 創建測試文件 (GBK 編碼)
Path testFile = Paths.get("test_gbk.txt");
Files.writeString(testFile, "你好,世界!Hello World!", Charset.forName("GBK"));
// 轉換為 UTF-8
String content = convert("test_gbk.txt", "UTF-8", false);
System.out.println("內容預覽: " + content);
// 驗證結果
String utf8Content = Files.readString(Paths.get("test_gbk.txt.converted"), StandardCharsets.UTF_8);
System.out.println("驗證讀取: " + utf8Content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 錯誤示範
String encoded = URLEncoder.encode("中文", "UTF-8"); // %E4%B8%AD%E6%96%87
String decoded = URLDecoder.decode(encoded, "GBK"); // 亂碼!
URLEncoder 用於 URL 參數編碼 (application/x-www-form-urlencoded),而非文件內容編碼轉換。文件轉換應直接按字符讀取、按字節寫入。
public static void convertLargeFile(String src, String dest, String fromCharset, String toCharset)
throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(src), fromCharset));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(dest), toCharset))) {
char[] buffer = new char[8192];
int len;
while ((len = reader.read(buffer)) != -1) {
writer.write(buffer, 0, len);
}
}
}
| 策略 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 覆蓋原文件 | 節省空間,路徑不變 | 轉換失敗導致數據永久損壞 | 有備份、非關鍵文件 |
| 生成新文件 (.converted) | 安全,可對比驗證 | 佔用雙倍空間 | 生產環境、關鍵數據 |
| 臨時文件 + 原子重命名 | 安全且無殘留 | 需同文件系統支持原子操作 | 高可用系統 |
常用編碼別名:
UTF-8 : "UTF-8", "utf8"
GBK : "GBK", "GB2312", "CP936", "windows-936"
GB18030 : "GB18030" (國家標準,兼容 GBK)
Big5 : "Big5", "CP950", "windows-950" (繁體中文)
Shift_JIS : "Shift_JIS", "SJIS", "CP932", "windows-932" (日文)
EUC-KR : "EUC-KR", "CP949", "windows-949" (韓文)
ISO-8859-1 : "ISO-8859-1", "Latin-1" (西歐語言,單字節)
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>74.1</version>
</dependency>
// 使用 ICU4J 檢測 (準確率極高)
CharsetDetector detector = new CharsetDetector();
detector.setText(Files.readAllBytes(file));
CharsetMatch match = detector.detect();
String encoding = match.getName(); // 如 "UTF-8", "GB18030"
public static void batchConvert(String dir, String targetCharset, String... extensions) throws IOException {
Files.walk(Paths.get(dir))
.filter(Files::isRegularFile)
.filter(p -> {
String name = p.getFileName().toString().toLowerCase();
for (String ext : extensions) if (name.endsWith(ext)) return true;
return false;
})
.forEach(p -> {
try {
convert(p.toString(), targetCharset, false);
} catch (IOException e) {
System.err.println("轉換失敗: " + p + " - " + e.getMessage());
}
});
}
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>tasks.withType(JavaCompile) { options.encoding = 'UTF-8' }若文件已被錯誤編碼讀取並保存 (如 UTF-8 內容按 GBK 讀取後存為 UTF-8),信息已不可逆損失。嘗試「逆向操作」:
// 假設原文件 UTF-8 被誤按 GBK 讀取
// 1. 將當前文件按 UTF-8 讀取為字符串
// 2. 用 GBK 編碼獲取字節數組
// 3. 再按 UTF-8 解碼字節數組
// 只有在「雙重編碼」場景下可能恢復
// 某些解析器 (如早期 XML/JSON) 不識別 UTF-8 BOM
// 解決:寫入時使用 StandardCharsets.UTF_8 (無 BOM)
// 或手動去除: Files.write(target, content.getBytes(StandardCharsets.UTF_8))
文件編碼轉換的核心是 正確檢測源編碼 → 按字符讀取 → 按目標編碼寫入。掌握 BOM 檢測、啟發式分析、流式處理、安全寫入策略,配合 ICU4J 等專業庫,可構建健壯的編碼轉換工具。記住:預防勝於治療,項目初期統一規範 UTF-8 並強制配置編譯/運行環境,可避免 99% 的編碼問題。