数据清洗实战:用Python从脏数字串中提取有效信息并构建可复用清理脚本
接到一批数据里面全是一串一串的数字比如“123567890”有的中间夹着空格有的混着字母有的长度忽长忽短。干过数据处理的人对这类活儿都不陌生这就是最典型的脏数据清洗场景。今天拿这个数字串当引子聊一聊从一串“看起来毫无意义”的数字序列里怎么把有效信息捞出来、怎么判断它是不是缺了位、怎么做成一套能复用的清洗脚本。适合刚接触数据清洗的读者也适合那些每天跟订单号、流水号、会员ID打交道却总被格式问题折磨的后端和数据同事。1. 接到一串“意义不明”的数字先别急着写正则1.1 先看数据长什么样样例和第一印象很多时候我们拿到手的数据并不是干干净净的表格而是一堆像“123567890”这样光秃秃的数字。我第一次处理这类数据时第一反应是直接复制到文本编辑器里肉眼扫一遍。这一步看起来原始却特别管用能看到前后的空格、中间的横杠、被误输入成字母的“0”和“1”、以及一些莫名缺失的位置。举个例子细看“123567890”这串数你会发现它少了“4”从1到0中间本该包含0到9十位但这里只有9个数字。这种跳号可能是有意的业务规则也可能是数据在录入、传输过程中丢了字符。光靠直接读取是发现不了这些问题的。所以我会把“观察”当成数据清洗的第一步不写任何代码先把数据打开看一眼。观察内容主要是字段类型是不是统一、长度分布是集中在某几个值附近还是一团乱、有没有明显混入中文或英文字母、有没有大量空值等。这些信息决定了后续清洗脚本的复杂度。你要是跳过这一步直接拿着正则去匹配大概率会在写完脚本之后发现一堆边界情况没覆盖又得回来补逻辑反而更慢。1.2 它到底可能是哪类数据五种常见的数字串来源要清洗一串数字先得猜它原本是什么。以“123567890”这类字符串为例我基本会往五个方向去猜。第一种是最常见的主键类编号比如订单号、流水号、会员ID。这种编号通常定长有校验位比如最后一位可能是通过前几位计算出来的。判断方法很简单把所有数据的长度做分布统计如果大多数长度一致那就是定长编号这时缺失位和长度异常就非常可疑。第二种是日期类或时间戳类数据比如“123567890”可以被拆成2023-05-67-89之类当然这个例子拆出来不合理但如果数字串总是11位或14位前面是年月日后面是时分秒那很可能是时间戳拼出来的。第三种是多字段拼接而成的复合ID比如“区域码 业务日期 顺序号”。这类串一旦有缺失位后续拆分字段就会全部错位处理时得先确认分隔规则。第四种是脱敏后的手机号、证件号、银行卡号。这些数据在导出时经常被做掩码处理用数字串传输时就会损失一定信息比如“123567890”可能原本是“1234*567890”星号位置被移除后就变成了缺位状态。第五种是纯随机或测试用的占位数据比如压测时生成的假ID这种没什么业务价值一般识别出来后直接过滤掉就行不值得花时间解析。判断数据来源不靠猜靠“证据”。我会先把数字串按长度分组再看每一组的字符构成、看是否有固定前缀或后缀必要时抽样去业务系统里对一对。这个判断环节如果省略后面所有清洗规则都可能建立在错误的假设上。2. 清洗该做哪几步需求拆解和方案选型2.1 核心目标拆解去噪、校验、还原、标记面对一批数字串清洗的最终目标并不是“把字符串里的非数字删掉”这么简单。我通常会把目标拆成四件事分别写逻辑。第一件是去噪。把肉眼可见的杂质去掉包括空格、换行、制表符、横杠、括号、中文数字、全角字符等。这个阶段只做减法不做任何猜测。第二件是校验。清洗后得到一个纯数字串接下来要判断它是否符合业务规则。基本校验规则包括长度是否在指定范围内、是否包含非数字字符、是否重复、是否在业务字典里存在。这个阶段能筛出不少问题数据。第三件是还原。有些数据可以靠内部规则推断补全比如已知某系列编号是十位但当前只有九位且缺少位可以由校验算法算出来那就可以尝试补位。但还原必须特别小心我的原则是只补有明确规则依据的位规则不明确的宁可标记异常也不硬猜。第四件是标记。清洗完的数据要有一个状态字段比如“正常”“长度异常”“字符异常”“疑似缺位”“无法解析”。在这个阶段不能直接把疑似异常的记录删掉而是保留原值、清洗后的值、状态标记方便后续人工复核。用“123567890”举例去噪后还是“123567890”校验长度发现比常见手机号11位短且少于十位定长编号的常见长度用数字集合比对发现缺了“4”于是标记为“疑似缺位”同时保留原样这样处理就不丢信息。整个流程里最核心的思路是“先核对规则再动数据”一切不可逆的操作都要往后放。2.2 为什么用Python而不是SQL或Excel数字串清洗这个需求理论上用Excel的公式和SQL的字符串函数也能做但我几乎不会用它们原因很简单可维护性和复杂逻辑的承载能力差太多。Excel的优点是人眼直观适合一次性几百条数据的快速排查但一旦数据到几万条下拉公式和筛选就会变得卡顿而且公式逻辑很难被团队其他成员复查。更麻烦的是如果清洗规则变动Excel里要一列一列地改出错的概率很高。SQL的字符串函数擅长把某个字段标准化比如用REGEXP_REPLACE处理Hive或MySQL里的字段非常快但SQL对过程式的“先校验、再补位、再标记”这套流程支持很弱。你想做一个逐步执行的清洗管线SQL写起来会非常别扭需要拆成一堆临时表可读性也差。Python最适合这种场景的核心原因有两个。一是字符串处理库和正则表达式写起来自然几步就能拼出完整的数据清洗流程二是pandas等库可以处理几十万行数据并且把每一步输出成新列方便追踪数据在每一步的变化。数据清洗本来就是个“试错—修正—再试”的过程Python的交互式环境正好能让人快速迭代。再加上最后导出CSV或写入数据库都很方便所以它是我处理这类问题的首选。3. 手写一版可落地的数字串清洗脚本3.1 准备数据和环境先明确一下后面要演示的这套脚本处理的是类似“123567890”的字符串列表输入可能是Excel导出的列也可能是日志里抓出来的列表。为了演示方便我构造一批有代表性的样例数据包括纯净数字串、带空格的、混入字母的、带横杠的和空值覆盖绝大多数常见噪音类型。环境方面只需要Python 3.8以上版本安装pandas即可。如果没有pandas只用标准库也能实现但后面导出统计报表时还是pandas方便。我先导入需要的库import re import pandas as pd # 样例数据模拟从业务表里导出的原始列 raw_list [ 123567890, 123 567 890, 12A35B67890, 1235-6789-0, 123356678890, , 9876543210, 1234567890, ]这段样例里第一条“123567890”是缺位数字串第二条带空格第三条混入字母A和B第四条带横杠第五条有重复数字可能代表业务上有重复第六条是空值第七条和第八条是长度为10的常见编号。构造这样一组数据的目的是让脚本能同时验证多种逻辑。3.2 第一步把数字从噪声里捞出来清洗数字串的第一步是把字符串里所有非数字字符去掉。这里容易踩坑的是直接用int()或者isdigit()去判断后者只判断“是不是全是数字”一旦字符串里混入空格就会返回False不适合做清洗。正确做法是用正则替换掉非数字部分def extract_digits(raw): if pd.isna(raw): return # 只保留0-9数字 return re.sub(r\D, , str(raw))这段代码用\D匹配所有非数字字符并删除剩下就是纯数字串。之所以用这种方式而不逐个字符判断是因为正则表达式一次扫描完成替换性能更好、代码更短。特别要注意的是先用pd.isna(raw)判断空值避免把NaN转成字符串“nan”之后被清洗成“”。清洗后逐条看下结果for raw in raw_list: print(repr(raw), -, repr(extract_digits(raw)))输出结果里带空格和横杠的都能变成纯数字“12A35B67890”会变成“123567890”空值变成空字符串。到这里噪音就被清干净了接下来才能做校验。3.3 第二步长度、字符集与缺失数字检测纯数字串拿到手之后接下来要回答三个问题长度对不对、字符集里有没有不该出现的内容、0到9各数字是不是都齐。长度判断依赖业务规则。如果没有业务规则我一般以超过10位的数字串为“候选异常”因为常见的手机号11位、银行卡号16到19位、订单号通常也在15到20位左右而9到10位的数字串反而可能是短编号或日期类数据。这里做成参数valid_lengths后续可以按实际业务传入。字符集判断比较简单清洗后全字符串必须全部是数字如果仍然有字母一定是正则没有覆盖到的特殊字符比如全角数字可以单独标记。缺失数字检测是我处理“123567890”这类数据时的重点。思路是把数字串转成集合和目标集合set(0123456789)做差集差集不为空说明有数字从未出现过。但这里有个容易误判的点某个数字没出现可能是它本身就是短编号比如6位邮编就没有“9”。所以缺失检测只适用于“理论上应包含0到9完整集合”的字符串或者业务上明确说明“编号内必须覆盖某些数字”。def analyze_sequence(digits, valid_lengthsNone): if not digits: return {status: EMPTY, length: 0} length len(digits) missing set(0123456789) - set(digits) has_letters any(ch.isalpha() for ch in digits) flag NORMAL if has_letters: flag HAS_CHAR if valid_lengths and length not in valid_lengths: flag LENGTH_ERR if missing and length 10: # 长度足够但缺数字大概率是移位或丢失 flag MISSING_DIGIT if flag NORMAL else flag MISSING_DIGIT return { status: flag, length: length, missing: .join(sorted(missing)), clean_value: digits, }我把这个函数设计成返回一个字典而不是直接打印是为了方便后续拼成DataFrame。一个容易被忽略的细节是函数里对缺失数字的判断附加了“长度不低于10”的条件。原因很简单如果原始编号本身就是短编号数字集合天然就不全不能误报为缺失。这个条件是我在实际处理中吃亏之后补上的一开始不做限制导致一批6位短编码全被标成了异常白白浪费半天人工排查时间。接着对8条样例数据批量跑一遍并整理成表格rows [] for i, raw in enumerate(raw_list): digits extract_digits(raw) result analyze_sequence(digits, valid_lengths{9, 10, 11}) result[idx] i result[raw] raw rows.append(result) df pd.DataFrame(rows)[[idx, raw, clean_value, length, missing, status]] print(df.to_string(indexFalse))输出结果大致是第一条长度9、缺4状态为MISSING_DIGIT第二条清洗后跟第一条一样也是MISSING_DIGIT第三条混入字母清洗后同样变成缺4的那串第四条清洗后长度为9缺4第五条长度12、有重复数字、缺失空状态是LENGTH_ERR第六条是空第七条第八条长度10状态正常。这样的输出就已经把“哪些数据有问题、问题是什么”讲清楚了。4. 真实场景下的问题排查与避坑清单4.1 常见坑一函数选择不当导致“数字”被丢掉Python里判断字符串是否为数字的方法有好几个str.isdigit()、str.isdecimal()、str.isnumeric()行为并不相同。我最初写清洗逻辑时用了isdigit()配合列表推导结果处理全角数字“”时代码直接报错或返回False因为这些全角字符在文档里肉眼看着是数字但机器认为是不同的字符集合。正确的做法是不要依赖isdigit()做过滤而是用正则\d或者[0-9]同时结合字符编码转换把全角数字转半角。我曾经在处理一批从繁体系统导出的流水号时遇到全角数字混入问题后来在清洗前统一做了全角转半角处理这个坑就消失了。建议在清洗流程最前面加一层全角转半角的逻辑宁可多做一步也不漏掉这类情况。另一个相关坑是空值被当成字符串。pandas里的NaN如果直接用str(raw)转成字符串会得到“nan”三个字母清洗后不仅没变成空反而留下“n”“a”被删后的痕迹。所以第一步必须先判空再进清洗流程。4.2 常见坑二把原始列直接覆盖刚开始做数据清洗时我习惯把清洗结果直接覆盖原字段比如直接执行df[code] df[code].map(extract_digits)。这个操作在脚本跑通的瞬间看起来没问题但一旦后面发现规则写错了连原始数据都没了只能重新导数据。后来我给自己定了一条铁规矩任何一个清洗任务输出表里必须同时保存三列——原始值、清洗后的值、状态标记。这样做有三个好处一是可以随时追溯某条数据的清洗过程二是如果后续调整规则可以用原始值重新算不用重新导数据三是给业务方看结果时对方能直观看到变化不需要反复解释。4.3 常见坑三只考虑单字段没考虑拼接字段数字串不一定只对应一个业务含义有可能是多个字段拼在一起。比如“区域码”加“日期”加“序号”被系统导出时可能直接用字符串拼接丢掉了分隔符。这种情况下如果只看全串的特征很难发现规律比如“123567890”里面前三位可能是区域码“123”中两位是某编码“56”后四位是“7890”。我的排查方法是先按长度、前缀、后缀粗略分组再找样本和业务同事核对。如果确定是多字段拼接就按字段长度切片解析切片顺序也要比对判断不能想当然。比如有一些系统是先序号后日期直接按“日期在前”的假设去切后面的解析就全错。这块的经验是数字串清洗最耗时间的往往不是写脚本而是搞清楚业务字段的意义。脚本只需要半小时字段调研经常要花一两天。4.4 问题速查表我把日常处理数字串时遇到的典型问题和对应解法整理成一张表方便直接对照。现象可能原因处理方式字符串混入空格和横杠导出格式或人工录入导致用正则删除非数字字符全角数字无法识别字符集编码不一致先做全角转半角再提取数字空值变成“nan”直接对NaN调用str先用pd.isna判空再决定是否置空长度普遍偏差可能混入了拼接字段或短编号按长度分组抽样核对业务含义数字集合缺某几位数据传输丢失或脱敏标记为疑似缺失不擅自补位出现重复数字可能是脏数据也可能是业务规则结合业务确认必要时按去重规则过滤这张表之所以值得收藏是因为数字串清洗看似简单实际大部分时间都耗在识别这些“看着像数字、其实有问题”的情况上。5. 从“清洗完”到“可复用”沉淀成一套小工具5.1 封装成函数参数化规则前面几节的代码解决了具体问题但脚本要真正发挥价值得把规则参数化做成一个可以被反复调用的模块。我通常会定义一个DigitCleaner类把有效长度集合、是否需要检测缺失数字、是否要做全角转换这些选项作为构造函数参数传进去。这样不同业务的数字串规范不需要改主体代码改配置就行。一个简单的封装示例class DigitCleaner: def __init__(self, valid_lengthsNone, check_missingFalse): self.valid_lengths valid_lengths self.check_missing check_missing def clean(self, raw): digits self._to_half_width(raw) digits extract_digits(digits) return analyze_sequence(digits, self.valid_lengths, self.check_missing) def _to_half_width(self, raw): # 全角转半角的映射逻辑 ...不要小看这一步我早期每次接到类似需求都重新写一遍脚本直到第三次才下定决心封装。封装之后后续再遇到“一堆奇怪的编号要清理”基本上只需要改一行配置、看一遍输出一小时内能交付效率和之前完全不是一个级别。5.2 处理结果如何输出和留痕脚本跑完结果不能只停在终端打印要输出成文件留痕。我常用的方式是把DataFrame导出为Excel或CSV同时生成一份简短的统计报告总记录数、正常数、异常数、异常类型分布。统计报告的作用是让业务方在打开文件的瞬间就知道这批数据质量怎么样不用自己去翻明细。输出代码大致是df.to_csv(clean_result.csv, indexFalse, encodingutf-8-sig) summary df[status].value_counts() print(summary)之所以用utf-8-sig编码是因为直接保存为utf-8后用Excel打开可能会乱码。这个细节我已经写进自己的模板不会再忘。留痕的价值在于如果过了两周有人来问“当时这批编号是怎么处理的”我能直接把输出文件和代码记录摆出来而不是靠回忆解释。5.3 后续扩展的几种玩法数字串清洗的需求往往不是孤立的顺着这个思路往下走还能做不少扩展。比如把清洗后的结果直接接到下游做编号归一化后再去关联业务表或者把异常记录自动汇总成“待人工复核清单”推给运营同事处理甚至对含有校验位的编号可以在清洗模块里加入校验算法自动识别出“最后一位不对”的数据这类扩展在订单系统和会员系统里非常实用。另外如果数字串的量级到了几百万行还能用pandas的向量化操作替代循环性能会快不少。这个方法的核心价值是通用性数字串只是表层的载体当你把“去噪、校验、还原、标记”这条思路吃透之后类似邮箱清洗、地址清洗、手机号清洗都能套用同一套框架。我个人在实际操作中的体会是像“123567890”这种看似随意的数字序列恰恰是最值得花时间处理的样例。它越乱越能逼你在一开始就把规则想清楚而不是拿着一个简单的replace就去应付。先把观察、规则、校验、留痕这套习惯养成后面再遇到再离谱的数据也只需要照着流程走一遍。