bigywater.tech
爬虫工程 · 49

文本清洗:nbsp、零宽字符、全角半角

网页文字里藏着一堆看不见或者看起来一样的字符:不间断空格、零宽空格、全角数字和逗号、组合重音。它们让字符串比较、去重和价格解析静默地出错。这篇逐类用 Python 验证,并给出一个清洗函数。

7 对 7
Python 当空白处理的 7 个“空格类”字符(nbsp、全角空格等)与不当空白处理的 7 个 Cf 字符(零宽空格、BOM 等)
12 而不是 12345
不清洗时,正则 ¥\s*([\d,]+) 对「¥12,345」取到的价格
2 对 1
不规范化与 NFC 之后,「café」两种写法放进集合的大小

空白类字符

实测 · 本机执行输出
  字符                               类别  isspace  strip 去掉   split 当分隔   \s 匹配   显示
  普通空格                         Zs     True         是           是       是   U+0020
  制表符                          Cc     True         是           是       是   U+0009
  换行                           Cc     True         是           是       是   U+000A
  不间断空格 nbsp                   Zs     True         是           是       是   U+00A0
  全角空格(表意文字空格)                 Zs     True         是           是       是   U+3000
  细空格 U+2009                   Zs     True         是           是       是   U+2009
  窄不间断空格 U+202F                Zs     True         是           是       是   U+202F
  零宽空格 U+200B                  Cf    False         否           否       否   U+200B
  零宽不连字 U+200C                 Cf    False         否           否       否   U+200C
  零宽连字 U+200D                  Cf    False         否           否       否   U+200D
  字节顺序标记/零宽不换行 U+FEFF          Cf    False         否           否       否   U+FEFF
  软连字符 U+00AD                  Cf    False         否           否       否   U+00AD
  左到右标记 U+200E                 Cf    False         否           否       否   U+200E
  单词连接符 U+2060                 Cf    False         否           否       否   U+2060
  例:'Price:\xa0¥100\u200b \u3000 ok\ufeff'.strip() = 'Price:\xa0¥100\u200b \u3000 ok\ufeff';.split() = ['Price:', '¥100\u200b', 'ok\ufeff']——零宽空格和 BOM 留在了结果里,看不见但存在

不间断空格 nbsp、全角空格 U+3000、细空格都被 str.strip()、str.split()、正则 \s 当作空白。零宽空格、零宽连字、BOM、软连字符、方向标记属于 Cf 类,isspace() 为 False,留在文本里——比较、去重、关键词匹配会莫名其妙地失败。

NFKC 的利弊

实测 · 本机执行输出
  '123ABC,.'            → NFKC '123ABC,.'            (有变化)
  'アイウ(ハンカク)'           → NFKC 'アイウ(ハンカク)'           (有变化)
  '㈱テスト'                → NFKC '(株)テスト'              (有变化)
  '①②③'                 → NFKC '123'                 (有变化)
  'x²+y³'               → NFKC 'x2+y3'               (有变化)
  'file flow'             → NFKC 'file flow'           (有变化)
  '™ ® ©'               → NFKC 'TM ® ©'              (有变化)
  '12,345.67円'          → NFKC '12,345.67円'          (有变化)
  '“引号”‘单引号’'           → NFKC '“引号”‘单引号’'           (不变)
  '…'                   → NFKC '...'                 (有变化)
  '——'                  → NFKC '——'                  (不变)
  '~'                   → NFKC '~'                   (有变化)
  '‐ – — −'             → NFKC '‐ – — −'             (不变)
  注意:NFKC 把 "x²+y³" 变成 "x2+y3"(数学含义变了)、把 "①" 变成 "1"、把 "fi" 变成 "fi"、把 "™" 变成 "TM";中文引号、省略号、破折号等有的变有的不变——不要无脑对全文做 NFKC
  中文标点 NFKC 之后:,→, 。→。 !→! ?→? :→: ;→; (→( )→) 【→【 】→】 、→、

NFKC 把全角数字字母和逗号变成半角(12,345.67円 → 12,345.67円),但同时改变含义:x² 变 x2、① 变 1、™ 变 TM、… 变 ...。只对需要的字段做,不要对全文做。

