新闻详情

新闻详情

首页 / 资讯中心 / 详情

Python爬虫文本清洗全攻略:从去空格到金额日期单位标准化

发布时间:2026/10/1 18:54:15来源:尧图网络
Python爬虫文本清洗全攻略:从去空格到金额日期单位标准化
做了几年爬虫相关的数据工作我收到的脏数据加起来大概能绕屏幕好几圈。刚开始写爬虫时我以为把网页抓回来、解析出想要的字段任务就算完成了。结果做数据分析的同事拿着我给的原始文本直摇头同一个价格一个字段是“¥ 1,299.00”另一个是“1299元”上架时间一个写“2023-09-13 08:00:00”一个写“2023年9月13日”销量一个写“已售2.3万件”一个写“23,000件”。这些数据根本没法排序、没法比较、没法聚合。文本清洗就是把这些从网页里抠出来的原始文本统一成结构清晰、格式标准的干净数据。这一节会带你拆解清洗中最常用的五个处理方向去空格、去噪、金额标准化、日期标准化、单位标准化每个部分都配可运行的Python代码零基础也能跟着做。适合正在学Python爬虫、希望自己抓下来的数据真正能用的朋友。1. 为什么说文本清洗是爬虫流程里最容易被低估的环节写爬虫的人通常把时间花在请求、反爬、解析这三块很少有人在写第一版代码时就把文本清洗放进计划。这个现象我太熟悉了当年自己也是这么过来的。直到某个需求要拿一批商品数据做价格区间分布才发现从不同页面解析出来的价格文本五花八门数值根本没法直接比较这才回头补清洗逻辑。1.1 原始爬取文本和你看到的内容隔着好几层你打开浏览器看到的是渲染后的页面爬虫拿到的是HTML源码两者之间差了好几次转换。HTML里除了正文文本还有大量的标签、属性、样式、脚本、注释、隐藏节点甚至还有为了布局而存在的不可见字符。把HTML直接转文本得到的内容往往夹杂着一堆和主题无关的片段。举个例子页面上显示商品标题Apple iPhone 15 (128GB) 蓝色 官方正品 【5G全网通】而对应的HTML可能长这样div classmain-title h1Apple iPhone 15 span classhighlight(128GB)/span 蓝色/h1 p classslogan官方正品nbsp;nbsp;【5G全网通】/p span styledisplay:none隐藏推广文字/span /div如果只是简单提取文本你会得到类似这样的东西Apple iPhone 15 (128GB) 蓝色 官方正品 【5G全网通】 隐藏推广文字注意元素之间的多余空白、全角空格还有那个隐藏的推广文字。这种文本直接入库后面做标题去重、关键词匹配、文本聚类全都会被干扰。1.2 不清洗会带来哪些连锁问题清洗不是形式主义它直接影响后续所有数据操作。举几个我遇到过的连锁问题。价格混乱导致无法聚合。商品比价场景里如果“¥ 1,299.00”和“1299元”同时存在按文本分组会把同一商品拆成两条记录按数值排序也会把货币符号当成文本的一部分参与排序结果完全没法看。日期混乱导致时间线颠倒。一个列表页里既有“2023-09-13”又有“2023年9月13日”还有“3天前”如果不统一成标准日期按时间倒序排列会把这些格式完全打乱同一时间的事件被分到不同的“组”里。单位混乱导致数量失真。重量字段一会出现“3500克”一会出现“3.5kg”一会出现“7斤”做加总时如果只提取数字不加单位换算加出来的总重量可能是错的。销量字段里的“2.3万件”和“23,000件”看起来相似直接解析数字前者可能变成2.3后者变成23000差距一个量级。这些问题的共同点在于页面解析负责“把网页变成文本”而文本清洗负责“把文本变成数据”。两者缺一不可。1.3 清洗在爬虫流程中的位置与输入输出约定爬虫的标准处理链路大致是发送请求、解析HTML、抽取目标字段、文本清洗、结构化入库。文本清洗处于“抽取”和“入库”之间它不关心数据是怎么来的只关心进来的字符串长什么样、出去的字符串是否符合约定。我给清洗层定过几条约定一是所有清洗函数的输入输出都是字符串不掺杂其他类型二是每个函数只做一类事情比如只去空格、只去标签、只转全角三是清洗规则按业务可配置不在函数里写死。这几条约定在项目变大以后非常有用因为不同来源的数据格式不一样一个通用清洗器也照顾不全必须允许按字段定制处理链。从下一节开始我会把最常用的一类清洗——去空格——单独拿出来讲清楚。别看它简单里面藏着的坑一点也不少。2. 去空格别被“看起来干净”骗了全角/零宽空格一个都不能漏去空格几乎是每个爬虫项目里第一个要写的清洗函数因为所有文本都客观存在着空白字符。但它也是最容易被小看的清洗步骤很多人在文本两端调用一次strip()就认为自己处理完了等到做字符串匹配时才发现中间藏着全角空格和零宽空格匹配不上也查不出来。2.1 空白字符类型盘点先明确一个概念Python里的空白字符不是一个而是一族。常见的包括ASCII空格U0020最常见的半角空格全角空格U3000中文排版里的全角空格宽度等于一个汉字制表符\t表格和源码缩进常见换行符\n与回车符\r几乎每个网页源码都有零宽空格U200B等零宽字符编辑器或复制文本时可能产生不间断空格U00A0HTML实体nbsp;转义后的结果BOMUFEFFUTF-8编码的文本开头可能带上。之所以要区分这么细是因为正则里的\s只匹配一部分空白。它匹配空格、制表符、换行、回车、换页、垂直制表但不匹配全角空格、零宽空格、不间断空格。换句话说直接用\s处理中文网页文本会漏掉不少真正的“脏空格”。2.2 压缩多余空白的标准写法去空格的第一个目标是“把连续的多余空白压缩成单个普通空格”第二个目标是“去掉字符串两端的空白”。import re text 2023年12月18日 # 1. 去掉两端空白 text text.strip() # 2. 压缩内部连续空白 text re.sub(r\s, , text)对于大多数ASCII场景这两行已经够用。但处理中文网页时建议把全角空格也纳入压缩范围text re.sub(r[\s\u3000], , text)[\s\u3000]是一个字符类表示“匹配任意空白字符或全角空格”后面的表示匹配一个或多个替换为单个普通空格。这样“全角空格普通空格换行”这样的组合也会被压缩成一个空格。零宽字符和BOM不合并到压缩里直接删除比较好因为它们没有宽度压缩成空格反而引入额外的分隔def remove_invisible_chars(text): return re.sub(r[\u200b\u200c\u200d\u2060\ufeff], , text)2.3 几个隐蔽空格的处理经验先压缩还是先去两端我的建议是先压缩后strip。原因是如果先strip去掉两端后再压缩中间被压缩后的文本可能因为开头或结尾的空格残留而需要二次strip。先压缩再strip顺序最简单结果也稳定。处理时保留换行还是去掉换行要看业务。纯文本正文比如商品标题、资讯标题通常要去掉换行把段落压成一行但地址、日志、代码块这类字段换行本身就是内容结构不能全压。这时可以分开处理# 保留换行只压缩空格制表符 cleaned re.sub(r[ \t], , text) # 压缩连续空行 cleaned re.sub(r\n{3,}, \n\n, cleaned)我自己踩过的一个小坑是从网页源码里解析文本时中间夹了nbsp;实体提取后变成不间断空格U00A0。它既不属于\s也不属于\u3000再怎么压缩也压不掉字符串匹配一致失败。后来排查时打印了每个字符的ord值才发现。这个问题的处理我已经放到上面的字符类里了\u00a0也可以加进去text re.sub(r[\s\u3000\u00a0], , text)这里顺便推荐一个排查手法遇到字符串匹配不上不要盯着正则改来改去先打印可疑位置的字符编码for i, ch in enumerate(text): if ch.isspace() or ord(ch) 127: print(i, repr(ch), hex(ord(ch)))这个方法我后来用了很多次几乎每次都能快速定位到那些“看不见的字符”。3. 去噪先清除页面结构再谈内容提取空格处理完之后下一个大噪声源是HTML本身。这里要分清两个层次标签是一种噪声标签里的文本也可能夹带噪声。去噪的标准做法是先用HTML解析器把页面结构清理掉再用正则处理剩余文本层的杂质。3.1 用BeautifulSoup抽取正文文本解析HTML不要用正则硬啃直接用BeautifulSoup。正则匹配标签的问题在于标签属性里可能包含比如a titlea b简单正则会把整段错误截断而且HTML的嵌套结构很难用正则表达清楚。from bs4 import BeautifulSoup html div classmain-title h1Apple iPhone 15 span classhighlight(128GB)/span 蓝色/h1 p classslogan官方正品 【5G全网通】/p scriptalert(fake);/script /div soup BeautifulSoup(html, html.parser) for tag in soup([script, style, noscript, iframe]): tag.decompose() text soup.get_text(separator , stripTrue) print(text)输出Apple iPhone 15 (128GB) 蓝色 官方正品 【5G全网通】这里有三个关键操作一是decompose()把script、style这类不会在页面上显示的节点直接移除避免它们混进正文二是separator 让块级元素之间有空格分隔防止“标题”和“副标题”粘在一起三是stripTrue去除每个文本段的首尾空白。如果网页结构复杂解析器可以换成lxml速度快不少soup BeautifulSoup(html, lxml)lxml是第三方库需要pip安装但在处理大批量页面时性能差距明显。日常学习用html.parser就够了。3.2 用正则补刀URL、邮箱与控制字符HTML标签清掉以后文本里还可能残留URL、邮箱、特殊控制字符。这些用正则处理最直接# 去掉URL注意排除中文句子结束标点 text re.sub(rhttps?://[^\s,。;], , text) # 去掉邮箱 text re.sub(r\S\S\.\S, , text) # 去掉控制字符ASCII 0-31、127-159 text re.sub(r[\u0000-\u001f\u007f-\u009f], , text)有一类经验容易被忽略去URL时如果使用https?://\S它会一直匹配到下一个空白为止而中文标点如逗号、句号并不算空白所以URL后面的“了解详情”里的逗号也可能被吞掉。用[^\s,。;]可以把常见中英文标点排除在URL提取范围外。实际效果是# 输入详情见 https://example.com/a欢迎咨询 # 输出详情见 欢迎咨询比逗号被吞掉要友好得多。另外有一个顺序上的建议先用BeautifulSoup处理DOM结构再用正则处理纯文本。反过来容易出问题因为正则可能误删HTML属性里包含的重要文本。3.3 噪声判断标准与全角转半角什么算噪声什么不算我给一个实用的判断标准与业务内容无关、且会干扰后续计算的字符都可以视为噪声与内容本身相关的标点不要盲目清除。很多时候全角字符也是一种噪声。比如全角字母数字“”混合在中文文本里做分词或匹配时经常对不上。全角转半角的实现如下def full_to_half(text): result [] for ch in text: code ord(ch) if 0xFF01 code 0xFF5E: # 全角字母/数字/符号对应半角范围是33~126 result.append(chr(code - 0xFEE0)) elif code 0x3000: result.append( ) else: result.append(ch) return .join(result)原理就是全角字符的Unicode码点落在0xFF01到0xFF5E之间减去0xFEE0就得到对应的半角字符。全角空格U3000单独处理成普通空格。这个函数我几乎每个爬虫项目都会用到尤其是抓取港澳台以及一些老系统的页面时效果立竿见影。去噪这一层做完文本基本就成型了。但“成型”不等于“可用”因为像价格、日期、单位这类字段还有额外的语义需要标准化接下来分三节逐个处理。4. 金额标准化让“¥ 1,299.00”和“1299元”变成同一个数字价格数据是爬虫场景里最常见的结构化字段也是最容易出问题的一个。很多人直接float(re.sub(r[^\d.], , text))一把梭结果把“1299元”和“$1299”当成同一个数把“2.3万”里的“万”丢掉把欧洲格式的“1.299,00”解析成1.299。金额标准化的目标是让不同站点、不同写法的价格变成同一套结构一个币种加一个数字。4.1 电商价格常见格式盘点先看看真实项目里会碰到的格式原始文本含义需要处理的内容¥ 1,299.00人民币货币符号、千分位、空格1299.00人民币注意¥和是两个Unicode字符1299元人民币中文单位RMB 1,299人民币英文缩写$1,299.00美元字符$自己就是正则元字符1.299,00欧元区格式千分位用点、小数用逗号2.3万计数单位中文数量单位不是货币单位我把“2.3万”也列进来因为电商场景里销量和价格经常一起出现而且“万”这个数量单位的处理逻辑和金额标准化殊途同归。理解这些格式后标准化的步骤就清晰了识别币种、提取数字、规范化精度。4.2 核心实现币种识别与数字提取币种识别用一张映射表加顺序判断即可CURRENCY_ALIASES [ (CNY, [人民币, rmb, , ¥, 元]), (USD, [美元, usd, $]), (EUR, [欧元, eur, €]), (GBP, [英镑, gbp, £]), ] def detect_currency(text): low text.lower() for code, aliases in CURRENCY_ALIASES: for alias in aliases: if alias.lower() in low: return code return CNY顺序上要把“美元”“欧元”这类更明确的词放在“元”这种通用词前面否则一段“美元1299元”的文本会被先命中“元”而误判成人民币。数字提取分两步走def parse_amount(text): # 优先处理欧洲格式1.299,00 - 1299.00 m re.search(r(\d{1,3}(?:\.\d{3}))(?:,(\d{2})), text) if m: integer_part m.group(1).replace(., ) return f{integer_part}.{m.group(2)} # 普通格式去掉千分位逗号后提取数字 m re.search(r(\d[\d,]*(?:\.\d)?), text) if m: return m.group(1).replace(,, ) return None普通格式的正则r(\d[\d,]*(?:\.\d)?)可以提取“1,299.00”“1299”“1299.5”这类常见写法。\d[\d,]*保证第一个字符是数字后续允许千分位逗号和更多数字(?:\.\d)?处理可选的小数部分。欧洲格式的判断放在普通格式之前是因为1.299,00如果不特殊处理普通正则会把“1.299”当作小数来抽得到完全错误的结果。先检测“三位分组点两位小数逗号”的模式能更准确地区分。把两个函数合并成标准化的入口from decimal import Decimal def normalize_amount(text): if not text: return None text re.sub(r\s, , text) currency detect_currency(text) amount_str parse_amount(text) if amount_str is None: return None amount Decimal(amount_str).quantize(Decimal(0.01)) return {currency: currency, amount: amount}输出统一为{currency: CNY, amount: Decimal(5999.00)}调用方拿到这个结构就能放心做价格比较、排序、聚合。4.3 金额精度Decimal 与整型存储写金额处理时第一原则是不要用float直接存、直接比较。float在二进制里无法精确表示0.1、0.2这类十进制小数比如0.1 0.2的结果可能变成0.30000000000000004这在金额场景里是不被允许的。推荐用Decimal统一精度amount Decimal(5999.00).quantize(Decimal(0.01))如果项目数据库对小数支持不太方便也可以把金额转成最小单位整型存储例如人民币转成分amount_cents int(Decimal(5999.00) * 100) # 599900展示时再除以100。这种方式在比价、排序、去重场景里最稳妥也避免了浮点比较的各种坑。我在建表时习惯把价格字段定义为INT注释标明单位是分后面所有SQL都不用担心精度问题。对精度还有一个细节normalize_amount里用了quantize来保留两位小数如果原始文本只有一位小数如“1299.5”会补成“1299.50”如果原始是整数也会补成“1299.00”。这样所有金额在结构上完全一致便于后续处理。5. 日期标准化把“3天前”和“2023年12月18日”统一时间线日期字段的混乱程度不比金额小。同一个发布时间有的站点给“2023-12-18 14:30:25”有的给“2023/12/18”有的给“2023年12月18日”还有的给“3天前”。日期标准化的目标是把这些变体统一成标准datetime对象对外输出ISO 8601格式字符串方便入库和排序。5.1 匹配常见中文日期的做法写一个能覆盖主流格式的解析函数from datetime import datetime def parse_date(text): text text.strip() # 2023-12-18 / 2023/12/18 / 2023.12.18 / 2023年12月18日 m re.match(r(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?, text) if m: year, month, day int(m.group(1)), int(m.group(2)), int(m.group(3)) # 提取时间部分 tm re.search(r(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?, text) hour, minute, second 0, 0, 0 if tm: hour int(tm.group(1)) minute int(tm.group(2)) second int(tm.group(3) or 0) return datetime(year, month, day, hour, minute, second) # 20231218 无分隔符格式 m re.match(r(\d{4})(\d{2})(\d{2}), text) if m: return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3))) # 12月18日 这种缺少年份的格式 m re.match(r(\d{1,2})月(\d{1,2})日, text) if m: now datetime.now() return datetime(now.year, int(m.group(1)), int(m.group(2))) return None[-/.年]这个字符类是我比较喜欢的写法它把“2023-12-18”“2023/12/18”“2023.12.18”“2023年12月18日”这几种常规分隔符统一匹配了。对零基础读者来说理解这一点就够了正则里的方括号表示“其中任意一个字符都可以”。缺少年份的日期“12月18日”处理时要小心如果现在是一月那么“12月18日”大概率指去年如果是十二月则大概率是今年。我的做法是设计函数时预留一个基准时间参数由调用方决定“现在”是哪一天而不是硬编码datetime.now()。这样可以针对列表页、详情页等不同场景传入不同的基准避免统一补当前年份导致错误。5.2 相对时间换算的边界情况“3分钟前”“2小时前”“5天前”“昨天18:30”这类相对时间常见于资讯、评论、论坛类网站。它们的本质都是“相对当前时刻的时间偏移”需要先换算成绝对时间。from datetime import datetime, timedelta def parse_relative_time(text, nowNone): if now is None: now datetime.now() text text.strip() if 刚刚 in text: return now m re.match(r(\d)\s*(秒|分钟|小时|天|周|个月|月)前, text) if m: num int(m.group(1)) unit m.group(2) if unit 秒: return now - timedelta(secondsnum) if unit 分钟: return now - timedelta(minutesnum) if unit 小时: return now - timedelta(hoursnum) if unit 天: return now - timedelta(daysnum) if unit 周: return now - timedelta(weeksnum) if unit in (个月, 月): # 简化处理按30天估算 # 生产环境建议用 dateutil.relativedelta 精确处理 return now - timedelta(days30 * num) return None这里有两个边界要特别提醒。第一个是“昨天”“前天”不要用now()直接减去24小时。更稳的写法是以当天零点为基准做减法def parse_recent_day(text, nowNone): if now is None: now datetime.now() today_midnight now.replace(hour0, minute0, second0, microsecond0) if text.startswith(昨天): target today_midnight - timedelta(days1) elif text.startswith(前天): target today_midnight - timedelta(days2) else: return None tm re.search(r(\d{1,2}):(\d{1,2}), text) if tm: target target.replace(hourint(tm.group(1)), minuteint(tm.group(2))) return target为什么不用now - timedelta(days1)因为如果现在是凌晨0点30分“昨天18:30”在绝对时间上其实是过去6小时但如果直接减24小时会得到前天18:30差了整整一天。以当天零点为基准才能保证“昨天”始终是自然日概念。第二个是“N个月前”不能简单用timedelta因为月份天数不固定。平时写代码能覆盖“秒/分钟/小时/天/周”这些常见单位就够了确实遇到“月”的情况最省事的办法是引入python-dateutil库用relativedelta精确处理from dateutil.relativedelta import relativedelta # now - 3个月精确处理月末、闰年 target now - relativedelta(months3)5.3 时区与输出格式建议很多海外网站的服务器返回的都是UTC时间抓下来之后如果直接当成本地时间入库后续排序会差好几个小时。建议的做法是统一存储为UTC时间戳展示时再转本地时区。Python里用datetime.astimezone()配合zoneinfo可以很干净地转换from datetime import datetime, timezone from zoneinfo import ZoneInfo # 假设抓到一个UTC时间 utc_time datetime(2023, 9, 13, 8, 0, 0, tzinfotimezone.utc) # 转成北京时间展示 beijing_time utc_time.astimezone(ZoneInfo(Asia/Shanghai))如果项目早期习惯用无时区的naive datetime建议至少在字段名上注明“默认UTC”否则不同来源的数据混在一起时区问题会非常难排查。输出格式方面我推荐统一用ISO 8601字符串入库例如2023-09-13T08:00:00。它不带歧义、支持字符串排序、在各类数据库和前端里都有现成解析方案。把datetime对象转成ISO字符串很简单dt.isoformat() # 2023-09-13T08:00:006. 单位标准化重量、长度、销量数字的归一化处理单位标准化在最开始常常被忽略但它是很多数据需求里绕不过去的一关。抓商品数据会碰到“克”“斤”“公斤”“磅”抓物流信息会碰到“cm”“英寸”“英尺”抓销量会碰到“2.3万”“1.2k”。单位不统一轻则显示混乱重则计算错误。6.1 单位映射表把一切换到基准单位归一化的核心思想很简单给每个单位定义一个到基准单位的换算系数解析时提取数字和单位相乘得到基准值。重量统一换算成克WEIGHT_TO_GRAM { 克: 1.0, g: 1.0, 千克: 1000.0, 公斤: 1000.0, kg: 1000.0, 斤: 500.0, # 市斤1斤 500克 两: 50.0, 磅: 453.59237, lb: 453.59237, lbs: 453.59237, 吨: 1000000.0, t: 1000000.0, }长度统一换算成米LENGTH_TO_METER { 毫米: 0.001, mm: 0.001, 厘米: 0.01, cm: 0.01, 米: 1.0, m: 1.0, 千米: 1000.0, 公里: 1000.0, km: 1000.0, 英寸: 0.0254, in: 0.0254, inch: 0.0254, 英尺: 0.3048, ft: 0.3048, feet: 0.3048, }这里有一个细节市斤的换算系数是500克但港澳地区的“斤”有时指600克司马斤。如果你的数据来自港澳台或跨境电商页面要确认当地语境。磅的换算值453.59237是高精度数值日常计算保留到小数两位就够。6.2 归一化函数实现与大小写问题有了映射表归一化函数就很简单def normalize_weight(text): if not text: return None text text.strip().lower() m re.search(r([\d.])\s*(克|千克|公斤|斤|两|磅|吨|g|kg|lb|lbs|t), text) if not m: return None try: value float(m.group(1)) except ValueError: return None unit m.group(2) factor WEIGHT_TO_GRAM.get(unit) if factor is None: return None return round(value * factor, 2)注意最前面做了text.lower()同时映射表的key全部是小写这样“KG”“Kg”“kg”都会被统一匹配成kg。这是处理英文单位缩写时最容易踩的坑不转大小写同一个单位可能漏掉大半。数字和单位之间的空格也要考虑。正则里用了\s*所以“3.5 kg”和“3.5kg”都能匹配。如果文本里是全角空格先把全角空格替换成普通空格再做匹配否则这里也会漏。float()转换失败的情况必须处理。爬虫文本里可能出现“3.5.6kg”这种脏数据float()会抛ValueError导致整个清洗流程中断。加一层try/except返回None即可。长度标准化的代码结构完全一样只是换了映射表和单位列表def normalize_length(text): if not text: return None text text.strip().lower() m re.search(r([\d.])\s*(毫米|厘米|米|千米|英寸|英尺|mm|cm|m|km|in|inch|ft), text) if not m: return None try: value float(m.group(1)) except ValueError: return None factor LENGTH_TO_METER.get(m.group(2)) if factor is None: return None return round(value * factor, 3)这种“正则映射表基准单位”的模式可以套用到面积、容量、速度等任何单位场景只要有明确的换算关系。6.3 计数单位与多级单位的匹配顺序电商销量常出现“已售2.3万件”“1.2k人付款”这类文本数字后面跟着中文或英文数量单位。处理逻辑是一样的COUNT_UNIT { 百: 100, 千: 1000, k: 1000, 万: 10000, 百万: 1000000, 亿: 100000000, } def normalize_count(text): if not text: return None text text.strip().lower() m re.search(r([\d.])\s*(百万|万|千|百|亿|k)?, text) if not m: return None value float(m.group(1)) unit m.group(2) or factor COUNT_UNIT.get(unit, 1) return int(round(value * factor))正则里“百万|万|千|百|亿|k”这个顺序不是随便排的遵循“长单位优先”原则如果你把“万”排在“百万”前面遇到“2百万”时正则会在“2百”处停下把“万”留到后面解析结果变成2乘以100——这显然是错的。把“百万”放前正则优先匹配它再匹配“万”逻辑就对了。另一个常见需求是把单位字符串直接替换进文本而不是只提取数值。可以用re.sub的回调实现def replace_weight_in_text(match): value float(match.group(1)) unit match.group(2) grams value * WEIGHT_TO_GRAM.get(unit, 1) return f{grams:.0f}克 text re.sub(r([\d.])\s*(斤|公斤|千克|克|两|磅), replace_weight_in_text, 5斤苹果) print(text) # 2500克苹果回调函数在每次匹配时被调用返回的字符串直接替代原匹配部分。这样做的好处是在保留原文其他字符的同时只把需要标准化的片段换掉。7. 一套可复用的清洗流程样板含完整代码到这里每个标准化函数都单独讲完了。最后把它们组装成一套可以在真实项目里直接跑的清洗流程顺便分享一个我在项目里用到的设计和一次踩坑记录。7.1 从电商商品页出发的完整清洗链假设有一个商品详情页抓到的原始HTML和字段文本如下raw_data { title_html: div classmain-title h1Apple iPhone 15 span classhighlight(128GB)/span 蓝色/h1 p classslogan官方正品 【5G全网通】/p span styledisplay:none隐藏推广文字/span /div , price_text: ¥ 5,999.00, sales_text: 已售2.3万件, date_text: 2023年9月13日 08:00:00, }把前面的函数串起来写一个clean_product_datadef clean_product_data(raw): # 标题先清HTML结构再做文本级清洗 title clean_html(raw[title_html]) title clean_text(title) # 价格标准化成 币种Decimal price normalize_amount(raw[price_text]) # 销量统一成整数 sales normalize_count(raw[sales_text]) # 日期优先解析绝对时间再尝试相对时间 release parse_date(raw[date_text]) if release is None: release parse_relative_time(raw[date_text]) return { title: title, price: price, sales: sales, release_date: release.isoformat() if release else None, }其中clean_html和clean_text可以直接使用第2、3节的实现def clean_html(html): soup BeautifulSoup(html, html.parser) for tag in soup([script, style, noscript, iframe]): tag.decompose() return soup.get_text(separator , stripTrue) def clean_text(text): text re.sub(r[\u200b\u200c\u200d\u2060\ufeff], , text) text re.sub(r[\s\u3000\u00a0], , text) text re.sub(rhttps?://[^\s,。;], , text) text re.sub(r\S\S\.\S, , text) text full_to_half(text) return text.strip()调用result clean_product_data(raw_data) print(result)输出{ title: Apple iPhone 15 (128GB) 蓝色 官方正品 【5G全网通】, price: {currency: CNY, amount: Decimal(5999.00)}, sales: 23000, release_date: 2023-09-13T08:00:00, }这样处理完的数据入库后可以放心地用SQL排序、比较、分组。7.2 清洗管道的设计思路清洗函数一多最怕的是代码越写越散最后变成一团乱麻。我维护一个爬虫项目两年后把清洗逻辑重构成了“管道”模式基本思路是PIPELINE [ remove_invisible_chars, collapse_spaces, remove_urls_and_emails, full_to_half, strip_text, ] def run_pipeline(text): for step in PIPELINE: text step(text) return text每个步骤都是一个“字符串进、字符串出”的纯函数用列表管理顺序。加规则时只需在新写一个函数后把它插到PIPELINE的合适位置不用改动主流程。这个模式对单测特别友好想验证哪个步骤出问题只需逐个调用并打印中间结果很快就能锁定位。另外要注意的是清洗规则应该分两层通用清洗器负责所有文本都需要的空白压缩、隐字符清除、HTML清理业务清洗器再针对金额、日期、单位这类有明确语义的字段单独处理。不要把所有规则塞进一个函数那样既难维护又容易在不同项目中互相干扰。7.3 一次典型的踩坑记录隐形空格与字符排查最后分享一次真实项目中印象很深的踩坑经历。某个比价项目里价格字段从页面解析出来长这样¥5,999.00看起来格式完全统一但normalize_amount始终提取不到数字。排查了很久最后打印字符编码才发现¥和5之间夹了一个\u00a0也就是不间断空格。它从HTML里的nbsp;实体转换而来视觉上只占一个普通空格的位置但既不属于\s也不属于全角空格\u3000所以之前的空白压缩正则根本没有处理它。修复也很简单把\u00a0加进字符类text re.sub(r[\s\u3000\u00a0], , text)从那之后我养成了一个习惯遇到字符串匹配不上先别改正则先排查不可见字符。代码只需要几行for i, ch in enumerate(text): if ch.isspace() or ord(ch) 127: print(i, repr(ch), hex(ord(ch)))输出里能直接看到U00A0、U3000、U200B这些异常字符位置也标得清清楚楚。这个方法在文本清洗的调试里帮了我无数次建议你也试试。文本清洗这件事单独看每个函数都很简单难的是把各种情况组合起来还要保持代码清晰。我的体会是规则宁可多写但一定要分类清晰、顺序可控每加一个新规则前先跑一遍历史样例防止新规则破坏了已经稳定的字段。这样积累下来清洗层会慢慢变成项目里最稳定的一部分。下一节可以在此基础上继续做结构化字段的校验与校验失败时的兜底策略到时候再看这些清洗函数如何与入库逻辑配合。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Spring Boot + MyBatis + PostgreSQL 实战指南:从选型到上线避坑 2026/10/1 19:43:50

