專案經驗,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)
背景
專案開發需要手動合入幾十種語言的翻譯到 string.xml 中,這是一件非常痛苦的事情:Copy、Paste,Copy、Paste,Copy、Paste… 人都快瘋了!被逼無奈寫了個自動替換翻譯的工具,原理很簡單:解析 Excel中的翻譯,替換到 Xml 中。Excel 解析用 jxl.jar,Xml 解析與修改用 DOM,一頓操作,一天就寫完了!正高興呢,趕緊使用 git diff 檢視修改對比,一看壞事了:“坑爹呢!這特麼根本不能用好嘛!原檔案的每一行都被識別成了新行(因為換行符變了),這程式碼還怎麼稽核?鬼知道你改了什麼!” 所以,本文記錄如何使用 Java 識別與轉換檔案換行符。
檔案換行符分類
Intellij>
File>
Line Separators:
檢視 ASCII 碼錶:
- \r(CR (carriage return)):十六進位制為 0x0D
- \n(LF (NL line feed, new line)):十六進位制為 0x0A
- Windows 換行符:\r\n,Enter鍵+換行鍵;
- Linux 換行符:\n,換行鍵;
- Mac 換行符:\r,Enter鍵。
- 沒有換行符:檔案的最後一行可以沒有換行符
識別檔案符
按行讀取檔案,然後再分別讀出接下來的兩個位元組,判斷其 int 值:
package com.yuloran.util;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public final class LineSeparatorHelper {
public enum LINE_SEPARATOR {
WINDOWS, LINUX, MAC, UNKNOWN
} private LineSeparatorHelper() {
} public static LINE_SEPARATOR getLineSeparator(File f) throws IllegalArgumentException {
if (f == null || !f.isFile() || !f.exists()) {
throw new IllegalArgumentException("file must exists!");
} RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(f, "r");
String line = raf.readLine();
if (line == null) {
return LINE_SEPARATOR.UNKNOWN;
} // 必須執行這一步,因為 RandomAccessFile 的 readLine() 會自動忽略並跳過換行符,所以需要先回退檔案指標位置 // "ISO-8859-1" 為 RandomAccessFile 使用的字符集,此處必須指定,否則中文 length 獲取不對 raf.seek(line.getBytes("ISO-8859-1").length);
byte nextByte = raf.readByte();
if (nextByte == 0x0A) {
return LINE_SEPARATOR.LINUX;
} if (nextByte != 0x0D) {
return LINE_SEPARATOR.UNKNOWN;
} try {
nextByte = raf.readByte();
if (nextByte == 0x0A) {
return LINE_SEPARATOR.WINDOWS;
} return LINE_SEPARATOR.MAC;
} catch (EOFException e) {
return LINE_SEPARATOR.MAC;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} return LINE_SEPARATOR.UNKNOWN;
}
}複製程式碼
使用 Intellij 建立一個 Java 工程,編寫一個控制檯應用,測試以上程式碼:
package com.yuloran;
import com.yuloran.util.LineSeparatorHelper;
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("test.txt");
System.out.println("line separator: " + LineSeparatorHelper.getLineSeparator(f).name());
}
}複製程式碼
test.txt 的換行符通過 File>
Line Separators 進行切換,換行符符號可用 Notepad 檢視,比如Windows 換行符為:
Notepad 顯示所有符號方法:
測試結果:
轉換檔案換行符
讀出新檔案換行符,若與原檔案換行符不一致,則新建一臨時檔案,逐行寫入原檔案內容,並在行尾寫入原檔案換行符,然後刪除原檔案,重新命名臨時檔案:
// 此處省略 LineSeparatorHelper 類其他程式碼... @SuppressWarnings("ResultOfMethodCallIgnored") public static boolean convert(LINE_SEPARATOR oldLs, File f, String charset) {
if (oldLs == null || oldLs == LINE_SEPARATOR.UNKNOWN) {
return false;
} if (f == null || !f.isFile() || !f.exists()) {
return false;
} if (charset == null || charset.isEmpty()) {
charset = "UTF-8";
} LINE_SEPARATOR newLs = getLineSeparator(f);
if (newLs == oldLs) {
return false;
} File temp = new File(f.getParent(), "temp.txt");
if (temp.exists()) {
temp.delete();
} BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(f), charset));
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(temp), charset));
String line;
int lineNumber = 0;
while ((line = br.readLine()) != null) {
if (lineNumber != 0) {
switch (oldLs) {
case WINDOWS: bw.append('\r').append('\n');
break;
case LINUX: bw.append('\n');
break;
case MAC: bw.append('\r');
break;
default:
}
} bw.write(line);
++lineNumber;
} return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
} if (bw != null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
} f.delete();
temp.renameTo(f);
} return false;
}複製程式碼
測試程式碼:
package com.yuloran;
import com.yuloran.util.LineSeparatorHelper;
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("test.txt");
System.out.println("original line separator: " + LineSeparatorHelper.getLineSeparator(f).name());
LineSeparatorHelper.convert(LineSeparatorHelper.LINE_SEPARATOR.WINDOWS, f, "UTF-8");
System.out.println("new line separator: " + LineSeparatorHelper.getLineSeparator(f).name());
}
}複製程式碼
測試結果:
總結
- RandomAccessFile 以 “ISO-8859-1” 編碼方式讀取一行,獲取位元組數時,須指定該編碼方式
- RandomAccessFile 讀取一行後,檔案指標指向下一行開頭,跳過了換行符所佔的位元組位置,讀取換行符時須回退檔案指標位置
- 沒有位元組可讀時,呼叫 readByte() 會丟擲 EOFException:
public final byte readByte() throws IOException {
int ch = this.read();
if (ch <
0) throw new EOFException();
return (byte)(ch);
}複製程式碼 - 重新命名檔案、刪除檔案須在 IO 流關閉後執行