Java 檔案換行符識別與轉換

Yuloran發表於2019-01-04

專案經驗,如需轉載,請註明作者: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:

換行符分類.png

檢視 ASCII 碼錶:

  • \r(CR (carriage return)):十六進位制為 0x0D
  • \n(LF (NL line feed, new line)):十六進位制為 0x0A
  1. Windows 換行符:\r\n,Enter鍵+換行鍵;
  2. Linux 換行符:\n,換行鍵;
  3. Mac 換行符:\r,Enter鍵。
  4. 沒有換行符:檔案的最後一行可以沒有換行符

識別檔案符

按行讀取檔案,然後再分別讀出接下來的兩個位元組,判斷其 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 工程,編寫一個控制檯應用,測試以上程式碼:

測試工程.png
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 換行符為:

windows換行符.png

Notepad 顯示所有符號方法:

Notepad 顯示所有符號.png

測試結果:

測試結果.png

轉換檔案換行符

讀出新檔案換行符,若與原檔案換行符不一致,則新建一臨時檔案,逐行寫入原檔案內容,並在行尾寫入原檔案換行符,然後刪除原檔案,重新命名臨時檔案:

    // 此處省略 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());

}
}複製程式碼

測試結果:

轉換前.png
執行轉換.png
轉換後.png

總結

  • 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 流關閉後執行

來源:https://juejin.im/post/5c2edc556fb9a049d97556ad

相關文章