Spring Boot + MyBatis + PostgreSQL 实战指南:从选型到上线避坑

在 Java 后端这一摊子里,Spring Boot 早就成了事实上的标配,而说到持久层框架,MyBatis 和 JPA 两派之争从来没停过。我这两年做过的项目中,凡是要精细化控制 SQL、要针对复杂查询做深度优化的,最后几乎都落在了 Spring…

阅读更多 →
解决npm安装报错:淘宝镜像证书过期与npm源切换指南 2026/10/1 19:43:43

解决npm安装报错:淘宝镜像证书过期与npm源切换指南

下午三点,新同事抱着电脑来找我,说是Node.js装不上,终端里躺着一行红色报错。我看了一眼,发现是那句眼熟的npm error request to https://registry.npm.taobao.org/cnpm failed, reason: certificate has expired。这个报错我在过…

阅读更多 →
单片机开发必备软件清单:从Keil、烧录到仿真调试的完整工具链 2026/10/1 19:43:37

单片机开发必备软件清单:从Keil、烧录到仿真调试的完整工具链

搞单片机这么多年,陆陆续续被新手问得最多的问题之一就是:单片机开发到底要用哪些软件?每次看到有人拿着一堆烧录器、开发板、教程在群里发愁,我就知道,大部分人不是不会写代码,而是被工具链的选型劝退了。…

