尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python文件处理实战:从CSV读取到数据清洗与统计报告

Python文件处理实战:从CSV读取到数据清洗与统计报告 又到了Python课交作业的日子。我拿到第五次作业的时候第一反应是“这次终于有点像真事儿的活儿了”——不再是一堆零碎的语法题而是要把文件读写、字符串处理、异常捕获和数据统计串起来画出一条完整的数据流水线。这次作业是处理一份学生成绩数据要求写一个脚本读取CSV文件、清洗明显不合理的脏数据、统计指标、输出报告。听上去不算难但真做起来到处都是坑编码不对读出来全是乱码空行和缺失值让程序直接崩溃更不用说Windows下的路径分隔符和各种隐蔽的类型错误。这篇就把整个思考过程和踩坑经历完整复盘一遍。正在学Python的、或者在赶类似课程作业的可以把这份拆解当参考照着走能省掉很多试错时间。先声明下面展示的代码不是唯一答案。评分老师真正看重的是思路对不对、边界处理是否严谨、代码能不能拿一份新数据接着跑。这一点比把某个函数写得花哨重要得多。1. 作业到底在考什么从第五次作业看核心能力1.1 题目逐句拆解成技术点我拿到题目后做的第一件事不是打开编辑器而是把作业要求里的每一句话拆开翻译成具体的技术点。很多同学觉得这一步多余其实恰恰相反作业里最容易被忽略的信息往往就是评分标准里卡人的地方。比如题目里写着“读取CSV文件”背后至少包括三个考验文件路径怎么写、用什么方式打开、会不会因为编码不同读取失败。这里就有很多同学栽在默认编码上Windows下用记事本另存的CSV经常是GBK而Python的open函数默认用系统编码在Linux或macOS上环境一变就直接报UnicodeDecodeError。再看“清洗数据”这四个字里面藏的东西更多。成绩列可能混着字符串、空值、负数和超过100的数字甚至还有整行数据都是空的。这些都是真实文件里最常见的情况作业设计到这里就是逼着你学会用strip去掉空格、用异常处理拦住转换失败的脏值、用逻辑判断把不合理的数据挡在门外。统计指标部分考察的是Python基础数据结构的掌握程度。平均值、最高分、最低分需要用列表推导式或者循环实现分组统计则离不开字典排序涉及sorted函数和lambda表达式。到了这一步作业结构已经非常清晰了表层是文件处理里层全是基本功。1.2 为什么这次作业特别像真实项目我敢说很多第一次接触这份作业的人会有一种感觉这次的东西终于不是“为了做题而做题”了。原因是它模拟了真实项目中非常常见的一条工作流拿到原始数据处理出一个能用的结果。数据清洗和基础统计分析在很多岗位上是新人的第一周工作内容。我见过太多实习生第一天上班就被丢一份乱七八糟的Excel表格要求“把这个月的数据整理一下发我”。他在学校做的就是这个作业但到了真实环境里数据量更大、错误更隐蔽、要求更严格唯一不变的是那套处理方法先看数据结构再逐行清洗最后汇总统计。所以这份作业本质上是在提前训练一种工程化思维。作业只要求输出一个结果但好代码的标准是换一份格式相同的数据也能跑、能对异常数据给提示而不是崩溃、能让别人在一小时内看懂逻辑。能达到这个水平作业就是优秀进入真实项目也能更快上手。反过来如果只是硬写一段只对自己的样例有效的代码那到了实际环境里一定寸步难行。2. 写代码之前先把环境和工作流程理清楚2.1 环境准备Python安装与虚拟环境那些事每次带人做这种综合性的作业我都会先问一句你的Python环境到底能不能见人很多同学以为安装Python就是下载、双击、下一步、完成。其实大量运行报错从根源上都能追溯到安装环节。官方python.org下载安装包时有一个地方非常容易忽略安装向导首页最下面的“Add Python to PATH”选项默认是不勾选的。如果不勾选后面在命令行里输python系统会提示找不到命令很多人的第一反应是重新安装其实勾上这个选项或者手动把Python的安装目录加到系统环境变量里就解决了。更隐蔽的问题是机器上装了多个Python版本。我见过一个同学自学时装了3.9课程要求用3.10他又装了一遍结果pip install装到了3.9的包目录python命令跑的是3.10程序运行时始终找不到第三方库。排查了很久才发现是解释器版本对不上。判定方法很简单在命令行里输入python --version和pip --version看两个版本号是否一致不一致就说明路径优先顺序出问题了。处理这种综合作业我强烈建议用虚拟环境。Python自带的venv模块不需要额外安装在项目文件夹里执行python -m venv venv然后激活。Windows下运行venv\Scripts\activatemacOS和Linux下运行source venv/bin/activate。激活后pip装的所有第三方库都只在这个项目里生效不会污染全局环境更重要的是即使你全局环境被搞乱了这个项目依然能跑。提示作业提交前在干净环境里试跑一遍是最有效的防翻车手段。我吃过亏才明白能跑在你自己的机器上不等于能在老师的机器上跑尽量减少对环境的依赖才是正道。2.2 先用伪代码把数据流理顺再动手环境搞定后别急着敲代码。我的习惯是先在纸上或者直接写在注释里把整个程序的数据流画出来。我当时的初步伪代码大致是这样的# 1. 读取csv文件 # 2. 跳过表头遍历每一行 # 3. 对每一行做清洗去空格、跳过空行、成绩转数字 # 4. 如果成绩非法记录错误并跳过这一行 # 5. 计算平均分、最高分、最低分 # 6. 按科目分组统计各科目的平均分 # 7. 输出结果到控制台同时写一份报告文件这一步看似简单但它能逼着你在动笔之前把“这个程序到底要做什么”想清楚。很多同学踩坑是因为拿到题目就写写到一半发现数据结构没想好回过头来改前面的代码越改越乱。伪代码写完后我习惯再规划一下程序的模块划分。不要把所有逻辑都塞进一个main函数里更不要用全局变量到处传数据。合理的划分是一个函数负责读取文件一个函数负责清洗数据一个函数负责统计分析最后一个入口函数把整个流程串起来。这样无论调试还是改需求都能精准定位到具体位置不用把整个文件从头翻到尾。提示模块化不是老手的专利。哪怕是最简单的作业用函数把职责拆开代码可读性都会有质的提升老师一眼就能看出你理解程序结构。3. 核心代码实现从读取文件到输出报告3.1 文件读取与编码问题乱码和BOM头数据流理清后第一个要解决的是文件读取。读取CSV文件最标准的做法是用with open来打开文件让系统在退出代码块后自动关闭文件不用手动调用close。这段代码我在第一个版本里是这样写的with open(scores.csv, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: print(row)这里用csv.DictReader而不是csv.reader是因为它能把每一行自动转成字典字段名直接来自CSV的表头。这样后面访问数据就可以写成row[姓名]、row[成绩]这种可读性非常高的形式而不是面对一堆数字下标在脑子里强行对应。但是乱码问题在作业中非常常见。Windows下用Excel或记事本保存的CSV很多是GBK编码。如果按照utf-8去读要么直接抛UnicodeDecodeError要么读出一堆不知道是什么的字。最简单的解法是打开文件时先用带utf-8编码试试报错就换GBK。更稳一点可以直接在打开的时候用errorsignore参数把无法解码的字符忽略掉但这个方法有风险副作用是可能静默丢掉部分数据不到万不得已我不推荐。还有一个非常隐蔽的坑就是BOM头。有些程序保存UTF-8文件时会自动加一个不可见的BOM标志直接导致第一个字段名变成“\ufeff姓名”后面用row[姓名]读取时永远拿到的是空值。第一次遇到这个问题的同学几乎都会懵因为打印字典时字段名看起来完全正常。这个问题的解法非常直接用编码utf-8-sig来打开文件with open(scores.csv, r, encodingutf-8-sig) as f: reader csv.DictReader(f)utf-8-sig会自动把BOM头吃掉并且兼容普通的UTF-8文件推荐作为默认选项。另外如果你在Windows上写代码写入文件时open里面的newline参数也值得注意。在Windows平台上默认换行符会被转成\r\n严重的话写入的CSV每行之间会多出一个空行。解决办法是写入时加上newline参数这个参数会禁止换行符转换输出会变得非常齐整。3.2 数据清洗脏数据怎么一步步变成规整数据数据读取没问题之后最大的工作量就落在清洗环节。我建议把清洗逻辑单独抽成一个函数接收字典形式的原始行数据返回清洗后的可用数据。def clean_row(row): name (row.get(姓名) or ).strip() subject (row.get(科目) or ).strip() score_str (row.get(成绩) or ).strip() if not name or not subject or not score_str: return None try: score float(score_str) except ValueError: return None if score 0 or score 100: return None return {姓名: name, 科目: subject, 成绩: score}这个函数看似简单但内部有三个关键处理用了(row.get(姓名) or )这种写法是因为当字典里没有这个keyget会返回None直接用None调strip会抛AttributeError。这里用or 把None和空字符串统一处理成空字符串后面判断not name就能同时拦住这两种情况。try/except包住float转换拦住‘95分’‘优秀’‘A’这类无法转成数字的数据。最后再判断成绩是否在0到100这个合法区间负数、超过100的分数一律视为脏数据。主流程里把清洗函数套进去cleaned [] with open(scores.csv, r, encodingutf-8-sig, newline) as f: reader csv.DictReader(f) for raw in reader: item clean_row(raw) if item is None: continue cleaned.append(item)这里每个非法的行都被静默跳过了。我在实际做的时候还会加一个counter统计跳过了多少条最后打印出来这样不仅能在作业报告里写出说明还能验证自己的清洗逻辑确实起了作用。比如统计出来“总记录500条清洗掉12条异常数据”比单纯说“我做了数据清洗”要有说服力得多。提示清洗逻辑不要直接在读取数据的循环里四处散落独立函数不仅好测试也方便你后续调整规则。比如老师要求把0分也算合法你的改动就控制在函数内部五行以内。3.3 统计、排序与最终报告的生成清洗完成的cleaned列表已经是规规整整的字典列表了。接下来的统计分析核心就是用Python内置函数和标准库不需要任何第三方库。先看最简单的总体统计scores [item[成绩] for item in cleaned] avg_score sum(scores) / len(scores) max_score max(scores) min_score min(scores)这种写法在数据量小的时候没有问题但如果数据量很大建议只在循环里累加总成绩并记录最大最小值一次性遍历得到所有统计量而不是反复遍历列表。作业数据一般不会太大用列表推导式更清晰可读性是第一位的。分组统计用字典处理grouped {} for item in cleaned: subject item[科目] if subject not in grouped: grouped[subject] [] grouped[subject].append(item[成绩]) for subject, score_list in grouped.items(): grouped_avg sum(score_list) / len(score_list) print(f{subject}: {grouped_avg:.2f})这个模式非常经典遇到按某个key分组的统计需求时基本都是这个套路。Python 3.7之后字典保持插入顺序所以你按第一次出现的顺序遍历就是合理自然的顺序不需要额外处理。如果要对结果排序比如输出科目平均分从高到低的排名就配合sorted和lambdafor subject, avg in sorted(grouped.items(), keylambda x: sum(x[1]) / len(x[1]), reverseTrue): print(f{subject}: {sum(avg) / len(avg):.2f})这里单独提一个被忽略的细节格式化输出。f-string里的:.2f表示保留两位小数成绩精度统一格式避免出现“平均分85.50000000000001”这种看起来极度业余的输出。输出报告的写法我不建议只print到控制台可以把结果同时写入summary.txt这份文件就是你的交付物。写完输出后打印一行“报告已生成”这让整个程序看起来非常完整。当然如果你在Windows控制台打印中文时遇到乱码大多是把源代码文件编码和终端编码搞混了尽量保证代码文件用UTF-8保存就好。有些作业还允许附加可视化强推matplotlib画柱状图但初学者最容易踩中中文字体不显示的坑所有标签变成方块。解决方法是绘图前手动指定中文字体import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] FalseSimHei在Windows系统上基本可用。macOS可能需要换成Arial Unicode MS或PingFang SC。这不算作业核心但是一个很亮眼的加分项。4. 踩坑实录文件处理作业最容易翻车的几个地方4.1 最常见错误速查表我把自己写过、以及帮别人排查过的问题整理成了一张表基本覆盖了这个阶段作业的高频bug。报错现象根本原因解决办法UnicodeDecodeError读取文件编码与文件实际编码不符打开时指定encoding或换成utf-8-sig读取到的第一个字段名有\ufeffCSV文件带BOM头用encodingutf-8-sig打开写入CSV后每行中间有空行Windows换行符被转换写入时加newlineFileNotFoundError文件路径不对或工作目录不对用os.path.abspath(文件)打印绝对路径确认AttributeError: NoneType object has no attribute striprow里没有对应字段名或值为None用(row.get(字段名) or ).strip()float转换ValueError成绩列有非数字内容try/except捕获并跳过中文输出乱码终端编码或源码编码不统一源码保存为UTF-8Windows终端可试chcp 65001程序能跑但结果全为空读取时BOM导致字典key多出字符打印row.keys()检查字段名其中FileNotFoundError值得单独说一下。出现这个问题大多数情况不是文件真的不存在而是程序的工作目录压根就不在你以为的那个文件夹里。IDE默认工作目录经常和.py文件所在目录不一致一个特别土但特别有效的排查方法就是先把文件和.py脚本放在同一个目录然后额外用os模块配合绝对路径来定位和确认import os print(os.path.abspath(scores.csv))看到打印出来的路径你就能立刻确认文件到底在哪、程序到底在找哪个目录。如果你希望程序不受启动目录影响可以用脚本所在目录来定位文件这样无论从哪个位置启动都能正确找到数据文件这是很多实际项目里会沿用的做法。4.2 调试方法从print到日志再到断点代码出了问题不要盯着屏幕发呆要自己动手找线索。我调试这种作业第一步永远是print大法在关键节点打印变量的值和类型。尤其在数据处理的代码里打印每一行的清洗结果能让你一目了然地看到数据在哪一步开始变形。print虽好用但代码里到处是print交作业前删起来很麻烦。如果你想做得更专业可以用标准库logging在程序入口设置日志级别想要的分析信息统一输出到控制台或日志文件。这种做法的好处是你不需要把调试代码删掉只需要把级别调到WARNING平时都不会打扰你。import logging logging.basicConfig(levellogging.INFO, format%(levelname)s: %(message)s) logging.info(共读取 %d 条记录, len(cleaned)) logging.warning(清洗掉 %d 条异常数据, skipped)再狂野一点的调试方式是断点。如果你用的是PyCharm在代码行号旁边点一下红点就出来了然后点debug运行程序会停在断点处你可以查看任意变量当下的值。这个阶段很多同学还不太敢用调试器觉得复杂但其实它比print快得多尤其是程序逻辑复杂、一次要跑很久的时候断点调试几乎是最有效的查错方式。提示遇到问题先确认是你自己的数据问题还是代码逻辑问题。最简单的方法把数据样本缩到5行以内如果5行跑不通那就是逻辑问题5行能跑通几百行报错那问题多半出在数据质量上。这就是“最小化复现”以后写再多代码都受用。4.3 一个隐蔽的坑列表复用导致的重复数据这是一个让初学者非常困惑的问题明明每次往列表里append一个字典最后打印却发现所有字典内容完全一样。原因在于如果你复用同一个字典变量每次修改后都append到列表里列表里存的其实都是指向同一个字典的引用而不是字典的拷贝。你会看到所有条目反映的是最后一次修改的结果。处理办法很直接每次append前创建一个新的字典对象或者在append时用copy()复制一份。这个坑最坑人的地方在于小数据量时偶尔能跑出正确结果数据一复杂就暴露。排查方法依然是那个土办法在append之后立刻打印一下这个字典看看它是不是符合预期。如果循环里打印没问题循环结束打印列表却出问题那基本可以断定是引用复用的问题。这类问题在作业里不太容易遇到因为作业数据往往比较简单。但只要你想追求健壮性在真实数据处理时类似问题一定还会碰到提前知道总比等到实习时才发现好。5. 把作业做成项目命令行、测试与代码整洁度5.1 用argparse把脚本升级成小工具如果你不想只做一个交差就完事的人这里有一个非常简单但效果很好的进阶把脚本从“固定处理scores.csv”升级成“处理任何你指定的CSV文件”。标准库argparse可以让你在命令行这样运行程序python analyze_scores.py --file scores.csv --output report.txt对应的代码框架大致是import argparse def main(): parser argparse.ArgumentParser(description学生成绩统计分析工具) parser.add_argument(--file, requiredTrue, helpCSV文件路径) parser.add_argument(--output, defaultreport.txt, help输出报告路径) args parser.parse_args() cleaned load_and_clean(args.file) summary calculate_stats(cleaned) write_report(summary, args.output) if __name__ __main__: main()这里把入口包在ifname main里既保证了脚本能被直接运行也保证了被其他模块导入时不会自动执行。加上argparse之后脚本的通用性瞬间提升不用改代码就能处理任意路径的数据文件。这个升级在作业里的加分效果远比多写两道算法题明显因为它直接证明了你具备写“工具”而不是写“一次性脚本”的思维。5.2 让代码能被review测试、命名与注释最后一部分是决定优秀与合格之间分水岭的地方代码整洁度。我不建议为了显得厉害去写特别花哨的列表推导式或一行式代码。代码首先是给人看的其次才是给机器跑的。命名要直观用total_score而不是ts用cleaned_data而不是cd。变量名一清二楚读代码的人根本不需要来回猜测。注释这事的标准我概括成一句话注释解释为什么而不是解释是什么。比如说“这道题的规则是成绩不能低于0分”这句注释是有价值的因为它说明了一个外部约束条件“这段代码把score转换成float”不加注释也能看懂这句就是噪音。写注释的时候多想一想这段逻辑如果不写注释后来的人会不会想不通为什么会在这里做这个判断。如果是那就值得写如果不是删掉也不可惜。测试也不一定要上pytest这种框架作业阶段可以用一个简单的验证函数来确保统计逻辑正确。比如构造一个只有五行的小数据手工算出平均分、最高分、最低分然后让程序跑一遍比对结果。这个习惯越小养成越好因为到了真实项目里不写测试的后果就是改一个老功能牵出一批新bug到时候你费的时间可比写测试多得多。提示交作业前再检查一次这四件事文件路径有没有写死、异常处理有没有覆盖数据为空的情况、结果输出是否格式统一、代码在文件末尾是否留有测试用的临时print。把这几件小事做到位代码质量基本上已经在你同学的平均水平之上了。这里顺便分享一个小技巧交作业之前把代码里的文件路径改成中文文件名、把成绩列里故意塞几个脏数据再运行一次。如果你的程序能在这种“刁难”下不崩溃、给出清晰提示说明你的健壮性已经超过班里一大半人。我在完成这次作业之后最深的体会是作业里真正培养的不是某一个语法知识而是一种“把问题拆成小任务、再逐个击破”的思维方式。从读文件到清洗数据再到统计和输出每一个环节遇到的问题在以后的代码生涯里都会不断以各种面貌重新出现。把第五次作业当成一个小项目来对待不光是完成课程要求更值得你认真做一遍。
返回列表