阅读 67

Java 文件换行符识别与转换

项目经验,如需转载,请注明作者: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,回车键+换行键;
  2. Linux 换行符:\n,换行键;
  3. Mac 换行符:\r,回车键。
  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 流关闭后执行
关注下面的标签,发现更多相似文章
评论