阅读更多 →
Madeira:ARM64 Linux上原生级Windows兼容执行层 2026/10/1 19:43:37

Madeira:ARM64 Linux上原生级Windows兼容执行层

1. “Madeira”不是地名,而是FEX-Emu生态中一个关键的Windows兼容层代号 你搜“Madeira”,第一反应可能是葡萄牙那个阳光海岛——但最近在Linux/ARM64开发者圈子里,这个词正悄悄取代“Wine”成为高频技术热词。它不指向地理坐标,而…

阅读更多 →
VMware虚拟机桥接网络配置详解:从原理到实战排障 2026/10/1 19:43:30

VMware虚拟机桥接网络配置详解:从原理到实战排障

VMware虚拟机配桥接网络这事,说难不难,但确实有不少人卡在“明明照着教程一步步来,虚拟机就是上不了网”这一步上。我自己前前后后帮同事和朋友解决过不少次这个问题,总结下来,绝大多数情况不是操作错,而是…

阅读更多 →
2026 Redis高频面试题全解析:从底层编码到缓存一致性 2026/10/1 19:43:30

2026 Redis高频面试题全解析:从底层编码到缓存一致性

1. 2026 面试风向:Redis 高频面试题为什么仍然绕不开这两年每次我复盘候选人面试记录,Redis 相关的问题出现频率基本都在中间件里的前两名。哪怕候选人简历里写的是 Java、Go、Python,只要提到缓存、分布式、高并发,Redis 就一定会…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