文本处理速查表

去重、删空行、对比差异、打码、统计字数……这些零碎的文本操作几乎每个开发者和运营都会遇到。本页把 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('a​b 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('a​b'))  → 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 世纪起被排版行业沿用。用它的好处是读者不会被内容吸引而忽略排版本身,能专心看字体、行距和留白。做设计稿和前端组件联调时用它占位,比用中文假文更容易判断西文的视觉效果。