换行符这东西,平时写代码几乎天天见,但真要让人说清楚\r、\n、\r\n、\n\r这四者的区别,能一口气讲明白的人其实不多。我见过太多项目里的诡异 bug,追到最后就是一行换行符没处理对:日志文件在 Linux 上打开正常,传到 Windows 上变成一整坨;从网页表单里复制出来的文本,正则死活匹配不上;串口收到的数据每行末尾多一个看不见的字符,解析直接错位。这些问题排查起来能耗掉一整个下午,但根因往往简单得让人想拍桌子。
这篇内容就是把这四种换行符彻底讲透。我会从它们的历史由来、字节层面的真实差异、各操作系统和编程语言里的默认行为,一直讲到实际开发中怎么检测、怎么转换、怎么避坑。不管你是刚学编程的新手,还是写了几年代码但一直没系统梳理过这块的老手,看完都能对"换行"这件事有一个清晰的认知。关键词就四个:\r、\n、\r\n、\n\r,但背后的门道远比这四个符号本身多。
1. 四种换行符的字节真相与历史渊源
1.1 从打字机说起:CR 和 LF 到底是什么意思
要理解换行符,得先回到机械打字机的年代。那时候的机器有两个独立动作:一个是把打印头推回最左边,叫Carriage Return(回车,CR);另一个是把纸张往上卷一行,叫Line Feed(换行,LF)。这两个动作是分开的,因为打字机确实需要分别执行——你先得把 carriage 推回去,再 feed 一行纸,才能开始打下一行。
计算机早期直接继承了这个概念。\r就是 CR,ASCII 码是13(十六进制0x0D);\n就是 LF,ASCII 码是10(十六进制0x0A)。注意这两个是不同的控制字符,不是同一个东西的两种写法。很多人误以为\r\n就是"换行符"的固定写法,其实它是两个字符的组合。
那\r\n和\n\r呢?前者是"先回车再换行",后者是"先换行再回车"。在打字机上,顺序其实有讲究:正常操作是先回车(回到行首)再换行(下移一行),所以\r\n是符合物理逻辑的顺序。\n\r则是反过来的,先下移再回到行首——在某些终端上效果一样,但在另一些设备上会导致光标位置错乱。
1.2 各操作系统的选择:为什么 Windows 和 Unix 不一样
这里有个广为流传的说法:Windows 用\r\n,Unix/Linux/macOS 用\n,老 Mac(OS 9 及之前)用\r。这个说法基本正确,但背后的原因值得说清楚。
早期电传打字机(Teletype)时代,\r\n是标准做法,因为物理设备需要两个动作。后来 Unix 的设计者觉得,既然终端驱动可以自动处理回车,那文件里存一个\n就够了,省一个字节。这个决定影响深远,成了 Unix 系的标准。Windows 继承了 DOS 的传统,而 DOS 又继承了 CP/M,CP/M 则沿用了电传打字机的\r\n。苹果在 OS X 之后转向 Unix 体系,也改用了\n。
所以本质上,这不是技术优劣问题,而是历史路径依赖。\r\n多一个字节,但在现代存储和网络带宽下,这点开销可以忽略。真正麻烦的是跨平台文本处理时的不一致。
| 换行符 | 字节序列(十六进制) | 典型使用场景 |
|---|---|---|
\n | 0A | Unix、Linux、macOS(OS X 后)、现代网络协议 |
\r | 0D | 经典 Mac OS(OS 9 及之前)、部分老式设备 |
\r\n | 0D 0A | Windows、DOS、HTTP 协议、SMTP 邮件 |
\n\r | 0A 0D | 极少见,某些特殊终端或错误实现 |
1.3\n\r为什么几乎见不到,但它确实存在
\n\r在实际系统里非常罕见,但并非不存在。它通常出现在两种情况下:一是某些老式终端或打印机要求先换行再回车;二是程序 bug 导致的错误顺序,比如在转换换行符时把顺序搞反了。
我遇到过一次真实案例:某嵌入式设备通过串口发送数据,协议文档写的是"每行以 CRLF 结束",但实际抓包发现是0A 0D。原因是设备固件里先写了\n再写\r,而文档作者想当然地以为 CRLF 就是\r\n。这种不一致在对接时非常坑,因为用常规的\r\n分割逻辑完全匹配不上。
提示:处理未知来源的文本时,不要假设换行符一定是
\r\n或\n。最稳妥的做法是先检测实际字节序列,再决定分割策略。
2. 编程语言里的换行符处理差异
2.1 C 语言:\n在文本模式下会被自动转换
C 语言里有个容易让人困惑的点:在 Windows 上以文本模式("r")打开文件时,读取到的\r\n会被自动转换成\n;写入时\n会被转换成\r\n。这是 C 标准库的行为,目的是让代码跨平台时不用关心底层差异。
但这个"贴心"设计经常导致问题。比如你用二进制模式("rb")读同一个文件,就会看到原始的\r\n。如果代码里混用了文本模式和二进制模式,或者对文件大小的计算依赖了实际字节数,就会出现偏差。我见过一个统计行数的程序,在 Windows 上用文本模式读文件算出来的字节数和用二进制模式不一致,排查了半天才发现是换行符转换搞的鬼。
#include <stdio.h> int main() { // 文本模式:Windows 上 \n 会被写成 \r\n FILE *fp = fopen("test.txt", "w"); fprintf(fp, "line1\nline2\n"); fclose(fp); // 实际文件内容(Windows):line1\r\nline2\r\n // 二进制模式:写入什么就是什么 fp = fopen("test2.txt", "wb"); fprintf(fp, "line1\nline2\n"); fclose(fp); // 实际文件内容:line1\nline2\n return 0; }2.2 Python:newline参数是控制换行行为的关键
Python 在换行处理上给了很细粒度的控制。open()函数的newline参数决定了读取和写入时如何处理换行符:
newline=None(默认):读取时把所有换行符统一转换成\n;写入时把\n转换成系统默认换行符。newline='':读取时不转换,保留原始换行符;写入时不转换。newline='\n':读取时只认\n为换行;写入时用\n。newline='\r\n':读取时只认\r\n;写入时用\r\n。
这个参数在处理跨平台文本时极其重要。比如你要读取一个 Windows 生成的 CSV 文件,用默认的newline=None通常没问题,因为\r\n会被转成\n。但如果你要精确统计原始行尾,就必须用newline=''。
# 默认行为:统一转成 \n with open('win_file.txt', 'r') as f: content = f.read() # \r\n 变成 \n # 保留原始换行符 with open('win_file.txt', 'r', newline='') as f: content = f.read() # \r\n 保持原样 # 写入时强制用 \r\n with open('out.txt', 'w', newline='\r\n') as f: f.write('line1\nline2\n') # 实际写入 line1\r\nline2\r\n2.3 Java 和 JavaScript:line.separator与\n的取舍
Java 里获取系统换行符用System.lineSeparator(),Windows 返回\r\n,Linux 返回\n。但很多 Java 项目里直接写\n,因为大部分场景下\n在 Windows 上也能正常显示(记事本从 Windows 10 开始支持\n了)。不过如果生成的文本要给老版本记事本或某些特定软件用,还是得用系统换行符。
JavaScript 里情况更简单:\n是标准,浏览器和 Node.js 都认。但处理从 Windows 上传的文本时,\r\n会原样保留,需要手动用replace(/\r\n/g, '\n')或split(/\r?\n/)来处理。正则\r?\n是个很实用的写法,能同时匹配\n和\r\n。
// 同时兼容 \n 和 \r\n 的分割 const lines = text.split(/\r?\n/); // 统一转换成 \n const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');2.4 各语言换行处理对比
| 语言 | 默认换行符 | 读取时是否自动转换 | 关键控制方式 |
|---|---|---|---|
| C | 系统相关 | 文本模式自动转换 | fopen模式"r"vs"rb" |
| Python | 系统相关 | 默认转换 | open(newline=...) |
| Java | 系统相关 | 部分转换 | System.lineSeparator() |
| JavaScript | \n | 不转换 | 手动正则处理 |
| Go | \n | 不转换 | 手动处理 |
| Rust | \n | 不转换 | lines()方法 |
3. 实际开发中最容易踩的换行符坑
3.1 文本文件跨平台传输后"变成一行"
这是最经典的换行符问题。在 Linux 上生成的文本文件传到 Windows 用记事本打开,如果文件里只有\n,老版本记事本会把它当成一行显示(因为记事本只认\r\n)。反过来,Windows 文件传到 Linux,\r\n里的\r会显示成^M或者导致脚本执行报错。
我处理过一个 CI 流水线的问题:构建脚本在 Linux 上跑得好好的,换到 Windows 的构建机上就报bad interpreter错误。原因是脚本文件是在 Windows 上编辑保存的,行尾是\r\n,Linux 的 shell 把\r当成了命令的一部分。解决办法是用dos2unix转换,或者在编辑器里设置行尾为 LF。
# 查看文件是否包含 \r cat -A file.txt | head # 如果行尾显示 ^M$,说明是 \r\n # 转换 \r\n 为 \n dos2unix file.txt # 或者 sed -i 's/\r$//' file.txt # 转换 \n 为 \r\n unix2dos file.txt3.2 正则表达式匹配行尾时漏掉\r
用$匹配行尾时,很多正则引擎默认$只匹配\n之前的位置,不匹配\r。所以对\r\n结尾的文本用.+$匹配,结果里会带上\r。这个\r看不见,但会导致字符串比较失败、数据库插入异常等问题。
Python 的re模块里,$默认匹配字符串末尾或\n之前。如果文本是\r\n,$会匹配在\n之前,但\r还在。解决办法是用\r?$或者先规范化换行符。
import re text = "hello\r\nworld\r\n" # 错误:匹配结果带 \r lines = re.findall(r'.+$', text, re.MULTILINE) # ['hello\r', 'world\r'] # 正确:用 \r?$ 或先规范化 lines = re.findall(r'.+\r?$', text, re.MULTILINE) # 或者 text_normalized = text.replace('\r\n', '\n') lines = re.findall(r'.+$', text_normalized, re.MULTILINE)3.3 从网页表单或 Excel 复制文本时的隐藏换行
从网页<textarea>或 Excel 单元格复制多行文本时,换行符可能是\r\n、\n甚至\r,取决于浏览器和操作系统。如果后端没做规范化,存进数据库的文本就会混着不同换行符,后续查询和展示都可能出问题。
我的习惯是在数据入口处统一规范化:把所有\r\n和\r都转成\n,再存库。这样后续处理只需要考虑一种情况。展示时如果需要\r\n,再按目标平台转换。
def normalize_newlines(text): """统一换行符为 \n""" return text.replace('\r\n', '\n').replace('\r', '\n') # 使用 raw = "line1\r\nline2\rline3\n" clean = normalize_newlines(raw) # 'line1\nline2\nline3\n'3.4 常见换行符问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 文件显示为一行 | 用了\n但查看器只认\r\n | cat -A或十六进制查看 | 转成\r\n |
脚本报bad interpreter | 行尾是\r\n | file命令或cat -A | dos2unix转换 |
| 正则匹配结果带隐藏字符 | $没处理\r | 打印repr()查看 | 用\r?$或先规范化 |
| 数据库文本比较失败 | 混用不同换行符 | 查询HEX()字段 | 入口处统一规范化 |
| 串口数据解析错位 | 换行符顺序是\n\r | 抓包看原始字节 | 按实际字节调整解析逻辑 |
4. 换行符检测、转换与规范化实战
4.1 如何准确检测文本用的是哪种换行符
检测换行符不能靠肉眼,得看字节。最直接的方法是用十六进制查看器,或者用编程语言读取原始字节。Python 里可以用open(..., 'rb')读二进制,然后统计\r\n、\n、\r的出现次数。
def detect_newline(filepath): """检测文件主要使用的换行符类型""" with open(filepath, 'rb') as f: data = f.read() crlf = data.count(b'\r\n') lf = data.count(b'\n') - crlf # 减去 \r\n 中的 \n cr = data.count(b'\r') - crlf # 减去 \r\n 中的 \r counts = {'\\r\\n': crlf, '\\n': lf, '\\r': cr} total = sum(counts.values()) if total == 0: return '无换行符' # 返回占比最高的 dominant = max(counts, key=counts.get) return f"主要换行符: {dominant} (CRLF={crlf}, LF={lf}, CR={cr})" # 使用 print(detect_newline('test.txt'))这个函数能告诉你文件里各种换行符的数量分布。如果发现混用,就需要规范化。实际项目中我还会加一个判断:如果\r\n和单独的\n都很多,说明文件是混合来源,必须统一处理。
4.2 批量转换换行符的几种可靠方案
转换换行符的工具很多,选哪个取决于场景。命令行下dos2unix和unix2dos是最成熟的,支持批量处理、自动检测、保留文件权限。Python 脚本适合集成到构建流程里。编辑器(VS Code、Sublime)适合手动改单个文件。
# 批量转换目录下所有 .txt 文件为 Unix 换行 find . -name "*.txt" -exec dos2unix {} \; # 批量转换回 Windows 换行 find . -name "*.txt" -exec unix2dos {} \; # 只查看不修改(-i 显示信息) dos2unix -i *.txtPython 方案更适合需要精细控制的场景,比如只转换特定类型的换行,或者转换后做其他处理:
import os def convert_newlines(src_dir, target='\n'): """批量转换目录下文本文件的换行符""" for root, dirs, files in os.walk(src_dir): for name in files: if not name.endswith(('.txt', '.csv', '.log', '.md')): continue path = os.path.join(root, name) with open(path, 'r', newline='', encoding='utf-8') as f: content = f.read() # 先统一成 \n,再转目标 content = content.replace('\r\n', '\n').replace('\r', '\n') if target != '\n': content = content.replace('\n', target) with open(path, 'w', newline='', encoding='utf-8') as f: f.write(content) print(f"已转换: {path}") # 转成 Windows 换行 convert_newlines('./data', target='\r\n')注意:转换前一定要备份,尤其是批量操作。我吃过一次亏,脚本写错了把二进制文件也当文本处理,结果文件损坏。后来养成了习惯:先 dry-run 打印要处理的文件列表,确认无误再实际执行。
4.3 在数据管道中做换行符规范化
如果你的系统要处理来自多个来源的文本数据,最稳妥的策略是在入口处做一次规范化,之后全程只用\n。这样下游所有处理逻辑都只需要考虑一种换行符,大大降低复杂度。
规范化的位置很关键。我的经验是放在数据接入层,也就是数据刚进入系统、还没做任何业务处理的时候。比如 Web API 收到请求后,先对文本字段做规范化再入库;文件导入时,先规范化再解析。
def normalize_text_field(text): """规范化文本字段的换行符""" if not isinstance(text, str): return text # 统一成 \n text = text.replace('\r\n', '\n').replace('\r', '\n') # 可选:去掉行尾空白 text = '\n'.join(line.rstrip() for line in text.split('\n')) return text # 在 API 入口使用 def create_article(data): data['content'] = normalize_text_field(data.get('content', '')) # ... 后续入库逻辑这个函数看起来简单,但能避免后面无数麻烦。我负责过的一个内容管理系统,早期没做规范化,数据库里同一篇文章的换行符五花八门,做全文检索和 diff 对比时全是噪音。后来加了这个入口规范化,新数据干净了,老数据也写脚本批量清洗了一遍。
4.4 换行符与编码的交互:UTF-8 BOM 的干扰
换行符问题有时会和编码问题混在一起。比如 UTF-8 BOM(字节EF BB BF)出现在文件开头时,某些解析器会把它当成内容的一部分,导致第一行解析异常。BOM 和换行符本身没关系,但排查问题时容易混淆。
如果发现文件开头有莫名其妙的字符,先用十六进制看看是不是 BOM。Python 里用encoding='utf-8-sig'可以自动处理 BOM。
# 自动跳过 UTF-8 BOM with open('file.txt', 'r', encoding='utf-8-sig') as f: content = f.read() # 检测是否有 BOM with open('file.txt', 'rb') as f: raw = f.read(3) has_bom = raw == b'\xef\xbb\xbf' print(f"有 BOM: {has_bom}")5. 换行符在协议与格式中的具体表现
5.1 HTTP 协议为什么强制用\r\n
HTTP/1.1 的报文格式明确规定,请求行、状态行、头部字段之间必须用\r\n分隔,头部和正文之间用两个\r\n(即\r\n\r\n)。这是 RFC 7230 里的硬性要求,不是建议。如果你手写 HTTP 请求,用了\n而不是\r\n,很多服务器会直接返回 400 错误。
为什么 HTTP 选\r\n?因为早期互联网协议大量借鉴了电传打字机的传统,\r\n是当时的通用做法。虽然现在看起来有点冗余,但协议已经固定,改不了了。
# 手写 HTTP 请求时必须用 \r\n request = ( "GET / HTTP/1.1\r\n" "Host: example.com\r\n" "Connection: close\r\n" "\r\n" )用 Python 的http.client或requests库时不用操心这个,库内部会处理好。但如果你用 socket 直接发 HTTP 请求,就必须自己写对。
5.2 CSV 文件里的换行符陷阱
CSV 格式对换行符的处理有个特殊之处:字段内部如果包含换行符,整个字段需要用双引号包裹。这时候字段内的换行符和行分隔符就混在一起了,解析器必须能区分。
比如这样一个 CSV:
name,description "product A","line1 line2" "product B","single line"product A的 description 字段里有一个换行符,但它不是行分隔符。用简单的split('\n')解析就会出错。Python 的csv模块能正确处理这种情况,但前提是打开文件时用newline=''。
import csv # 正确:newline='' 让 csv 模块自己处理换行 with open('data.csv', 'r', newline='', encoding='utf-8') as f: reader = csv.reader(f) for row in reader: print(row) # 错误:默认 newline 可能导致字段内换行被误处理 with open('data.csv', 'r', encoding='utf-8') as f: reader = csv.reader(f) # 可能出问题这个newline=''是 Python csv 模块文档里明确推荐的,但很多人不知道,踩坑后才去查。
5.3 日志文件的行尾处理经验
日志文件通常按行写入,每行末尾加换行符。Linux 服务一般用\n,Windows 服务用\r\n。如果日志要跨平台分析,最好统一成\n。
我在做日志采集时遇到过一个坑:采集 agent 在 Windows 上运行,读到的日志行尾是\r\n,但发送到 Kafka 时没去掉\r,下游的解析程序用\n分割后,每行末尾都带一个\r,导致正则匹配失败。后来在 agent 里加了一步rstrip('\r\n')才解决。
# 读取日志时去掉行尾换行符 with open('app.log', 'r', encoding='utf-8') as f: for line in f: line = line.rstrip('\r\n') # 同时去掉 \r 和 \n # 处理 line用rstrip('\r\n')比rstrip('\n')更安全,因为它能同时处理\r\n和\n两种情况。注意rstrip会去掉所有末尾的\r和\n字符,如果行尾有多个换行符也会一起去掉,这通常是想要的行为。
5.4 不同场景下的换行符选择建议
| 场景 | 推荐换行符 | 理由 |
|---|---|---|
| 跨平台源代码 | \n | Git 可配置自动转换,大多数工具链支持 |
| Windows 专用配置文件 | \r\n | 老版本记事本等工具兼容性 |
| 网络协议(HTTP/SMTP) | \r\n | 协议规范强制要求 |
| 日志文件 | \n | 便于跨平台分析 |
| 数据库存储 | \n | 统一规范,避免比较问题 |
| 用户输入文本 | 入口处规范化为\n | 降低下游处理复杂度 |
6. 换行符相关的工具与调试技巧
6.1 用十六进制查看器看清真实字节
排查换行符问题,第一步永远是看原始字节。命令行下xxd、hexdump、od都能用。xxd的输出最直观,左边是偏移量,中间是十六进制,右边是 ASCII 显示。
# 查看文件前 64 字节的十六进制 xxd -l 64 file.txt # 输出示例: # 00000000: 6c69 6e65 310d 0a6c 696e 6532 0d0a line1..line2.. # 0d0a 就是 \r\ncat -A也是个好工具,它会把不可见字符显示出来:\r显示为^M,\n显示为$。所以\r\n会显示成^M$,一眼就能看出来。
cat -A file.txt # line1^M$ # line2^M$6.2 编辑器里显示和转换换行符
VS Code 右下角状态栏会显示当前文件的行尾类型(LF 或 CRLF),点击可以切换。这个功能很实用,尤其是编辑跨平台文件时。Sublime Text 通过View -> Line Endings查看和切换。Notepad++ 在视图 -> 显示符号 -> 显示行尾符里可以打开行尾显示。
我的习惯是在 VS Code 里设置"files.eol": "\n",让新建文件默认用 LF。这样即使团队里有 Windows 用户,提交到 Git 的代码也是统一的。Git 本身也有core.autocrlf配置来处理这个问题:
# Windows 上:提交时转 LF,检出时转 CRLF git config --global core.autocrlf true # Linux/Mac 上:提交时转 LF,检出时不转 git config --global core.autocrlf input # 完全禁用自动转换 git config --global core.autocrlf false6.3 编写健壮的文本解析代码
处理外部文本时,防御性编程很重要。我的经验是:永远不要假设换行符是某一种。解析前先规范化,或者用能兼容多种换行符的正则。
import re def parse_lines(text): """健壮的行解析,兼容 \r\n、\n、\r""" # 方法1:先规范化 normalized = text.replace('\r\n', '\n').replace('\r', '\n') return normalized.split('\n') def parse_lines_regex(text): """用正则分割,兼容多种换行符""" return re.split(r'\r\n|\r|\n', text) # 两种方法效果类似,方法1更直观 text = "a\r\nb\rc\nd" print(parse_lines(text)) # ['a', 'b', 'c', 'd'] print(parse_lines_regex(text)) # ['a', 'b', 'c', 'd']正则\r\n|\r|\n的顺序很重要:必须把\r\n放在前面,否则\r会先匹配,导致\r\n被拆成两个空行。这是个经典的正则陷阱,我在代码审查里见过好几次。
6.4 换行符问题的系统化排查流程
遇到换行符相关问题时,按这个流程走能快速定位:
- 看原始字节:用
xxd或cat -A确认实际换行符是什么。 - 确认读取方式:检查代码里打开文件时用的模式(文本/二进制)和
newline参数。 - 检查处理逻辑:正则、
split、strip等操作是否正确处理了\r。 - 确认输出目标:目标平台或协议要求什么换行符,是否需要转换。
- 验证修复:转换后再次用十六进制确认,并跑一遍完整流程。
这个流程看起来简单,但能覆盖 90% 以上的换行符问题。我处理过最复杂的一次是三层系统间的换行符不一致:前端传\r\n,中间件转成了\n,后端又按\r\n解析,结果每行都多一个空行。按这个流程逐层排查,半小时就定位了。
7. 一些容易被忽略的边界情况
7.1 空行和连续换行符的处理
连续换行符会产生空行,处理时要注意区分"空行"和"行尾换行符"。比如"a\n\nb"用split('\n')得到['a', '', 'b'],中间的空字符串代表一个空行。如果业务逻辑不想要空行,需要额外过滤。
text = "a\n\nb\n" lines = text.split('\n') # ['a', '', 'b', ''] # 去掉空行 non_empty = [line for line in lines if line] # ['a', 'b'] # 注意:末尾的 '' 是最后一个 \n 产生的splitlines()方法比split('\n')更智能,它能识别多种换行符,并且不会在末尾产生多余的空字符串(除非文本以换行符结尾且你用了keepends=True)。
text = "a\r\nb\rc\n" print(text.splitlines()) # ['a', 'b', 'c'] print(text.split('\n')) # ['a\r', 'b\rc', '']7.2 换行符与字符串长度计算
\r\n是两个字符,\n是一个字符。计算字符串长度或字节数时,这个差异会影响结果。比如限制用户输入 100 个字符,如果用户从 Windows 复制了带\r\n的文本,实际字符数会比看起来多。
text_win = "line1\r\nline2" text_unix = "line1\nline2" print(len(text_win)) # 12 print(len(text_unix)) # 11 print(len(text_win.encode('utf-8'))) # 12 print(len(text_unix.encode('utf-8'))) # 11如果长度限制很严格,最好在计算前先规范化换行符,或者明确按字节数限制。
7.3 数据库中的换行符存储
不同数据库对换行符的处理略有差异。MySQL 的TEXT类型会原样存储换行符,但CHAR类型可能会截断尾部空格和换行。PostgreSQL 的TEXT也是原样存储。查询时如果用LIKE匹配,\r可能会干扰结果。
我的做法是:入库前统一规范化为\n,查询时如果需要匹配行尾,用LIKE '%\n'而不是LIKE '%\r\n'。这样逻辑简单,不容易出错。
-- 查询以换行结尾的记录(已规范化为 \n) SELECT * FROM articles WHERE content LIKE '%\n'; -- 如果没规范化,可能需要同时考虑 \r\n SELECT * FROM articles WHERE content LIKE '%\r\n' OR content LIKE '%\n';7.4 换行符在 JSON 和 XML 中的转义
JSON 字符串里,换行符必须转义成\n(JSON 规范不允许字符串里出现原始换行符)。\r转义成\r。解析 JSON 时,这些转义会被还原成实际字符。
{ "text": "line1\nline2\r\nline3" }XML 里换行符可以原样出现,但解析时 XML 处理器可能会做换行符规范化(把\r\n和\r都转成\n),这是 XML 规范的一部分。如果依赖原始换行符,需要用xml:space="preserve"或者用 CDATA 包裹。
<text><![CDATA[line1 line2]]></text>这些细节在跨系统数据交换时特别重要。我做过一个 JSON API 对接,对方系统对换行符做了额外转义,导致我们解析出来的文本里全是字面的\n两个字符,而不是真正的换行。排查后发现是对方在生成 JSON 时手动拼接字符串,把\n写成了\\n。这种问题只能靠仔细核对原始报文来解决。
8. 我个人的换行符处理习惯
写了这么多年代码,换行符这块我总结了几条自己的习惯,分享出来供参考。
第一条:入口规范化,出口按需转换。所有外部输入的文本,在进入业务逻辑前统一把\r\n和\r转成\n。输出时根据目标平台决定是否转回\r\n。这样中间层永远只处理\n,逻辑最简单。
第二条:读文件优先用二进制模式做检测,用文本模式做处理。需要确认换行符类型时用'rb',正常处理时用文本模式加合适的newline参数。两者结合,既能看到真相,又能简化代码。
第三条:正则里永远用\r?\n而不是\n。这个习惯能避免绝大多数行尾匹配问题。虽然多打了几个字符,但省下的调试时间远超这点成本。
第四条:跨平台项目在 Git 里配置.gitattributes。明确指定文本文件的行尾处理方式,避免团队成员不同系统导致的换行符混乱。
# .gitattributes * text=auto *.sh text eol=lf *.bat text eol=crlf *.py text eol=lf第五条:遇到诡异问题时先看十六进制。换行符问题最坑的地方就是"看不见",xxd和cat -A是最快的定位工具。养成这个习惯后,很多问题几分钟就能确认根因。
换行符这东西,说小很小,就是几个控制字符;说大也大,跨平台、跨协议、跨语言时处处是坑。把这四种换行符的来龙去脉搞清楚,再配上规范化的处理习惯,能省下大量排查时间。希望这些经验对你有用,下次再遇到"文件变成一行"或者"正则匹配不上"的时候,能第一时间想到往换行符上查。