文本文件中的行分隔符

这可能是关于换行符最全面的一篇文章。即使现在不是,后面也会将新的内容补充进来,让它成为最全面的一篇。

1、介绍

当我们用一个编辑器打开一个文本文件,在其中输入一个字符'a',这时候,就会有一个对应的字符'a'的编码(如果编码格式是ACII码,那么这里记入的编码就是“97”,写成16进制就是“0x61”)记入到该文件中。类似的输入一个'b',文件中便会记入一个对应的字符'b'的编码。然而,如果我们按下键盘上的‘Enter’键,现象上看,文本内容发生了换行。但是,这时候,对应的文件中究竟记入了什么内容,来标记文件发生了换行呢?
实际上,对于这个问题,不同的操作系统,沿用了不同的操作传统。如下:

操作系统 ASCII码 转义字符 名称 英文名称 历史描述
Unix 0A \n 换行 LineFeed 使光标下移一格
Mac 0D \r 回车 CarriageReturn 使光标到行首
Dos和Windows 0A0D \r\n 回车换行 CRLF

注:
Mac OS 9 以及之前的系统的换行符是CR,从Mac OS X (后来改名为“OS X”)开始的换行符是LF即‘\n',和Unix统一了。

2、可能会引发的问题和解决

不同平台的换行符不同,会导致的各种异响不到的问题。比如:Unix/Mac系统下的文件在Windows里打开的话,所有文字会变成一行;而Windows里的文件在Unix/Mac下打开的话,在每行的结尾可能会多出一个^M符号。
如果只是将文件在编辑器中打开,供人肉眼阅读,这个问题还是挺好处理的。换一个更加智能的编辑器就好了。有的编辑器能够自动识别行分隔符,有的甚至允许用户自己指定行分隔符。这里面我遇到的对这个问题处理最好的编辑器,是JetBrains公司出的Java集成开发环境IntelliJ IDEA。


IDEA中行分隔符识别和切换

在打开文本文件的左下方,标签标识当前文件的行分隔符,鼠标点击,会弹出一个上拉列表,允许用户修改不同的行分隔符,非常方便。(类似地,文件编码的修改也在这个位置,不能更好用了。)
比人肉眼阅读麻烦的是,写程序处理文本文件的时候。一个按行处理文本文件的程序可能能够正确处理Windows上生成的文本文件,但是换成一个平台上产生的文件,可能就无法正确运行。这时候,可能就需要先识别是不是文件的分隔符导致的问题,然后,决定是不是要做必要的转换。

3、行分隔符的命令行识别

上面已经提到过了,更加智能的编辑器肯定是能够识别行分隔符的。但是,很多时候,我们有的只是一个终端、命令行。所以,这部分主要介绍如何通过命令来识别行分隔符。

3.1 xxd命令

如果能看到文件存储的二进制字节,自然可以知道文件的行分隔符是什么,图形化的智能编辑器大部分都自带这个功能。命令行下也有好多工具可以查看文本文件的16进制输出,这里以xxd命令为例介绍(如下测试,连同本文的其他测试都是在macOS Mojave 版本号10.14.1环境下执行的)。

$ cat a.txt 
a
b
c
$ xxd -g1 a.txt 
00000000: 61 0a 62 0a 63 0a                                a.b.c.
$ 

上面的命令中-g1的参数是指一个字节为一组查看16进制编码。从命令的结果可以看出,该文件的行分隔符是0a,也就是\n。xxd命令输出的右边a.b.c.,是带表文件文本内容,其中的点就是带表不可打印字符\n。而在下面的执行结果中,不难看出文件b.txt的行分隔符是\r\n

$ cat b.txt 
a
b
c
$ xxd -g1 b.txt 
00000000: 61 0d 0a 62 0d 0a 63 0d 0a                       a..b..c..
$ 

3.2 cat命令

有的操作系统发行版中,自带的命令行中没有上面的xxd工具,通过cat命令其实也可以查看文本文件的行分隔符。如下是cat命令各个选项的解释:

-A, --show-all           等价于 -vET
-b, --number-nonblank    对非空输出行编号
-e                       等价于 -vE
-E, --show-ends          在每行结束处显示 $
-n, --number     对输出的所有行编号,由1开始对所有输出的行数编号
-s, --squeeze-blank  有连续两行以上的空白行,就代换为一行的空白行 
-t                       与 -vT 等价
-T, --show-tabs          将跳格字符显示为 ^I
-u                       (被忽略)
-v, --show-nonprinting   使用 ^ 和 M- 引用,除了 LF 和 TAB 之外

