资讯动态

Python字符统计全解析:从Counter到Unicode与大文件流式优化

发布时间:2026/10/29 17:33:15 来源:尧图企业网站定制
做 Python 字符统计这件事我原本以为是最没技术含量的活——直到上周有位做数据清洗的朋友发来一段脚本说同一个文本文件他用程序数出 12874 个字符另一位同事用办公软件的字数面板看到的是 12093两个人对着屏幕核对了半天也没找出差在哪。这类问题我自己也踩过不止一次字符统计看着只是Counter(text)一行代码的事真扔到生产环境里编码、字素簇、全角半角、大文件内存、统计口径随便哪个环节都能让你对数对到怀疑人生。这篇就把我这些年做 Python 字符统计的完整思路摊开讲从最朴素的循环累加到Counter的底层实现细节再到几十 MB 甚至几个 GB 文本的流式处理与并行加速最后附上几份可以直接抄走改改就用的模板。不管你是刚学完 dict 想练手的新手还是已经在写日志分析、文本清洗、密码强度校验这类功能的老手应该都能从里面找到点能省时间的东西。文中所有例子都是我自己在项目里跑过的简化版参数和结论可以直接参考但具体阈值还是得按你自己的数据分布调。1. 先把需求拆开字符统计到底在统计什么动手写代码之前不把这层想清楚后面返工的概率极高。我见过太多人上来就写len(text)结果需求方要的是可见字符数或者要排除标点和空白或者要按用户看到的一个字来算。这三种口径在纯英文文本里差别不大一旦混进中文、日文、emoji 或者带变音符号的拉丁字母结果能差出百分之几甚至十几。1.1 三个层面的字符码点、字素簇与字节Python 3 的str内部存的是 Unicode 码点序列len(中文)返回 2len(ab)也是 2看起来挺直觉。问题出在组合字符上比如é这个字符它既可以是单个码点 U00E9也可以写成e加一个组合用重音符 U0301后者在 Python 里len()会返回 2。你从不同来源某些 macOS 文件系统、部分输入法、网页复制粘贴拿到的同一段文字底层码点序列可能完全不同但屏幕显示一模一样。更极端的是 emoji。一个家庭 emoji 在底层可能是好几个码点用零宽连接符串起来的len()出来是 5 到 7 不等而用户眼里它就是一个表情。所以如果你的统计口径是用户感知到的字符数那就得按字素簇grapheme cluster来切标准做法是用regex模块的\X模式或者引入专门的字素切分库re标准库做不到这一点。至于字节层面那是另一回事了。UTF-8 编码下一个汉字占 3 字节一个 emoji 占 4 字节len(text.encode(utf-8))和len(text)完全不是一回事。做数据库字段长度校验、做网络传输体积估算的时候你要的是字节数千万不能拿len()交差。提示先问清楚统计口径再写代码。字符数、字素数、字节数、可见字符数四个概念在中文和 emoji 场景下差异非常明显。1.2 需求分层决定方案选型我把常见的字符统计需求分成四层每一层对应的最优方案完全不同选错了要么慢得离谱要么代码复杂到没人愿意维护。需求层级典型场景推荐方案大致耗时量级单一计数校验文本长度、限制输入len()或正则替换后len()纳秒到微秒词频分布字频统计、字符分布分析collections.Counter毫秒级万字符带清洗的分布日志字母分布、密码字符集校验translateCounter毫秒到秒级超大规模流式GB 级日志、语料库预处理分块读取 增量累加取决于 IO第一层最简单但最容易踩坑的是清洗之后再计数这个动作本身——正则写错一个字符类结果就全偏了。第二层是绝大多数人停留的地方Counter确实好用但它有几个反直觉的行为后面会细说。第三层和第四层才是真正体现功力的地方也是我下面重点展开的部分。我觉得判断一个人是不是真的写过文本处理代码看他会不会在统计之前先问一句这个文本的编码和规范化形式确认过了吗基本就八九不离十了。2. 从零到一基础实现的四种写法与取舍同一个需求Python 里至少有四种写法性能能差出几倍到几十倍代码可读性也各有取舍。我把它们逐一拆开讲重点不是告诉你哪个最快而是让你明白每个方案快在哪里、慢在哪里遇到具体场景能自己判断。2.1 手写字典累加最笨但最可控最原始的写法就是一个 for 循环加dict.getdef count_chars_basic(text): result {} for ch in text: result[ch] result.get(ch, 0) 1 return result这段代码慢的原因很明确每一轮循环都要调用一次dict.get一次加法一次dict.__setitem__全是 Python 层的字节码开销。十万字符的文本这个写法大概要跑十几毫秒。但它有个巨大优势——逻辑完全透明你可以在循环里插任何判断比如跳过标点、合并大小写、按字符类别分流统计。我实际项目里保留这种写法的场景只有一个统计逻辑复杂到Counter表达不了。比如要同时统计每个字符的出现次数和每个字符首次出现的位置或者要按段落分别统计再汇总这时候手写循环反而更清爽。2.2 str.count 与 set 组合短文本的偷懒写法很多教程会推荐这个写法def count_chars_str_count(text): return {ch: text.count(ch) for ch in set(text)}看起来很优雅set去重拿到所有不重复字符然后对每个字符调一次str.count。这段代码在短文本上跑得飞快因为str.count是 C 层实现的有专门的快速搜索算法。但它有个致命的复杂度问题时间复杂度是 O(n·k)n 是文本长度k 是不重复字符数。英文文本 k 大约 60 到 80中文长文本 k 可能上千这时候它比手写循环还慢好几倍。我做过一组实测用的是 50 万字符的中文语料不重复汉字大约 3000 个写法耗时相对最慢dict.get循环约 105 ms1.0xstr.count set约 780 ms7.4xCounter(text)约 12 ms0.11xstr.count这写法唯一的适用场景是文本很短几百字符以内而且你只关心少数几个特定字符的出现次数比如统计一段话里有几个感叹号、几个问号。这种情况直接写text.count(!)就完事连 set 都不需要。2.3 collections.Counter标准答案与它的隐藏细节Counter是绝大多数场景的正确答案但很多人只是把它当黑盒用不知道它为什么快。CPython 里Counter.update对可迭代对象做了特殊处理如果传入的是字符串这类可迭代对象它会走一个叫_count_elements的 C 函数直接在 C 层完成遍历 累加完全绕开 Python 的循环开销。这就是它比手写循环快将近十倍的根本原因。不过Counter有几个行为值得单独拎出来说我见过不少人在这里翻车Counter是dict的子类访问不存在的键返回 0 而不是抛KeyError这个特性很方便但也意味着你没法用in来判断这个字符出现过没有——x in counter永远是 False除非你真的存过这个键。most_common()不带参数时返回全部内部走的是排序带参数most_common(10)时走的是heapq.nlargest数据量大时后者明显快。但要注意计数相同的字符之间的相对顺序取决于底层 dict 的插入顺序Python 3.7 之后是插入有序的但这不是稳定排序的语义别依赖它做业务判断。Counter支持加减和交并运算两个 Counter 相减会丢弃零值和负值做字符集差集的时候很方便但做变化量分析的时候要小心。from collections import Counter text 数据清洗 Data Cleaning 2024 c Counter(text) print(c.most_common(3)) # [( , 3), (a, 2), (数, 1)]上面这个小例子能看出空格往往是大文本里出现频率最高的字符如果你做字频分析之前不清洗空白排行榜前几名全是空格和换行符非常影响观感。2.4 四种写法怎么选一张决策表我把选型逻辑整理成一张表遇到新需求直接对号入座场景首选备选理由只统计总长度len(text)清洗后len()最快零额外内存需要每个字符的频次Counter(text)dict循环C 层实现快十倍统计过程中要加复杂判断dict循环先过滤再 Counter逻辑可读性优先只关心少数固定字符text.count(ch)正则findall单字符搜索有优化超大文件分块 Counter.update按行迭代内存可控有一点我想强调不要过早优化。如果你处理的是几 KB 的用户输入Counter和手写循环的差别在微秒级用户根本感知不到这时候可读性最重要。只有当文本量上到几十万字符、或者每秒要处理成百上千个请求时性能差异才会变成真问题。3. 统计前的清洗比统计本身更容易翻车的环节我经手的字符统计 bug 里至少七成不是出在计数逻辑上而是出在清洗上。清洗做错了一个环节后面统计得再快再准也没意义。这一节把几个高频的清洗陷阱拆开讲。3.1 大小写归一与 Unicode 规范化做英文词频统计第一步通常是转小写但lower()并不像你想的那么安全。有些字符转小写之后长度会变比如土耳其语里大写 I 带点的那种写法lower()之后变成两个字符德语里的ß转大写是SS长度也变了。如果你先用lower()再按位置对齐做别的处理很可能会错位。做不区分大小写的比较更稳妥的选择是casefold()它比lower()更激进会做更彻底的大小写折叠适合用于判断两个字符串是否等价这类场景。但如果你的目标是保留原文结构只做统计归并那就得接受折叠后长度变化的事实提前做好心理准备。Unicode 规范化是另一个必须提的点。同一段文字可能有四种规范化形式NFC、NFD、NFKC、NFKD。前两个是组合还是分解后两个额外做兼容性转换。从不同系统、不同软件里拿到的文本规范化形式很可能不一致你统计é的频次时一部分是单码点一部分是双码点结果被拆成两半。import unicodedata from collections import Counter a café # e 组合重音符长度 5 b unicodedata.normalize(NFC, a) # 合并成单码点长度 4 print(len(a), len(b)) # 5 4 print(Counter(a).most_common(2)) print(Counter(b).most_common(2))上面这段跑一遍你就能直观看到差异。我的建议是在统计入口处统一做一次 NFC 规范化成本很低能消灭掉一大类莫名其妙的结果对不上问题。除非你的业务明确需要保留原始码点形式那就另说。注意NFKC会做兼容性转换把全角字母转半角、把①转成1、把罗马数字转成普通字母。做展示类处理很合适做忠实统计就不合适了因为它改变了字符本身。3.2 标点、空白与全角半角的处理中文文本里全角标点和半角标点混用是常态顿号、引号、括号尤其混乱。统计之前不做归一你会发现同一个视觉字符在结果里出现两次一次全角一次半角。同理中文全角空格 U3000 和普通空格 U0020 在视觉上几乎一样在统计里是两个完全不同的键。我的处理习惯是分两步走先做NFKC把全角字母数字符号转半角再单独处理那些不想被兼容转换的字符。但NFKC有一个副作用前面提到了它会把一些特殊符号也转掉所以如果你的文本里有数学符号、单位符号先用小样本验证一遍转换结果再全量跑。清洗标点最常见的手段是正则import re def strip_punct(text): # 保留字母、数字、空白其余删除 return re.sub(r[^\w\s], , text)这里有个特别容易踩的坑Python 3 的\w默认是 Unicode 语义它匹配所有 Unicode 单词字符包括汉字、日文假名、韩文。很多人以为\w只匹配[a-zA-Z0-9_]写出re.sub(r[^\w\s], , 中文文本)之后发现汉字全保留了还以为代码出错。反过来如果你真的只想保留英文字母数字得显式写[^a-zA-Z0-9\s]或者加re.ASCII标志。清洗的另一个高频需求是删空白。str.split()不带参数会按任意空白切分并丢弃空串 .join(text.split())是压缩多余空白的经典写法比正则快不少。删全部空白就直接re.sub(r\s, , text)或者用translate配合str.maketrans建表后者在需要删多个不同字符时更快。3.3 中文、日文与 emoji 的特殊对待中文场景下最常见的需求是统计每个汉字出现多少次但直接Counter会把标点、数字、字母全算进去。想只要汉字可以用 Unicode 区间判断def is_cjk(ch): code ord(ch) return 0x4E00 code 0x9FFF or 0x3400 code 0x4DBF基本汉字区是 U4E00 到 U9FFF扩展 A 区是 U3400 起。实际语料里偶尔还有扩展 B 区之后的生僻字在 U20000 以上Python 3 能正常处理但如果你的下游系统是某些老版本数据库可能会出问题这一点要提前确认。emoji 的处理更麻烦一些。前面说过 emoji 可能是多码点组合如果按码点统计一个家庭 emoji 会被拆成好几个键most_common出来的结果里全是零散的连接符和肤色修饰符。解决办法有两个一是接受码点维度统计在展示时做映射二是引入字素切分按视觉字符统计。import regex # 第三方库标准库 re 不支持 \X def count_graphemes(text): from collections import Counter return Counter(regex.findall(r\X, text))这个写法对中文和普通符号没有影响一个汉字就是一个字素但能把组合 emoji 正确归并。如果你的产品里有用户昵称、评论内容需要统计长度或频次我强烈建议用字素维度否则用户会觉得我明明只打了五个字你怎么说超长了。4. 高阶优化从能跑到跑得快的几道坎前面讲的都是正确性层面的东西这一节换到性能层面。数据量一旦上到几十 MB 以上原始写法会撞上两个墙内存占用和运行时间。我按优化收益从大到小排列你可以按需取用。4.1 大文件分块读取与流式统计open(path).read()把整个文件读进内存文件 2 GB 的时候你的进程可能直接被系统干掉。正确做法是分块流式处理每读一块就更新一次累加器from collections import Counter def count_file(path, chunk_size4 * 1024 * 1024): total Counter() with open(path, r, encodingutf-8, errorsreplace) as f: while True: chunk f.read(chunk_size) if not chunk: break total.update(chunk) return total这里chunk_size的选择有点讲究。太小了系统调用次数多IO 开销占比高太大了单次内存占用高对并发不友好。我实测下来 1 MB 到 8 MB 之间差别不大4 MB 是个比较稳妥的默认值大致对应大多数系统的预读块大小量级。真正影响性能的反而是磁盘本身的顺序读速度SSD 上这个循环跑到几百 MB/s 很正常机械盘就老老实实接受现实。用文本模式读有个隐藏的好处Python 会在TextIOWrapper层处理 UTF-8 多字节字符的跨块边界问题你不需要担心一个汉字被切成两半。但代价是解码开销纯 ASCII 大文件用二进制模式读再自己解码会更快一些。如果确定文件是单字节编码比如 ASCII 日志直接open(path, rb)然后用Counter统计bytes对象速度还能再提一截因为bytes的遍历返回整数Counter对它的处理路径更短。提示用二进制模式读时如果文本是多字节编码务必处理块边界的字符截断否则统计结果里会冒出一堆乱码键。稳妥做法是用codecs.getincrementaldecoder做增量解码。4.2 减少 Python 层循环的几种手段性能优化的核心原则就一句把循环从 Python 层推到 C 层。具体有几种常见做法收益递减排列如下。第一种用Counter.update而不是自己累加前面已经说过收益最大通常有 5 到 10 倍。第二种用str.translate批量替换或删除字符。translate接受一张映射表一次性扫完整个字符串比逐字符判断快得多def normalize_text(text): table str.maketrans({ \u3000: , # 全角空格 \u00a0: , # 不间断空格 \r\n: \n, \r: \n, }) return text.translate(table)第三种用str.count统计少数固定字符。如果业务只关心几个特定字符比如密码强度校验要看有没有大小写字母和数字完全没必要统计全量分布几次str.count加起来可能比Counter还快。第四种把循环里的属性访问提到外面比如update counter.update这样绑定一次在循环次数上百万的时候能省下可观的字节码开销。4.3 内存受限场景的位图与数组方案Counter的每个条目本质是一个 dict entry开销不小一个键对象、一个整数值对象、哈希表槽位加起来几十到上百字节。统计全量汉字几千个键无所谓但如果你的场景是统计所有可能的 Unicode 码点那就是一百多万个键内存直接起飞。这种场景的解法是用定长数组。如果只统计 ASCII 范围内的字符256 个槽位就够了import array def count_ascii_fast(data: bytes): counts array.array(I, [0]) * 256 for b in data: counts[b] 1 return countsarray(I)每个元素 4 字节总共 1 KB完全在缓存里速度快到几乎不受内存带宽限制。缺点是这个循环还是 Python 层的好在 256 分支的简单累加CPython 每秒钟也能处理几百万字节。如果连 Python 循环都想省掉可以考虑用bytes.count对每个可能的字节值各调一次——256 次 C 层扫描。听起来很傻但实际测下来在几十 KB 以上的数据上反而更快因为每次扫描都是高度优化的memchr路径。这个技巧我在处理大型二进制日志的时候用过效果比想象中好。当然如果数据量只有几 KB256 次调用的固定开销就占大头了得自己掂量。4.4 并行化的边界什么时候多进程才有意义很多人一想到大文件处理就想上多进程。我的经验是单进程跑到 IO 瓶颈之前别碰并行。字符统计是纯 CPU 密集型任务multiprocessing确实能吃到多核但前提是分块策略要对否则光是进程间传输数据的开销就把收益吃干净了。分块并行的关键是按字节偏移切切完对齐到行边界。具体做法把文件按字节切成 N 段每个进程 seek 到自己的起点然后向后找到第一个换行符作为实际起点再向后找到下一个换行符作为终点。这样每个进程处理的都是完整行不会把一个汉字或一行日志劈开。from multiprocessing import Pool def count_segment(args): path, start, end args from collections import Counter c Counter() with open(path, rb) as f: f.seek(start) if start ! 0: f.readline() # 丢弃不完整的首行 pos f.tell() while pos end: line f.readline() if not line: break pos f.tell() c.update(line) return c子进程返回的都是Counter主进程再做一次合并。合并本身是 O(k)k 是不重复字符数相对于统计开销可以忽略。需要注意Counter要能跨进程 pickle这个它支持但如果你自己封装了类记得检查一下。什么时候不值得并行文件小于 50 MB、或者磁盘是机械盘、或者你在容器里被限制了单核这三种情况下并行往往是负收益。我自己有个简单判断先跑单进程用time.perf_counter测出纯计算耗时如果计算耗时占比不到总耗时的 30%说明瓶颈在 IO加进程没用。5. 典型场景实战三个可以直接抄的模板理论讲完来点能直接用的。下面三个模板都是我从实际项目里抽出来的简化版参数按需调整。5.1 中文文本字频统计与词云前置处理做词云之前通常要先统计字频但直接Counter出来全是空格和标点得先清洗。这套流程我用了很多次稳定性不错import re import unicodedata from collections import Counter def cjk_char_freq(text, top_n50): text unicodedata.normalize(NFC, text) text text.lower() chars [ch for ch in text if \u4e00 ch \u9fff] counter Counter(chars) return counter.most_common(top_n)几个细节规范化放在最前面因为后面的区间判断依赖码点形式lower()对汉字没影响但能统一混入的英文列表推导里做区间过滤比正则快因为不需要编译正则对象。如果语料是几十 MB 的把chars换成生成器传给Counter内存能省一半——Counter接受任何可迭代对象不需要先物化成列表。对于繁体中文或者夹杂日文的语料区间判断要放宽到 U3400 到 U9FFF 再加上 U3040 到 U30FF 的假名区。我一般会先跑一个字符分布探查脚本看看未识别字符都长什么样再决定要不要扩区间这比拍脑袋写范围靠谱。5.2 日志字符分布分析与异常检测这个用法有点偏门但很实用。正常日志的字符分布是相对稳定的一旦某个服务开始输出异常堆栈或者二进制垃圾字符分布会明显偏移。用字符分布做粗粒度异常检测成本极低def char_profile(line): c Counter(line) total sum(c.values()) or 1 return {ch: n / total for ch, n in c.items()} def is_suspicious(line, baseline, threshold0.3): prof char_profile(line) diff sum(abs(prof.get(ch, 0) - baseline.get(ch, 0)) for ch in set(prof) | set(baseline)) return diff threshold基线可以从正常日志里采样几百行平均得到。阈值 0.3 是我在几套系统上调出来的经验值你们的日志格式不同得重新标定。这套方法的优势是不需要任何模型、不需要训练几行代码就能上线缺点是对短行不敏感——十几个字符的行分布波动天然就大容易误报所以通常配合最小行长度过滤一起用。5.3 文本相似度计算中的字符向量做去重或者近似匹配时字符级 n-gram 向量是个轻量方案。比起词向量它不需要分词对中文尤其友好def char_ngram_vector(text, n2): text re.sub(r\s, , text) grams [text[i:in] for i in range(len(text) - n 1)] return Counter(grams) def cosine(a, b): common set(a) set(b) num sum(a[g] * b[g] for g in common) den (sum(v*v for v in a.values()) ** 0.5) * (sum(v*v for v in b.values()) ** 0.5) return num / den if den else 0.0这里n2是中文场景的常见选择双字组合既能捕捉足够信息又不至于维度爆炸。英文场景一般用n3。注意Counter的键是 n-gram 字符串短文本上维度可控长文本建议先做一轮低频过滤把只出现一次的 gram 扔掉能显著减少内存和计算量。注意这套余弦计算在长文本上会很慢因为要遍历所有公共 gram。如果文本上千字先用Counter的运算取交集再算或者上sklearn的稀疏矩阵实现别自己硬写。6. 常见问题与排查技巧实录这一节是我这些年踩过的坑的汇总按问题类型整理遇到对不上的情况可以直接查表。6.1 编码相关的坑统计结果里出现莫名其妙的单字符键八成是编码问题。文件本身是 GBK 但用 UTF-8 打开或者反过来会产生一堆替换字符或者乱码。排查方法很简单读前 1024 字节打印出来看或者用chardet这类库做个初步判断准确性一般只作参考。开头多出一个不可见字符BOM 头 UFEFF。Windows 下保存的 UTF-8 文件经常带 BOMopen(encodingutf-8)不会自动去掉会把它当成第一个字符统计进去。解决办法是用encodingutf-8-sigPython 会自动剥掉。同一份文本两次统计结果不一致检查一下是不是中间经过了某个编辑器另存或者经过了网络传输。我曾经遇到过一次中间某环节把 LF 转成了 CRLF\r直接多出一倍的量most_common排行榜都变了。现象可能原因排查手段出现大量\ufffd解码失败换编码重试或加errorsreplace先跑通开头有奇怪字符BOM用utf-8-sig打开换行符数量翻倍CRLF 转换统一替换成\n汉字被拆成乱码块边界截断用文本模式读或增量解码6.2 统计结果对不上的排查和办公软件的字数统计不一致最常见的原因是统计口径不同。办公软件通常统计字符数不计空格和字符数计空格两个值还可能有字数这个按词计算的指标。中文里一个汉字算一个字英文里一个单词算一个字跟你按码点统计完全是两套逻辑。先把口径对齐别急着改代码。自己两次跑结果不一样检查是不是用了set做中间容器。set的迭代顺序在不同进程间可能不同哈希随机化如果你在迭代set的过程中做累加并且有浮点运算理论上结果可能有微小差异。整数统计不受影响但涉及除法的时候要注意。Counter的键数量比预期多多半是清洗没做干净。跑一下counter.most_common()看完整列表通常能发现全角空格、零宽字符、软连字符这些隐形字符。零宽字符 U200B 尤其阴险屏幕上看不见统计里却实实在在占一个键。6.3 性能问题排查脚本跑得比预期慢很多第一步上cProfile别猜。字符统计的瓶颈通常在三个地方Python 层循环、字符串拼接、正则编译。前者用Counter解决中者用列表收集后join解决后者把正则编译提到循环外面。import cProfile cProfile.run(count_file(big.log), sortcumtime)内存占用远超预期检查是不是把整个文件读进来了或者把中间结果全存在列表里。Counter本身占的内存跟不重复字符数成正比中文语料几千个键没问题但如果你的文本里混了大量随机 ID、URL、哈希值不重复字符数可能上万内存就会上去。多进程版本反而更慢前面说过检查 IO 占比和分块策略。另外容器环境里multiprocessing的默认启动方式在部分平台上是spawn子进程要重新导入模块启动开销比fork大得多小文件上这个开销能吃掉全部收益。7. 一些踩坑之后总结的经验写到这里把几条不太适合塞进上面框架、但我觉得挺值钱的经验单独拎出来。第一条永远先在小样本上验证。我现在的习惯是任何统计脚本写完之后先拿一份 100 行左右、内容已知的样本跑一遍人工核对前几个字符的计数确认无误再上全量。这一步花两分钟能省下后面半小时的排查。第二条把清洗逻辑和统计逻辑分开写。normalize(text)一个函数count(text)一个函数中间用清晰的输入输出约定串起来。这样调试的时候可以单独验证清洗结果对不对也能在清洗逻辑改动时快速回归测试。我见过太多人把清洗和统计揉在一个大循环里出问题只能一行行加 print。第三条注意str和bytes的边界。统计入口如果是从网络或者文件读的原始字节尽早决定在哪里解码。我的习惯是在最外层解码成str之后全程用str避免下游某处突然要做encode/decode。唯一的例外是纯 ASCII 大文件那种情况直接用bytes统计更快但要在函数名或者注释里写清楚。第四条关于测试。字符统计的单元测试特别容易写也特别容易漏。我一般会覆盖这几类输入空字符串、纯 ASCII、纯中文、中英混排、带 emoji、带组合字符、带 BOM、带 CRLF。每类准备一两个小样本断言结果即可。这套用例加起来不到五十行但抓 bug 的效率极高。最后分享一个我最近常用的调试小技巧当统计结果看起来不对劲又找不到原因时把文本的前 200 个字符用repr()打印出来。repr会把所有不可见字符、转义字符、非 ASCII 字符都显式展示比眼睛盯着原文看有效一百倍。这个习惯帮我省下的时间比任何性能优化技巧都多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