清理混乱数据的根本在于准备:先让所有行遵守同一套规则,才谈得上修内容。 把三种分隔符统一成一种,改动了 19 行;切完发现有 5 行只有 4 段。 补空要补在对的位置。这几行第三段是日期,说明缺的是城市,空位得插在第三列。
上一篇只统计不改。这一篇开始动手,但动的只是「形状」,不是「内容」。 这个顺序是本课反复强调的:先形状,后内容。反了就会一直在错的格子上改。
一、准备是什么意思
书里那句话:清理混乱数据的根本在于准备。 准备的意思是——先让所有行遵守同一套规则,再开始修具体内容。 规则只有一条:一行五个字段,字段之间用同一个分隔符。
这一步不做,后面所有拆列、替换、去重都是在错的形状上修修补补。 形状不对的时候修内容,等于在斜的地基上砌墙。
二、统一分隔符
把斜杠和中文逗号全部换成井号,改动了 19 行。 这里有个细节:中文逗号是「,」,不是「,」。这两个长得像,程序里完全不同。 换成同一个之后,才能用同一条规则切分。
统一不是「统一成好看的那个」,是「统一成一个」。选哪个都行,只要只有一个。
三、切一下,看段数对不对
统一切完之后,按井号切分,数每行切出几段。 结果:58 行是五段,5 行只有四段。 四段的那几行,少了一个字段。不处理的话,拆成表格会整行错位——日期顶到城市那一列。
错位不报错,只会让后面按城市做的统计全歪。 「切完先数段数」是拆列之前必须做的一步检查。
四、修复:把空位补对
补之前先看内容:这几行第三段是「2023-xx-xx」,明显是日期。 五段的标准形状是 姓名/电话/城市/日期/等级,第三段该是城市。 所以缺的是城市,空位要插在第三列,不能补在末尾。
补的是空字符串,不是猜一个城市填进去。猜值等于在数据里造了一个假事实。 补完之后 63 行 × 5 列,形状统一了。这才叫「组织好」。
五、为什么顺序不能反
假设先洗内容:把 +86 去掉、把 ^ 去掉、把空格删掉。 这时候行还是混着三种分隔符,你根本没法确认「去掉的这 4 个字符是在电话字段里,还是在城市字段里」。 所以只能一行一行看,一遍一遍改。
先统一形状,才有「字段」这个概念;有了字段,清洗才能批量做。 这就是「准备」两个字的重量:它决定了后面能不能自动化。
六、动手 Demo:准备与组织:先让每行长得一样
统一分隔符,切分,数段数分布,再把缺字段的行补空。 每一步都打印行数,确认没有多也没有少。
SUR = ["王", "李", "张", "刘", "陈", "杨", "赵", "黄", "欧阳", "司马", "周", "吴"]
GIV = ["小明", "伟", "芳", "娜", "强", "磊", "静", "洋", "勇", "艳"]
CITY = ["北京 朝阳", "上海 浦东", "广州 天河", "深圳 南山",
"杭州 西湖", "成都 武侯", "武汉 洪山", "西安 雁塔"]
LV = ["VIP", "A", "B", "C"]
def tel(i):
if i % 5 == 0:
return "+8613%09d" % (i * 137 + 1000)
if i % 5 == 1:
d = "%08d" % (i * 271 + 3000)
return "139-%s-%s" % (d[:4], d[4:])
return "138%08d" % (i * 313 + 5000)
def nm(i):
s, g = SUR[i % 12], GIV[(i * 3) % 10]
if i % 11 == 0:
return s + "^" + g
if i % 13 == 0:
return s + " " + g
return s + g
def sep(i):
return "/" if i % 7 == 0 else ("," if i % 7 == 3 else "#")
RAW = []
for i in range(60):
c = CITY[i % 8]
if i % 6 == 0:
c = " " + c + " "
f = [nm(i), tel(i), c, "2023-%02d-%02d" % (i % 12 + 1, i % 27 + 1), LV[i % 4]]
if i % 17 == 5:
f = f[:2] + f[3:]
RAW.append(sep(i).join(f))
RAW += [RAW[5], RAW[17], RAW[33]]
# 统一分隔符
fixed = [s.replace("/", "#").replace(",", "#") for s in RAW]
print("改动 %d 行" % sum(1 for a, b in zip(RAW, fixed) if a != b))
seg = [s.split("#") for s in fixed]
cnt = {}
for x in seg:
cnt[len(x)] = cnt.get(len(x), 0) + 1
for k in sorted(cnt):
print("%d 段:%d 行" % (k, cnt[k]))
import re
repaired = []
for x in seg:
if len(x) == 5:
repaired.append(x)
elif len(x) == 4:
if re.match("^20\\d\\d-", x[2]):
x = x[:2] + [""] + x[2:]
else:
x = x + [""]
repaired.append(x)
print("补齐后 %d 行,每行 %d 列" % (len(repaired), len(repaired[0])))
print("行数没变:%s" % (len(repaired) == len(fixed)))
实跑输出:
--------------------------------------------------------------------
准备与组织:先让每行长得一样
--------------------------------------------------------------------
① 清理的根本在于准备
--------------------------------------------------------------------
书里那句话:清理混乱数据的根本在于准备。准备的意思是——先让所有行遵守同一套规则。
规则只有一条:一行五个字段,字段之间用同一个分隔符。
这一步不做,后面所有拆列、替换、去重都是在错的形状上修修补补。
② 统一分隔符
--------------------------------------------------------------------
把 / 和中文逗号全部换成 #,改动了 19 行。
现在每一行都用 # 分隔,可以按同一套规则切了。
③ 切一下,看段数对不对
--------------------------------------------------------------------
| 切出来几段 | 有多少行 |
|:---:|---:|
| 4 段 | 5 行 |
| 5 段 | 58 行 |
大部分行是五段,但有 5 行不是——说明那些行少了一个字段。
少字段的行如果不处理,拆成表格后会整行错位:日期顶到城市那一列,等级顶到日期那一列。
④ 修复:把段数补齐
--------------------------------------------------------------------
给缺字段的行补一个空字符串,补了 5 行。
补之前先看内容:这几行第三段是「2023-xx-xx」,明显是日期,说明缺的是城市,空位得补在第三列。
补空而不是猜值。猜一个城市填进去,等于在数据里造了一个假事实。
⑤ 现在有形状了
--------------------------------------------------------------------
63 行 × 5 列。列名:姓名、电话、城市、入职日期、等级。
这才叫「组织好」。组织好之后,才谈得上修复内容。
顺序很重要:先统一形状,再统一内容。反过来做,你会一直在错的格子上改。
最后一行是个「不变量检查」:补空只会插入字符,不该改变行数。这种检查写一句,能挡掉一大类手滑。 注意补空的位置是靠「第三段是不是日期」判断出来的,不是猜的。规则写清楚,别人复算得出来。
30 秒实验
- 如果缺的是第一列(姓名),还能靠「看内容」判断出来吗?该怎么做?
- 把三种分隔符统一成中文逗号而不是井号,后面的代码要改几处?
- 想一想:为什么「先形状后内容」比「先内容后形状」省事?
常见误区
- 先洗内容再统一形状:字段边界没定,洗的时候只能一行一行看。
- 补空补在末尾:错位照旧,而且更难查——因为行数、列数都「对」了。
- 缺字段就猜一个值:猜出来的值是假事实,比空着危险得多。
- 切完不看段数分布:直接按五列解包,遇到四段行就崩,或者悄悄错位。
自测三问(点开对答案)
1. 「准备」具体指什么动作? —— 让所有行遵守同一套形状规则:同一个分隔符、同样的字段数、字段顺序一致。 2. 为什么补空要判断位置? —— 缺的可能不是最后一列。位置补错了,前面看着都对,数据整体却是错的。 3. 为什么先统一形状再洗内容? —— 只有分了字段,才能对某个字段批量处理;否则每一步都只能靠肉眼看整行。形状齐了,下一篇正式拆列:一行字符串变成五个字段,从此可以按列算东西。
- ☐ (本篇完,下一篇 13.3 按分隔符拆成多列)