换行符

表示换行的控制字符。有 LF (Unix)、CR (旧 Mac) 和 CRLF (Windows) 三种类型。

换行符是文本数据中标记一行结束和下一行开始的控制字符。主要有三种类型:LF (换行,U+000A)、CR (回车,U+000D) 和 CRLF (CR 后跟 LF 的两字符组合)。这些名称源自打字机的机械动作:CR 将打印头移回行首,LF 将纸张向前送一行。这一概念从计算机诞生之初就被沿用至今。

不同操作系统使用不同的标准换行符。Unix/Linux/macOS 使用 LF,Windows 使用 CRLF,旧版 Mac OS (Classic Mac OS、Mac OS 9 及更早版本) 使用 CR。这种差异直接影响文件兼容性,在不同操作系统之间交换文本文件时可能会出现问题。在 Git 中,可以通过 core.autocrlf 设置控制换行符的自动转换,团队开发中通常使用 .gitattributes 文件统一整个仓库的换行符策略。

在编程中,换行符的差异经常成为 bug 的根源。如果文件读取代码只预期 LF,处理 CRLF 文件时每行末尾会残留 CR。使用正则表达式用 $ 匹配行尾时,也可能因换行符差异而无法按预期工作。要实现健壮的文本处理,需要统一处理 \r\n、\n 和 \r。

即使是同一个正则表达式,不同语言对行尾的解释也不一样。对含有 CRLF 的字符串以多行模式匹配 a$ 时,JavaScript 的 /a$/m 会返回真,而 Python 的 re.search("a$", s, re.M) 匹配不上。因为 Python 的 $ 只把 LF 的紧前面视为行尾,残留在 a 后面的 CR 会碍事。读取一侧的自动转换也容易被忽略。Python 的 open() 默认会启用换行的自动转换 (universal newlines),所以即使读的是 CRLF 的文件,作为字符串传过来时也已经被替换成了 LF。想按原样处理原本的换行时,就指定 newline=''。反过来,str.splitlines() 不只在 CR、LF、CRLF 处切行,连垂直制表符 (U+000B) 和 U+2028 这样的字符也会切,因此若只想按换行符来切,明确写出用于分割的字符更为安全。

换行符在各种协议和规范中也有明确规定。像 HTTP/1.1 这样的文本形式协议用 CRLF 分隔头部,CSV 文件的规范 (RFC 4180) 也定义为各条记录以 CRLF 分隔。SMTP (邮件发送协议) 规定行以 CR 后跟 LF 来终结,并且不得把除此之外的字符或排列识别、生成为行终结 (RFC 5321)。另一方面,JSON 中禁止把 U+0000 到 U+001F 的控制字符原样放进字符串里 (RFC 8259),因此换行要写成 \n 这样的 2 字符转义,或 \u000A 这样的 6 字符写法。把取得的数据的换行原样嵌进去的 JSON,看上去只是换了行,实际却会引发解析错误。

规范的细节会直接变成实务中的陷阱。RFC 4180 规定,文件的最后一条记录末尾有换行也可以、没有也可以。而且它还规定含有换行、双引号或逗号的字段要用双引号括起来,所以 CSV 并不能简单地按换行来分割。位于引号内侧的 CRLF 不是记录的分隔,而是字段的内容,自己先按行切开再按逗号分割的实现就会在这里坏掉。处理 CSV 时,不要从行分割起自己造,交给会连引号的解释一起做的库才安全。

一个常见的误解是,由于换行符不可见,人们往往认为 "用哪种都一样",但实际上它们的字节数不同。LF 为 1 字节,CRLF 为 2 字节,因此对于大量文本数据,换行符的选择会影响文件大小。许多编辑器和工具也具备检测和修复混合换行符 (同一文件中同时存在 LF 和 CRLF) 的功能。末尾有没有换行同样会改变结果。在把换行视作 "行的终结" 来数的工具里,最后一行没有换行的文件,那一行就不会计入数量。把只有 a、换行、b 这 3 个字符的文件交给 wc -l,看上去是 2 行,返回的却是 1。

从字符计数的角度来看,是否将换行符计入字符数会影响结果。大多数字符计数工具将换行计为 1 个字符,但 CRLF 是计为 1 个字符还是 2 个字符因工具而异。要获得准确的字符数,了解所使用工具对换行符的处理方式非常重要。例如在 JavaScript 中 "a\r\nb".length 会返回 4,CRLF 被数成 2 个字符;同样的内容换成 LF 则是 3。在输入字符数有上限的表单里,粘贴用 Windows 写的含换行的文章时,即使所数的文字相同,也可能比预想更早触到上限。

分享这篇文章