Excel数据清洗,我用Python写了套脚本,同事追着要(附完整代码)
- 2026-09-21 12:40:20
每次拿到业务方发的Excel就想辞职——空格、合并单元格、日期格式五花八门、手机号里还夹着"138-0013-8000"……这篇把套路一次讲清,代码可直接抄。
一、先说说那些让人血压升高的瞬间
做数据的人都懂,最耗时的从来不是建模,是清洗。
一份"看起来很干净"的Excel,打开往往是这样的:
| 姓名 | 手机号 | 入职日期 | 月薪 | 部门 |
|---|---|---|---|---|
| ␣张三 | 13800138000 | 2023/01/05 | 12,000 | 技术部 |
| 李四 | 138-0013-8001 | 2023-02-11 | 15000元 | ␣技术部 |
| 王五␣ | 13800138002 | 2023.03.20 | 1.8万 | 市场部 |
| 张三 | 13800138000 | 2023年4月8日 | 12000 | 技术部 |
| 赵六 | abc | 20230512 | 面议 | 市场部 |
| 钱七 | 13800138006 | 2023/07/01 | 20000 | 人事部 |
姓名前后有空格、手机号格式不统一、日期有5种写法、薪资混着"元""万""面议"、还有重复行和空值。
手动处理这7行,你大概要花15分钟。如果有7万行呢?
下面这套流程,跑完只要3秒。
二、环境准备(30秒)
pip install pandas openpyxlpandas:数据处理核心openpyxl:读写.xlsx文件、顺便做格式化
三、核心思路:先定"标准",再清洗
清洗的本质是:把杂乱的值,映射到统一的标准格式上。
| 字段 | 标准 |
|---|---|
| 姓名 | 去掉所有空白字符 |
| 手机号 | 11位纯数字,1[3-9] 开头 |
| 日期 | datetime 类型 |
| 月薪 | float,单位统一为元 |
| 部门 | 去空格 + 枚举值校验 |
带着这个表写代码,思路会非常清晰。
四、逐项击破(附代码)
Step 0:读取时就把坑堵住
import pandas as pdimport numpy as npimport re# dtype=str:全部按字符串读,防止手机号/身份证变成 1.38E+10df = pd.read_excel("员工信息_原始.xlsx", dtype=str)print(f"原始数据:{df.shape[0]} 行,{df.shape[1]} 列")
坑1:不指定
dtype=str,18位身份证号会被截断成科学计数法,且不可逆。
Step 1:清洗列名
真实场景里列名经常是 "姓名 "、"月薪(元)"、"手机 号"。
df.columns = (df.columns.astype(str).str.strip().str.replace(r"[\s\u3000]+", "", regex=True) # 半角+全角空格.str.replace(r"[()()]", "", regex=True) # 中英文括号)
结果:月薪(元) → 月薪元,姓名 → 姓名。
Step 2:文本列去空格和隐形字符
最阴险的是零宽字符(\u200b),肉眼看不出,但 VLOOKUP 永远匹配不上。
for col in df.select_dtypes(include="object").columns:df[col] = (df[col].astype(str).str.replace(r"[\u200b\u3000\xa0]", "", regex=True) # 零宽/全角/不换行空格.str.strip())# 把"伪空值"统一还原成 NaNdf = df.replace({"nan": np.nan, "NaN": np.nan, "None": np.nan,"": np.nan, "-": np.nan, "无": np.nan, "null": np.nan})
关键点:先统一空值表示,后面的 dropna / fillna 才准。
Step 3:去重
before = len(df)df = df.drop_duplicates()print(f"删除重复行:{before - len(df)} 条")
顺序很重要:一定要先清洗再判重。否则
"张三"和"␣张三"会被当成两个人。
Step 4:手机号规范化
def clean_phone(x):if pd.isna(x):return np.nandigits = re.sub(r"\D", "", str(x)) # 只留数字if digits.startswith("86") and len(digits) == 13:digits = digits[2:] # 去掉 +86 前缀return digits if re.fullmatch(r"1[3-9]\d{9}", digits) else np.nandf["手机号"] = df["手机号"].apply(clean_phone)
138-0013-8001 → 13800138001,abc → NaN(后面统一处理)。
Step 5:日期规范化(重灾区)
2023/01/05、2023-02-11、2023.03.20、2023年4月8日、20230512 —— 五种写法,一个函数搞定。
def clean_date(x):if pd.isna(x):return pd.NaTs = str(x).strip()s = s.replace("年", "-").replace("月", "-").replace("日", "")s = re.sub(r"[./]", "-", s)s = re.sub(r"\s+.*$", "", s) # 去掉 " 09:30:00" 这类时间后缀if re.fullmatch(r"\d{8}", s): # 20230512return pd.to_datetime(s, format="%Y%m%d", errors="coerce")return pd.to_datetime(s, errors="coerce")df["入职日期"] = df["入职日期"].apply(clean_date)df["入职日期"] = df["入职日期"].dt.date # 只保留日期部分
坑2:
pd.to_datetime遇到20230512不会报错,但可能解析成奇怪的结果。8位纯数字必须单独指定 format。
Step 6:金额规范化
def clean_money(x):if pd.isna(x):return np.nans = str(x).strip().replace(",", "").replace(",", "")if "万" in s:num = re.search(r"\d+(\.\d+)?", s)return float(num.group()) * 10000 if num else np.nannum = re.search(r"\d+(\.\d+)?", s)return float(num.group()) if num else np.nandf["月薪"] = df["月薪"].apply(clean_money)
12,000 → 12000.0,1.8万 → 18000.0,面议 → NaN。
Step 7:缺失值处理(千万别无脑 fillna)
分三类处理:
# ① 关键字段缺失 → 直接剔除,并留痕key_cols = ["姓名", "手机号"]bad_rows = df[df[key_cols].isna().any(axis=1)]bad_rows.to_excel("需人工复核.xlsx", index=False) # 不要静默丢弃!df = df.dropna(subset=key_cols)# ② 分类字段 → 填"未知"df["部门"] = df["部门"].fillna("未知")# ③ 数值字段 → 视业务决定# df["月薪"] = df["月薪"].fillna(df["月薪"].median())
坑3:
fillna(0)和fillna(均值)是最常见的错误。薪资缺失填0,会让平均值严重失真。缺失值怎么填,是业务问题,不是技术问题。
Step 8:异常值检测
q1, q3 = df["月薪"].quantile([0.25, 0.75])iqr = q3 - q1lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqroutliers = df[(df["月薪"] < lower) | (df["月薪"] > upper)]print(f"发现 {len(outliers)} 条薪资异常,建议人工复核")
坑4:IQR 法在样本量 < 30 时极不稳定,小数据集请直接用业务规则(如"月薪 > 0 且 < 100万")。
五、串起来 + 输出美化
from openpyxl.styles import Font, Alignment, PatternFilldef clean_pipeline(df: pd.DataFrame) -> pd.DataFrame:# ... 上面 Step1~8 全部塞进来 ...return dfdf = pd.read_excel("员工信息_原始.xlsx", dtype=str)df = clean_pipeline(df)with pd.ExcelWriter("员工信息_已清洗.xlsx", engine="openpyxl") as writer:df.to_excel(writer, index=False, sheet_name="清洗结果")ws = writer.sheets["清洗结果"]# 表头样式for cell in ws[1]:cell.font = Font(bold=True, color="FFFFFF")cell.fill = PatternFill("solid", fgColor="4F81BD")cell.alignment = Alignment(horizontal="center", vertical="center")# 自适应列宽for col in ws.columns:width = max(len(str(c.value)) if c.value else 0 for c in col) + 4ws.column_dimensions[col[0].column_letter].width = min(width, 30)ws.freeze_panes = "A2" # 冻结首行ws.auto_filter.ref = ws.dimensions # 自动筛选print(f"✅ 清洗完成,共 {len(df)} 行")
洗前后对比:
| 姓名 | 手机号 | 入职日期 | 月薪 | 部门 |
|---|---|---|---|---|
| 张三 | 13800138000 | 2023-01-05 | 12000.0 | 技术部 |
| 李四 | 13800138001 | 2023-02-11 | 15000.0 | 技术部 |
| 王五 | 13800138002 | 2023-03-20 | 18000.0 | 市场部 |
| 赵六 | NaN | 2023-05-12 | NaN | 市场部 |
| 钱七 | 13800138006 | 2023-07-01 | 20000.0 | 人事部 |
六、进阶:批量处理一整个文件夹
from pathlib import Pathdef batch_clean(input_dir: str, output_dir: str):Path(output_dir).mkdir(parents=True, exist_ok=True)for file in Path(input_dir).glob("*.xls*"):if file.name.startswith("~$"): # 跳过Excel临时文件continuetry:df = pd.read_excel(file, dtype=str)df = clean_pipeline(df)out = Path(output_dir) / f"已清洗_{file.stem}.xlsx"df.to_excel(out, index=False)print(f"✅ {file.name} → {out.name}({len(df)}行)")except Exception as e:print(f"❌ {file.name} 失败:{e}")batch_clean("./原始报表", "./清洗结果")
批量处理的三个必备设计:
跳过
~$开头的文件 —— Excel 打开时的临时文件,读了必报错输出到新目录 —— 永远不要覆盖原始文件,出事能回溯
单个文件失败不影响整体 ——
try/except包住循环体
七、几个踩过的坑,替你省点时间
| 坑 | 症状 | 解法 |
|---|---|---|
| 科学计数法 | 手机号变 1.38E+10 | 读取时 dtype=str |
| 零宽字符 | VLOOKUP 匹配不上 | 正则去掉 \u200b\u3000\xa0 |
| 日期 8 位数字 | 解析成 1970 年 | 单独指定 format="%Y%m%d" |
| 大文件卡死 | 10万行读 5 分钟 | 改用 engine="calamine" 或先转 CSV |
| 覆盖源文件 | 数据没了 | 输出到独立目录 |
大文件读取优化:
pip install python-calaminedf = pd.read_excel("大文件.xlsx", engine="calamine", dtype=str)实测比默认引擎快 3~5 倍。
八、最后说两句
这套脚本的核心不是代码,而是把"清洗"拆成可复用的步骤:
统一空值 → 清洗文本 → 判重 → 字段级规范化 → 缺失值分类处理 → 异常值检测 → 输出留痕
字段会变,但这个骨架不变。把它封装成 clean_pipeline(),下次换个字段配置就能直接跑。
别再一行行手动删空格了,那是在用生命给Excel打工。