文本处理速查表
去重、删空行、对比差异、打码、统计字数……这些零碎的文本操作几乎每个开发者和运营都会遇到。本页把 9 类常见操作的作用与场景列成对照表,并厘清最容易混淆的五种统计口径。
原理解析
文本处理是开发者的高频刚需:大小写转换、空白压缩、换行符统一(Windows 的 CRLF 与 Unix 的 LF 混用常引发 diff 噪声)、编码探测与转码(UTF-8/GBK 互转)、去重与计数。
底层靠字符编码知识与正则。注意「看不见的字符」——BOM、零宽空格、不间断空格——会让字符串比较与哈希莫名失败。处理用户输入应先归一化(NFC)、再校验,才能避免隐蔽 bug。
什么时候该查这张表
这张表在你要对文本做查找替换、分割合并、大小写转换、去重等处理时有用。它列出常见文本操作各自做什么与典型场景。选操作前先明确要处理的是「单行」「多行」还是「按分隔符的记录」。
不该查的情况:你在做全文检索引擎或文本挖掘。本表是日常的字符串与行处理操作,不含索引、分词与语义分析。
判断标准:先看输入结构。单行内的操作(提取邮箱、替换字符)与跨行的操作(按行分割、去重排序)用的工具完全不同;处理 CSV/TSV 这类结构化文本时要按分隔符解析而不是按行 split,否则字段里的换行会破坏结构。
特别注意空白字符与不可见字符。从网页复制、从 PDF 复制来的文本常带零宽字符(U+200B–200D)、不换行空格(U+00A0)、各种引号变体,它们会导致「明明看起来一样却匹配不上」。做文本比对前先统一规范化。
边界与常见陷阱
不可见字符让「看起来一样」的文本匹配失败
从网页或 Word 复制的文本常混入零宽空格(U+200B)、零宽连接符(U+200D)、不换行空格(U+00A0)、BOM、以及直角引号变体。结果是 「trim()」 之后仍有内容、字符串比较不相等、正则 「^abc$」 不匹配 —— 而且完全看不出原因。防御:入库或比对前做规范化(转成 NFKC、去零宽字符、把各种引号统一为 ASCII),并在调试时打印字符码而不是肉眼比对。
按行 split 处理 CSV 会因字段内换行而破坏结构
RFC 4180 允许字段内包含换行,只要用双引号包裹。所以按行 split 会把一条记录拆成两半,字段数对不上,后续处理全乱。正确做法是用真正的 CSV 解析器(Python 的 csv 模块、papaparse、ExcelJS 等),它们会处理引号转义、字段内换行与分隔符转义。手写 split 只能处理「确认无引号无换行的简单格式」。
全局替换会误伤不该动的地方
「把所有点号换成逗号」这种全局替换会同时改掉小数点、版本号(1.2.3)与域名。更隐蔽的是正则替换里 「.」 匹配任意字符:「a.c」 会匹配 abc、a1c,用它做分隔符转换会误伤。规则:替换前确认模式会匹配多少处(先只统计不改),用 「\b」 或字符类限定边界(「\d+\.\d+」 只匹配小数),并对关键数据保留原文以便回滚。
与相邻内容的分工
要实际执行这些文本操作,用站内文本相关工具;本页是操作选型与风险提示。
要处理编码与乱码问题,用站内「字符编码」页;本页关注文本处理而非编码。
要设计日志采集的脱敏与解析方案,属日志系统范畴,本表不覆盖。
若目的是排查「文本匹配不上」,先打印字符码看有没有不可见字符,再考虑正则逻辑。
实战片段
要点:① 文本里常混有零宽字符(U+200B–U+200D)、不换行空格(U+00A0)、BOM 与各种引号变体,导致「看起来一样却匹配不上」—— 比对前必须做 NFKC 规范化并去零宽字符,调试时打印字符码而不是肉眼比对;② CSV 必须用真正的解析器处理,字段内允许包含换行(RFC 4180),按行 split 会把一条记录拆成两半;③ 替换前先统计命中数,用字符类限定边界 —— 裸的点号在正则里匹配任意字符,a.c 会命中 abc,全局替换点号还会改坏小数与版本号;④ 去重要按业务键而不是整行文本。
// ① 规范化:去不可见字符 + NFKC + 引号统一
const ZERO_WIDTH = /[-]/g
const QUOTES = { \u2018: "'", \u2019: "'", \u201C: '"', \u201D: '"' }
function normalizeText(s) {
return s
.replace(ZERO_WIDTH, '') // 零宽字符与 BOM
.replace(/ /g, ' ') // 不换行空格 → 普通空格
.replace(/[‘’]/g, "'") // 中文引号 → ASCII
.replace(/[“”]/g, '"')
.normalize('NFKC') // 全角 → 半角、兼容字符归一
.replace(/
/g, '
') // 换行统一
.trim()
}
console.log(normalizeText('ab c').length) // 3 而不是 5
// ② 调试时打印字符码而不是肉眼比对
function showCodes(s) {
return [...s].map(c => {
const cp = c.codePointAt(0)
const hex = cp.toString(16).toUpperCase().padStart(4, '0')
// 用普通字符串而非模板串:这一行只是演示如何显示字符码,
// 用模板串会与本页大量出现的美元符号加大括号字面量冲突(TS 会当插值解析)。
return hex + (cp < 128 ? ' ' : ' ')
}).join(' ')
}
// console.log(showCodes('ab')) → 0061 200B 0062
// ③ CSV 必须用解析器,不能按行 split(字段内可能有换行)
import { parse } from 'papaparse'
const csv = 'a,b
"line1
line2",c' // 第二行含引号包裹的换行
const wrong = csv.split('
').filter(Boolean) // ❌ 拆成 3 段
const right = parse(csv, { header: true }).data // ✅ 2 条记录
console.log(wrong.length, right.length) // 3 2
// ④ 替换前先统计命中数,且用字符类限定边界
function countMatches(text, re) {
return (text.match(new RegExp(re.source, 'g')) || []).length
}
const text = '1.5 and 1.2.3 and v1.0 and 2.25'
// ❌ /./g 会命中所有点:4 处
// ✅ 只匹配小数:d+.d+
console.log('所有点:', countMatches(text, /./g)) // 4
console.log('仅小数:', countMatches(text, /\d+\.\d+/g)) // 2
const fixed = text.replace(/d+.d+/g, m => m.replace('.', ','))
console.log(fixed) // 1,5 and 1.2.3 and v1.0 and 2,25 ← 版本号未被动
// ⑤ 去重排序要按业务键,不是按整行
const lines = ['b,2', 'a,1', 'b,2']
const seen = new Set()
const uniq = lines.filter(l => { if (seen.has(l)) return false; seen.add(l); return true })
console.log(uniq) // ['b,2', 'a,1'] —— 重复的 b,2 被去掉实践注解
- 「去重」要先定义比较口径按整行、按去空白后的行、还是按大小写不敏感去重,结果差别很大。也不要用去重替代「保留首次出现」这类业务语义——如果数据里含时间戳,去重后保留哪一条会直接影响后续分析。
- 打码与脱敏不可逆,但字数会暴露信息把手机号中间四位替换成 **** 之后,剩余位数与格式本身仍是信息(可判断号段与运营商)。真正脱敏应整体替换或使用等长占位,并在日志中同步处理,否则一次误打印即泄露。
- Unicode 归一化会改变字节但不改变语义同一个「é」可以用单码位或 e + 组合重音两种方式表示,直接比较不相等。做文本比对或去重前先做 NFC 归一化,否则会出现「肉眼一样却不相等」的诡异结果。
数据来源
- Unicode UAX #15 (normalization)
- Unicode UTS #18 (regular expressions)
- Unicode Annex #11 (East Asian width)
上述编号、名称与定义均来自公开规范,本站不主张对规范数据本身的独创性;本页的编排方式、中文释义与实践注解由本站撰写。
9 类常见文本处理操作
| 操作 | 做什么 | 典型场景 |
|---|---|---|
| 字数统计 | 统计字符数、词数、行数、字节数与最长行 | 投稿字数限制、接口报文长度校验 |
| 去重行 | 删除重复行,通常保留首次出现的顺序 | 清洗导出名单、合并多个关键词表 |
| 删除空行 | 去掉纯空白行与仅含空格的行 | 整理从 PDF 或网页复制的文本 |
| 文本对比 | 逐行或逐词比较两份文本,标出增删改 | 核对配置文件改动、审稿改稿 |
| 字符串打码 | 把部分字符替换为星号或掩码 | 截图前隐藏手机号、邮箱、密钥 |
| 单复数转换 | 按英文规则把名词转成单数或复数 | 生成国际化文案、代码里的量词 |
| 邮箱归一化 | 统一大小写、拆分别名与域名、去重点 | 去重注册用户、清洗邮件列表 |
| NATO 音标 | 把字母转成 Alpha、Bravo、Charlie 等读音词 | 电话里核对订单号、验证码 |
| 占位文本生成 | 生成指定段落或字数的假文填充排版 | 设计稿占位、前端组件联调 |
五种统计口径的区别
同样一段文字,换一种口径数字可以差三倍。统计前先确认对方要的是哪一种。
| 口径 | 定义 | 示例 |
|---|---|---|
| 字符数 | Unicode 码点个数,一个汉字算 1 | 「你好 hi」→ 4 |
| 字节数 | UTF-8 编码后的存储字节数,汉字通常 3 | 「你好 hi」→ 8 |
| 词数 | 按空白切分;中文无空格需按字或分词统计 | 「你好 hi」→ 1 或 3(视口径) |
| 行数 | 换行符个数加一,注意 CRLF 与 LF 差异 | 两行文本 → 2 |
| 显示宽度 | 全角字符按 2、半角按 1 估算的占位宽度 | 「你好 hi」→ 6 |
常见问题
中文字数到底怎么算才算对?
没有唯一答案,取决于用途。投稿和稿费结算通常按「字符数(不含空格)」或「字数」口径,一个汉字算一个字;数据库字段长度限制看的是字节数,UTF-8 下一个汉字占 3 字节;而排版宽度要看显示宽度,全角算两格。统计前先确认对方要的是哪一种口径,差距可以到三倍。
去重行会不会打乱原来的顺序?
多数工具的默认行为是保留首次出现的顺序,也就是保留第一次读到该行时的位置,删除后面重复的行。如果你需要按字母排序后再去重,应该先排序再去重。反过来,如果顺序本身有意义(比如日志时间线),就不要先排序。
NATO 音标字母表现在还有用吗?
有,而且在电话客服、远程协作场景里非常实用。它把 26 个字母映射到不易听混的词(A 是 Alpha、B 是 Bravo),专门解决 b 和 d、m 和 n 在语音里难以区分的问题。核对订单号、验证码、序列号时逐字念音标词,比反复说「b 还是 d」高效得多。
Lorem ipsum 到底是什么,为什么大家都用它?
它是一段经过改写、看起来像拉丁文但没有实际含义的填充文本,源自西塞罗的著作,从 16 世纪起被排版行业沿用。用它的好处是读者不会被内容吸引而忽略排版本身,能专心看字体、行距和留白。做设计稿和前端组件联调时用它占位,比用中文假文更容易判断西文的视觉效果。