可以看出-A选项的作用就是在文件每行结尾显示$,同时显示除了LF(\n换行符)和TAB之外的所有不可打印字符。如下是从维基百科扒下来的不可打印字符列表:

  • 0 (null, NUL, \0, ^@), originally intended to be an ignored character, but now used by many programming languages including C to mark the end of a string.
  • 7 (bell, BEL, \a, ^G), which may cause the device to emit a warning such as a bell or beep sound or the screen flashing.
  • 8 (backspace, BS, \b, ^H), may overprint the previous character.
  • 9 (horizontal tab, HT, \t, ^I), moves the printing position right to the next tab stop.
  • 10 (line feed, LF, \n, ^J), moves the print head down one line, or to the left edge and down. Used as the end of line marker in most UNIX systems and variants.
  • 11 (vertical tab, VT, \v, ^K), vertical tabulation.
  • 12 (form feed, FF, \f, ^L), to cause a printer to eject paper to the top of the next page, or a video terminal to clear the screen.
  • 13 (carriage return, CR, \r, ^M), moves the printing position to the start of the line, allowing overprinting. Used as the end of line marker in Classic Mac OS, OS-9, FLEX (and variants). A CR+LF pair is used by CP/M-80 and its derivatives including DOS and Windows, and by Application Layer protocols such as FTP, SMTP, and HTTP.
  • 26 (Control-Z, SUB, EOF, ^Z). Acts as an end-of-file for the Windows text-mode file I/o.
  • 27 (escape, ESC, \e (GCC only), ^[). Introduces an escape sequence.
    下面是cat命令查看行分隔符的一个例子:
$ cat -A a.txt | head -1
cat: illegal option -- A
usage: cat [-benstuv] [file ...]
$ 

可以看出mac系统自带的命令行cat工具不支持-A选项。不过,在支持的系统上,配合head命令,可以看出如果文件的换行符是\n输出行的末尾只会有一个$,如果换行符是\r\n,输出行的末尾就会是^M$。从上面cat命令的解释也不难看出这一点。

4、行分隔符的装换

如果确定了是行分隔符的导致的问题,有时候,就需要进行行分隔符的转换。最简单的方式,可能是上面提到的像IDEA那样的更加智能的图形化文本编辑器,在界面上点点点操作几下就完成了。然而,这不见得是最方便的,比如在命令行的环境中,除了命令一无所有。因此,这里着重介绍命令行下的解决方案。

4.1 sed命令

提到命令行下的文件编辑sed命令肯定是绕不过去的。如果要将行分隔符从\n换成\r\n最直觉的写法可能是(-i选项的意思是直接在原文件上进行编辑):

sed -i 's/\n/\r\n/g' a.txt

然而这个方法,却屡试屡败。原因就在于sed命令是按照行来读文件的,逐行处理,默认地sed认为行分隔符是\n,所以,不会出现在sed处理的文本行内容中,导致这个方案失败。所以,可能的解决办法就是将所有文件内容读进来处理,而不是逐行处理。解决的办法大概有如下几个:

4.1.1 绕过换行符进行替换

既然sed处理的文本行中不包含换行符,我们可以用$来辅助实现替换:

sed -i 's/$/\r/g' a.txt -- \n转\r\n
sed -i 's/\r//g' a.txt -- \r\n转\n

但是,在我的系统上,这样写的效果却是:

$ sed -i '' 's/$/\r/g' a.txt 
$ xxd -g1 a.txt
00000000: 61 72 0a 62 72 0a 63 72 0a                       ar.br.cr.
$ 

这里之所以-i选项后面加''是因为这个系统上sed要求-i时,必须指定扩展。然而,仍然运行失败的原因在于macos没法像Linux那样将\r识别为特殊字符。为了给sed传入\r需要写成:

$ sed -i '' $'s/$/\r/g' a.txt 
$ xxd -g1 a.txt
00000000: 61 0d 0a 62 0d 0a 63 0d 0a                       a..b..c..
$ 
$ sed -i '' $'s/\r//g' a.txt 
$ xxd -g1 a.txt
00000000: 61 0a 62 0a 63 0a                                a.b.c.
$ 
$ sed -i '' "s/$/$(printf '\r')/" a.txt
$ xxd -g1 a.txt
00000000: 61 0d 0a 62 0d 0a 63 0d 0a                       a..b..c..
$

这里$''的作用就是让其中的转义字符正确被翻译。同样的,用$()也可以达到这个效果,不过外面的单引号要换成双引号。

4.1.2 sed的z选项

对于GNU版本的sed,可以使用-z选项。

-z
--null-data
--zero-terminated

Treat the input as a set of lines, each terminated by a zero byte (the ASCII ‘NUL’ character) instead of a newline. This option can be used with commands like ‘sort -z’ and ‘find -print0’ to process arbitrary file names.

下面是一个例子:

sed -i -z 's/\n/\r\n/g' a.txt

4.1.3循环读入所有文件内容

对于GNU版本的sed,也可以写一个循环,将文件全部读入之后,再交给sed处理:

sed ':a;N;$!ba;s/\n/\r\n/g' a.txt
This will read the whole file in a loop, then replaces the newline(s) with a space.

Explanation:

Create a label via :a.
Append the current and next line to the pattern space via N.
If we are before the last line, branch to the created label $!ba ($! means not to do it on the last line as there should be one final newline).
Finally the substitution replaces every newline with a space on the pattern space (which is the whole file).

5、正则表达式中换行符和文件开头结尾标识的先后顺序

到这里,换行符的识别、转换等都介绍完了。这里讲最后一个之前令我困扰的问题,^$\r\n这几个符号在正则匹配中的先后顺序是什么。这里,直接贴下正则表达式网站上的介绍:

For anchors there's an additional consideration when CR and LF occur as a pair and the regex flavor treats both these characters as line breaks. Delphi, Java, and the JGsoft flavor treat CRLF as an indivisible pair. ^ matches after CRLF and $ matches before CRLF, but neither match in the middle of a CRLF pair. JavaScript and XPath treat CRLF pairs as two line breaks. ^ matches in the middle of and after CRLF, while $ matches before and in the middle of CRLF.

也就是说,Delphi、Java和JGsoft风格的正则将CRLF看成一个整体,^匹配CRLF后面,$匹配CRLF前面,两者都不匹配CRLF中间。而JavaScript和XPath认为CRLF是两个换行符,^匹配CRLF中间和后面,$匹配CRLF中间和前面。


写了整整两个工作日的晚上,希望对自己和大家都有帮助。写得比较辛苦,也希望转载能注明出处链接,当然,也只是希望。

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容