NFC 与 NFD

实测 · 本机执行输出
  'café' 与 'café':显示都是 café;==:False;len:4 和 5;NFC 之后相等:True
  韩文 "한글":NFC len=2,NFD len=6,==:False,NFC 之后相等:True
  去重集合:{a, b} 的大小 = 2;规范化后 = 1

带重音的字符可以是一个码点,也可以是“字母加组合符号”;显示一样,== 为 False。去重或比较之前统一成 NFC。

数字解析

实测 · 本机执行输出
  '123'         int() = 123         float() = 123.0       NFKC 后 '123'
  '12.5'        int() = ValueError  float() = ValueError  NFKC 后 '12.5'
  '١٢٣'         int() = 123         float() = 123.0       NFKC 后 '١٢٣'
  '123'         int() = 123         float() = 123.0       NFKC 后 '123'
  '1,234.56'    int() = ValueError  float() = ValueError  NFKC 后 '1,234.56'
  '1,234.56'    int() = ValueError  float() = ValueError  NFKC 后 '1,234.56'
  '1 234,56'    int() = ValueError  float() = ValueError  NFKC 后 '1 234,56'
  '¥100'        int() = ValueError  float() = ValueError  NFKC 后 '¥100'
  '¥100'        int() = ValueError  float() = ValueError  NFKC 后 '¥100'
  '100円'        int() = ValueError  float() = ValueError  NFKC 后 '100円'
  '12.5万'       int() = ValueError  float() = ValueError  NFKC 后 '12.5万'

Python 的 int() 接受各种 Unicode 数字(全角、阿拉伯-印度),但不接受全角小数点、千分位逗号、货币符号和“万”。NFKC 能转全角,逗号与符号还要自己处理。

HTML 取文本与清洗函数

实测 · 本机执行输出
  原始 HTML:'<div class="p"> Price:&nbsp;<b>¥12,345</b><br>half\u200bwidth アイウ\n   next&#8203;line&emsp;ok <span>a</span><span>b</span></div>'
  text_content():' Price:\xa0¥12,345half\u200bwidth アイウ\n   next\u200bline\u2003ok ab'
  "".join(itertext()):' Price:\xa0¥12,345half\u200bwidth アイウ\n   next\u200bline\u2003ok ab'
  xpath string():' Price:\xa0¥12,345half\u200bwidth アイウ\n   next\u200bline\u2003ok ab'
  " ".join(itertext()):' Price:\xa0 ¥12,345 half\u200bwidth アイウ\n   next\u200bline\u2003ok  a b'
  clean(text_content()):'Price: ¥12,345halfwidth アイウ nextline ok ab'
  clean(" ".join(itertext())):'Price: ¥12,345 halfwidth アイウ nextline ok a b'
  从清洗后的文本里取价格:12,345 → 12345
  不清洗直接用同一个正则:匹配到 '12'——Python 的 \d 匹配全角数字,但全角逗号 "," 不在 [\d,] 里,价格被截断;int('12') = 12,静默得到了错误的 12 而不是 12345

text_content()、itertext()、string() 都保留不间断空格、零宽字符和全角字符,<br> 不产生分隔。清洗函数的步骤:NFKC(只针对要做的字段)、去掉零宽/软连字符/方向标记、把各种空白折叠成一个空格。不清洗时用 \d 的正则会把全角价格截成 12——这种错误没有任何异常。

分步演示

文本清洗
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。Python 3.14.8(unicodedata 的 Unicode 版本 16.0.0)标准库 re、unicodedata;lxml 6.1.1 的 text_content、itertext、xpath string();所有结果都是当场运行的字符串操作,不发网络请求;阿拉伯-印度数字等只试了几个样例
    • 没有验证:其它语言或脚本里的特殊空白与组合字符(泰语、阿拉伯语的连字);NFKC 对日文、韩文、中文兼容字符的全部影响(只试了常见样例);emoji 的变体选择符 U+FE0F 与肤色修饰符;双向文本的控制字符;对大文本做规范化的性能;真实站点里这些字符出现的频率
    京ICP备2026067502号-1京公网安备11010502063487号