
简介清华大学大数据应用人才培养系列《数据清洗》课程第五章课件共48页并含习题面向高校学生、职场新人及有经验的数据处理人员聚焦数据抽取这一关键环节系统讲解文本文件抽取、Web数据抽取、数据库数据抽取与增量数据抽取。课件以Kettle工具为主线通过实例演示分隔符识别、字段配置、数据库连接及结果预览等完整流程并介绍了HTML、JSON、XML等网页数据的解析思路数据库与增量抽取部分则包含连接配置、变更日志跟踪和新旧版本比较等实用方法。压缩包内为单个PPTX文件大小约3.78MB便于按章节自学或课堂引用。目前已有1088人学习/下载配套习题有助于巩固所学适合作为大数据、数据清洗方向的教学参考及自学资料。1. 数据清洗不是跑一遍脚本就完事第5章到底在解决什么做大数据项目的人多半都有过这种经历ETL 脚本写完、跑通、调度挂上第二天一看结果发现当天增量数据比前一天少了三成。查到最后不是代码逻辑错了是源头数据里混进了格式不一致的文本、网页采集的乱码、数据库字段的隐式转换以及增量抽取时漏掉的边界时间戳。清华这份《数据清洗》课程第5章把「文本、web、数据库、增量数据抽取」四类来源摆在一起讲其实就是在告诉我们数据清洗不是清洗本身的问题而是数据从哪来、以什么形态来、来多少决定了清洗策略。这一章适合正在做数据接入、数仓建设、大数据毕业设计或真实业务报表的人看完能把清洗从见招拆招提升到按来源定方案的层面。数据清洗的教科书定义是处理缺失值、去重、格式统一但真实生产里文本、web 和数据库三类数据源的脏数据特征完全不同。文本数据的问题是编码和半结构化web 数据的问题是噪声标签和动态加载数据库数据的问题是类型隐式转换和跨库方言而增量数据抽取则决定了清洗时效——是全量重跑还是每天只处理新增直接关系到集群资源和数据延迟。下面按这条主线把四类问题的清洗思路、实现细节和踩坑点逐一拆开。2. 先把清洗对象拆清楚文本、web、数据库三类数据各有各的脏法2.1 文本数据清洗别只想着去空格编码和结构才是大头文本类数据在真实项目里占比最高也是最容易被低估的一类。很多初学者拿到 CSV 或日志文件第一反应是strip()去空格、replace()换掉特殊字符但真正让清洗脚本跑挂的往往是编码问题。UTF-8 和 GBK 混存的场景在中文数据集里非常常见一个文件用pandas.read_csv直接读大概率在第三行就抛UnicodeDecodeError。我一般会先探测文件编码再用errorsreplace或分段读取兜底而不是让任务中断。import chardet import pandas as pd # 1. 探测编码避免 read_csv 直接崩 with open(user_log.txt, rb) as f: raw f.read(100000) # 取前 100KB 判断足够 enc chardet.detect(raw)[encoding] print(detected encoding:, enc) # 2. 用探测到的编码读取并容错非法字节 df pd.read_csv(user_log.txt, encodingenc, errorsreplace, sep\t)这段代码的要点有两个。第一chardet.detect只读文件开头一段速度极快不用全文件扫描但它对纯英文文本识别可能返回asciiISO-8859-1 之类中文场景下如果返回MacRoman就要警惕通常是样本太短误判这时可以加大采样字节数。第二errorsreplace会把非法字节替换成 字符虽然不优雅但能保住整批数据的加载后续再用正则去识别和补录这些异常位。文本清洗的第二大问题是半结构化——比如一行里有多个分隔符或者同一字段在不同行里格式不一致常见做法是先按最粗粒度分列再逐列精洗。文本清洗的分割策略我一般遵循从宽到窄先用正则把明显的块切出来再对每块做细粒度校验。不要一上来就按逗号或制表符分列日志里的逗号往往出现在 message 字段中。一个更稳的顺序是替换统一换行符 → 按分隔符粗分 → 逐字段类型校验 → 集中处理异常行。异常行不要直接丢弃单独落盘成quarantine文件方便后面回溯。这个习惯在增量场景下尤其重要因为上游格式变更往往先体现在异常行里。2.2 web 数据清洗爬下来的数据一半时间花在去噪和结构化上web 数据在课程第5章里单独占一节是有原因的。从网页里抽取的信息远远不止是去掉 HTML 标签那么简单。真实场景中网页采集数据包含导航栏、广告、相关推荐等噪声块如果直接整页入库后续做文本分析时特征会被严重稀释。常见做法是先用 CSS 选择器或 XPath 锁定正文容器再做标签剥离和空白压缩。requests拿到的 HTML 要过一遍lxml或BeautifulSoup并且要处理动态加载——很多站点正文是 JS 渲染的直接请求拿不到内容。import requests from bs4 import BeautifulSoup import re # 常见做法先拉页面再用选择器卡正文最后统一剥标签 resp requests.get(https://example.com/news/123, timeout10, headers{ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) soup BeautifulSoup(resp.text, lxml) # 定位正文容器不同站点这个选择器差异很大需要人工确认 article soup.select_one(article.news-content) or soup.select_one(div#content) if article: text article.get_text(separator\n) # 压缩连续空行去掉不可见字符 text re.sub(r\s, , text).strip()这段代码里最关键的是select_one的备选链第一个选择器没命中就尝试第二个。实际爬虫项目里同一个站点改版后 class 名变化是常态这种写法能把维护成本压低。另一个重点是get_text(separator\n)如果不指定 separatorBeautifulSoup 会把块级元素文本拼成一坨后续断句全是坑。web 清洗的另一大问题是 HTML 实体比如amp;、#39;这类字符get_text后依然存在跑 NLP 前要用html.unescape统一还原。再有就是编码探测网页返回的 charset 有时在 meta 里有时在响应头里建议优先取响应头失败再解析 meta否则中文页面极易乱码。2.3 数据库数据清洗类型、约束、方言三重坑数据库数据的清洗和前两类完全不同——它已经是结构化数据了但问题出在结构假装得很完整。典型场景是日期字段存成了字符串数值字段里混着null和空字符串或者同一个 ID 在不同表里类型不一致。从数据库抽数入库时这些隐式问题会在 join 和聚合时集中爆发。课程第5章把数据库单独列出来核心想强调的是入库前的类型与约束校验远比入库后的清洗代价低。常见做法是在 SELECT 阶段就做类型转换或者用中间表承接原始值再做规范化。-- 常见做法抽数时直接完成类型与默认值处理 SELECT COALESCE(NULLIF(user_id, ), unknown) AS user_id, SAFE_CAST(reg_time AS TIMESTAMP) AS reg_time, CASE WHEN LENGTH(phone) 11 THEN NULL ELSE phone END AS phone FROM source_db.users WHERE dt 2025-01-01这段 SQL 里的NULLIF把空字符串转成 NULL再由COALESCE给定 fallback 值SAFE_CAST是 BigQuery 语法转换失败返回 NULL 而不是报错其他引擎要用TRY_CAST或CAST配合异常处理。LENGTH(phone)这段做的是业务约束校验不满足规则的直接置空比在应用层逐行判断快一个数量级。跨数据库方言是另一道坎MySQL、PostgreSQL、SQL Server 对布尔值、时间字面量、NULL 排序的定义都有差异数据入库前最好统一成目标数仓的类型体系。常见做法是建一张 staging 表保留原始字段类型再做一层 typed 视图这样清洗逻辑可追溯误杀数据时还能回头查。3. 增量数据抽取从全量到增量的关键一步以及它的清洗差异3.1 增量抽取的四种方式时间戳、增量日志、CDC、全量比对数据清洗如果只做一次全量那叫数据整理真正考验功夫的是每天的增量部分。清华第5章把增量抽取放在数据清洗课程里是因为增量数据的清洗策略和全量有本质区别。增量抽取常见的四种方式是时间戳增量最易实现但依赖业务表有update_time、增量日志解析比如 MySQL 的 binlog对业务无侵入但需要开启相关配置和解析工具、CDC 工具如 Debezium捕获表级变更事件、以及全量比对用主键和哈希做差集最通用但开销最大。选择哪种取决于源系统的改造空间和时效要求。以最常见的自增主键 时间戳为例增量抽取踩过最多的坑是时间边界重复。如果任务在2025-01-02 00:00:30跑前一天的数据用WHERE update_time 2025-01-01 00:00:00 AND update_time 2025-01-02 00:00:00这个左闭右开区间看似没问题但源库如果有时区设置偏差或者业务端在 23:59:59 写入后又更新了一次漏数就会发生。我一般会做边界外扩查询区间向前后各扩 10 分钟然后在清洗层按主键去重用update_time取最新版本。这比单纯靠 WHERE 区间更稳。3.2 增量清洗的幂等设计重复跑要得到同一份结果增量清洗和全量清洗最大的不同在于幂等性。全量清洗只要函数正确跑几遍结果都一样增量清洗如果既要处理新增又要处理更新同一行数据在不同批次里会出现多次。比如用户订单状态从已支付变成已发货这个变化发生在第二天那第二天的增量数据里如果只 append 新记录下游统计订单金额时会算两遍。解决思路是增量清洗要产出业务主键 最新状态的拉链表或者在数仓层做 upsert。无唯一业务主键的表是另一类噩梦。比如日志表只有log_id但log_id在源端因为 bug 出现了重复增量抽取把两天数据都抽上来按log_idupsert 时会把不同业务含义的日志互相覆盖。常见做法是引入源系统主键 数据日期作为复合主键并在清洗层对同名主键做版本排序取max(etl_time)那条。这个逻辑不要写在应用内存里最好落在目标表的 MERGE 语句中这样即使任务重跑也能自动收敛。3.3 增量比对的兜底方案用哈希又快又省资源当源系统没有update_time只有insert_time而你又要捕捉更新数据全量比对几乎是唯一选择。全量比对不意味着每天拉全表——可以对源表做增量分区再对目标表对应分区计算哈希。比如把要比较的字段拼接后用MD5生成指纹每次只比对源和目标同一主键的指纹做差集再回源表取明细。这个方案不用碰 binlog 也不用建 CDC是数据仓库同学最常用的兜底手段。-- 常见做法源和目标都算哈希比差集再抽明细避免全量逐行对比 SELECT s.id FROM source_db.orders s LEFT JOIN dw.orders_hash t ON s.id t.id WHERE t.id IS NULL OR s.row_hash t.row_hash这里有个细节row_hash建议用CONCAT_WS(|, col1, col2, ...)拼接分隔符必须带否则abc|def和ab|cdef会撞出同一个哈希。字段顺序也要固定否则源和目标字段顺序不一致时哈希永远对不上。用哈希比对还有一个附加好处就是能发现数据没变但字段顺序变了的上游变更这在 ETL 链路里属于很隐蔽的问题全量比对揭出来的概率比日志解析高不少。哈希方案的缺点是源表数据量大时会拖垮源库解决方式是在源库按id分片多线程读或者走只读从库。4. 落一个能跑的清洗管道从四类数据源到统一清洗结果4.1 设计清洗管道分来源、分层、留审计理论拆完直接落到可复现的管道上。文本、web、数据库三类数据源加上增量抽取逻辑管道设计遵循一个原则不同来源数据走不同清洗函数统一输出格式每一层都留审计字段。层与层之间解耦上游抽数失败不影响下游清洗清洗失败不影响后续加载。数据来源、清洗时间、处理条数、异常条数这些审计信息要跟着数据一起走遇到问题可以快速圈定是哪一段出的错。分层设计里第一层是接入层只做最小改动保证数据能读第二层是清洗层处理具体脏数据第三层是标准化层统一字段名和类型。很多项目把清洗逻辑直接写进接入层导致源格式一变就得改整个管道这是数据清洗里最常见也最伤筋动骨的架构问题。增量抽取的逻辑放在接入层和清洗层之间作为独立模块。4.2 完整实现一段可跑的 Python 管道示意用一个最小管道把四类数据源串起来代码结构能直接套用到真实项目。import pandas as pd import hashlib from datetime import datetime, timedelta # 核心清洗函数按来源分发返回统一 DataFrame含审计列 def clean_text(raw_df): df raw_df.copy() df[content] df[content].str.replace(\r\n, \n, regexFalse) df[content] df[content].str.strip() return df def clean_web(raw_df): df raw_df.copy() # 去掉 HTML 标签和多余空白已在爬虫层做过一版这里兜底 df[title] df[title].str.replace(r[^], , regexTrue) df[content] df[content].str.replace(r\s, , regexTrue) return df def clean_db(raw_df): df raw_df.copy() df[price] pd.to_numeric(df[price], errorscoerce) df[dt] pd.to_datetime(df[dt], errorscoerce) return df def add_row_hash(df, key_cols, hash_cols): # 增量比对用的行哈希拼接时加大写分隔符防碰撞 df[row_hash] df[hash_cols].astype(str).agg( |.join, axis1 ).map(lambda s: hashlib.md5(s.encode()).hexdigest()) return df # 入口按数据来源分别读取分别清洗再统一合并 def clean_pipeline(source_type, raw_path, exec_date): if source_type text: raw pd.read_csv(raw_path, sep\t) cleaned clean_text(raw) elif source_type web: raw pd.read_json(raw_path) cleaned clean_web(raw) elif source_type db: raw pd.read_parquet(raw_path) cleaned clean_db(raw) else: raise ValueError(funknown source type: {source_type}) # 审计列谁在什么时候清洗的方便回溯 cleaned[etl_time] datetime.now().strftime(%Y-%m-%d %H:%M:%S) cleaned[source_type] source_type cleaned[exec_date] exec_date return cleaned这段代码的要点在于清洗函数独立成clean_text、clean_web、clean_db三个模块后续新数据源只要加一个函数和一个分支就行不需要动主体流程。pd.to_numeric和pd.to_datetime里的errorscoerce是把非法值转成 NaN比抛异常更符合清洗的容错思路。审计列etl_time记录的是任务执行时间exec_date是数据所属日期这两个概念必须区分清楚——增量任务里如果混用补数时会把历史分区全重写一遍。这个管道在实际项目里还需要套上一层调度框架Airflow、DolphinScheduler 或简单的 cron并在清洗层前面加一个分区续跑逻辑如果某天的任务失败了一次重跑时要先把目标分区清掉再写新数据。否则会出现旧数据和新数据共存、下游查重查不干净的问题。这个细节虽然小但增量场景下几乎必踩。4.3 管道验证清洗前后对比与断点检查管道跑完不能只看没报错。清洗结果要过三道验证第一行数与上游对比异常丢弃率超过 5% 就要告警第二关键字段的空值率、类型合法性和去重率在清洗前后要有明确改善第三抽样比对清洗样本人工确认业务含义没有被破坏。这三点里第三点最容易被跳过但它恰恰是防止清洗规则越洗越歪的护身符。断点检查是做增量清洗时特别好用的一招选一条已知业务含义的脏数据手动执行清洗函数看输出是否符合预期再跑完整管道对比这条数据在管道里和手动执行结果是否一致。如果不一致说明管道里还有隐式步骤在改数据比如read_csv自动类型推断或agg(|.join)时 NaN 变成字符串 nan。这种断点复现法在排增量数据丢数时效率极高比起翻日志猜测靠谱得多。5. 数据清洗避坑指南来自一线项目的血泪经验5.1 在 Python 脚本里直接改原表一个让人翻车的习惯做数据清洗时新手容易把 DataFrame 的inplaceTrue用得很随意以为原地修改省内存。但真实项目中源数据往往需要反复重跑、对照清洗前后差异一旦在脚本里直接改了原始读入的数据管道出错时连清洗之前长什么样都看不到了重新造数据更麻烦。解决方式是只读源清洗结果写入新表或新文件从不在原 DataFrame 上做硬覆盖。清洗管道要能随时从原始数据重放才能应对需求变更——需求改一次就重新造一遍源数据这在生产里是灾难。5.2 CSV 里的多行字段和引号陷阱文本清洗最容易栽的跟头pandas.read_csv默认认为引号内的换行是字段值的一部分但真实日志文件里引号可能不闭合逗号和换行混在一起直接导致列错位和行数错乱。用sep\t读文本数据偶尔会遇到某一行里多出一个制表符整行字段往后错一位此时 DataFrame 不会报错但数据含义全变了。解决方法是先按行读入检测每行的分隔符数量把分隔符数量异常的记录单独拎出来人工或规则修复后再重新拼接成结构化表格。5.3 增量时间字段在不同时区环境下的偏移增量数据抽少的元凶数据库datetime字段如果存入的是CST时间而数仓环境默认UTC按时间戳增量抽取时会有一个 8 小时偏移导致每天漏掉 8 小时的数据或重复抽 8 小时的数据。这类问题在现象上表现为某个时段的数据量明显偏少其他时段正常。排查时先看增量抽取脚本里有没有设置时区。解决方式是在抽数 SQL 里显式转换时区字段或者统一在目标层约定所有时间字段为同一时区。不要在多个脚本里各转各的时区转换一旦分散排查成本会成倍上升。5.4 数据库连接串和批量读取的隐式类型库表清洗的隐形暗礁用 SQL 客户端或 Python 连接数据库时有时候一个表的id字段是int另一个表是varchar两个表 join 时数据库会做隐式转换。如果varchar列里有非数字字符join 结果会把这一行全部丢弃数据在最终报表里无声消失。解决方式是清洗层明确字段类型映射表对每个目标字段声明源类型和目标类型转换失败的行单独收集不要丢进结果里。字段类型映射这张表本身就是数据字典的雏形业务团队拿它做口径对齐时能省很多沟通成本。5.5 增量任务重跑时忘了先清分区数据翻倍的经典操作调度平台里配置了import_time作为增量分区字段任务失败后重跑如果不清分区新数据会直接叠加写到已有分区上。对下游来说同一个用户会出现两条一模一样的记录金额翻倍。配合第 3 章提到的行哈希比对重跑时应该先按exec_date删除目标分区再写入新清洗结果。有些平台支持任务失败自动重跑但自动重跑时很少有人会记得带上清分区动作。所以这个逻辑宁可写死进管道代码里也不要依赖人的记忆。6. 进阶验证与效率技巧把清洗规则从能跑推向可信清洗管道能跑之后真正的工程价值在于校验规则和规则本身的演化管理。一个数据源的清洗规则不是写一次就固定的——上游字段新增、业务口径调整、数据质量波动都会催生新规则。我现在的习惯是维护一张规则表每条规则带有字段名、清洗类型、启用日期、操作人、规则描述。清洗管道每跑一次就把命中的规则写进审计日志下游如果发现某天数据异常可以直接锁定是哪个时间点加的新规则在起作用。-- 常见做法规则表结构示意配合审计日志做回溯 CREATE TABLE cleanup_rules ( rule_id INT PRIMARY KEY, field_name STRING, rule_type STRING, -- trim / type_cast / regex_replace / dedup rule_expr STRING, enabled_date DATE, owner STRING, description STRING );配合规则表还有一个验证技巧能大幅提升可信度就是清洗前后质量指标对比自动化把空值率、唯一值率、类型合法率、长度分布这几项指标在每轮清洗后自动算出来写进一张表和前一天对比。任何一项指标突变超过预设阈值就自动挂起下游任务并发出告警。这个做法不需要复杂的血缘系统一张对比表加一个告警脚本就能撑起一个小型数仓的质量监控。最后说一个我习惯留到做完整套清洗管道后回头检查的细节把清洗逻辑和数据读取逻辑分开测试。数据读取变了比如源库换了连接方式不应该影响清洗结果清洗规则改了也不应该影响读取逻辑。用虚构数据构造一个测试集每次改规则后跑一遍测试集这个习惯能挡住很多改了一处清洗逻辑结果别处字段被带偏的意外。希望这套从拆分数据源到增量抽取再到规则管理的思路能帮你少走几趟清洗的黑匣子弯路。本文还有配套的精品资源点击获取