
寒假刚开始的时候我收到了Python寒假第二次作业点开题目那一瞬间说实话有点慌。第一次作业还是练练变量、列表、循环交上去的时候觉得Python也不过如此第二次作业直接升了一个维度要求用函数组织代码做一个带文件读写的统计程序还要从网页上获取数据进行可视化。从能跑到像样差距一下子拉开了。这篇文章就把我完成这份作业的全过程复盘一遍题目怎么拆解、函数模块化怎么写、数据和文件的坑在哪里、爬虫和可视化怎么落地以及寒假深夜被报错反复折磨后沉淀下来的排查链路。无论你是刚学完Python基础的大学生还是转行自学编程的初学者这份复盘应该都能帮你避开不少弯路。1. 面对寒假第二次作业第一件事不是打开编辑器很多人拿到这种综合性作业习惯性直接打开PyCharm或者VS Code开始敲代码我的经历告诉我这是最容易翻车的开场方式。第二次作业和第一次最大的区别是它不再是一道题而是一个小项目。如果不先把需求拆干净后面写着写着就会发现各模块互相打架改一处崩三处。1.1 先给作业画一张能力地图我拿到作业后做的第一件事是把题目里的每个动词圈出来。比如统计读取爬取展示每个动词背后对应的都是明确的技术栈。我当时把作业要求整理成了四个大块数据获取从本地文件CSV/TXT/JSON读数据部分题目要求从网页爬取。数据处理去重、过滤、类型转换统计最大值、最小值、平均值等。程序结构所有核心逻辑必须封装成函数不能全写在一个脚本里。输出展示控制台输出结果进阶要求用matplotlib画统计图。画完这张图我才发现作业的真正考点不是某个语法点而是结构化编程的意识把大问题拆小、把小块拼成整体。这也是为什么很多人第一次作业考满分、第二次作业却写成一团乱麻的原因。没有拆题这一步后面所有努力都是在给一个歪掉的地基添砖加瓦。1.2 第二次作业的隐藏考点结构化思维我辅导过一个学弟他的代码能跑出结果但整个文件400多行没有函数没有注释全靠一个接一个的循环暴力往下堆。交上去虽然功能实现了但老师让他加一个新统计维度他花了一整个晚上才找到该在哪插代码。这就是缺少结构化思维的典型症状。第二次作业的隐藏考点就是看你有没有意识到代码不只是写给计算机看的更是写给下一个自己看的。函数拆得好不好模块边界清不清晰决定了这个程序是能跑还是能维护。我当时强制自己遵守一个规则每个函数只做一件事函数名必须能直接读出它的作用。后面所有调试的顺畅都得益于这个一开始看起来有点费时间的决定。2. 函数与模块化这份作业逼我改掉的编码习惯第一次作业我大量使用print来观察中间结果俗称面向输出编程。到了寒假第二次作业数据规模上来了步骤也多了如果不做函数封装每一段调试都要从头跑一遍整个脚本效率低得让人崩溃。2.1 为什么作业要求里反复强调必须封装成函数一个非常直观的原因可复用。统计功能要同时应用在本地文件和网页下载的数据上没有函数就得复制粘贴两遍代码。有了函数传入不同的数据源路径同一套逻辑直接跑两遍结果一致还不需要维护副本。第二个原因是可测试。函数是天然的测试单元只要有明确的输入和预期输出就可以单独验证。我当时写了一个清洗数据的函数直接传进去一条脏数据看返回结果是否符合预期不需要读整个程序逻辑。这种调试体验是写面条代码完全感受不到的。第三个原因说出来有点扎心老师看作业也是看结构的。一个函数列表清清楚楚说明你真的理解了这道题的设计意图而不是靠运气和暴力堆出来。在作业和面试之间这份意识是连贯的。2.2 默认参数、返回值、作用域三个绕不开的细节封装函数的时候很多初学者会踩一个特别隐蔽的坑把可变对象当成默认参数。我当时就写过这样的代码def add_item(name, item_list[]): item_list.append(name) return item_list第一次调用返回[苹果]第二次调用返回[苹果, 香蕉]看起来没问题可一旦在循环里反复调用列表会不断累积结果完全错乱。原因是Python的默认参数只会在定义时求值一次后续每次调用复用的都是同一个列表对象。正确的写法是def add_item(name, item_listNone): if item_list is None: item_list [] item_list.append(name) return item_list另一个常见问题就是return和print搞混。一个函数如果只打印结果却不返回外层代码拿到的是None后面所有对这个结果的运算都会直接报错。我的习惯是函数内部尽量不print所有计算结论通过return传递打印统一放到调用方。这样函数既可以在程序里用也可以在命令行单独调试时看输出职责单一排查起来快得多。作用域的问题也很典型在函数内部直接操作全局变量或者修改了一个和全局变量同名的局部变量然后发现外层数据根本没变。Python里通过global声明可以改全局变量但我个人的建议是尽量别用。数据流应该通过参数传进去、通过返回值送出来这样每个函数都是独立的黑盒组合起来才不容易出错。2.3 一个作业项目该有的目录组织第二次作业的推荐结构我当时参考了一个老学长给的模板亲测好用winter_assignment/ ├── main.py # 程序入口负责调度各模块 ├── data_loader.py # 文件读写、网页请求相关函数 ├── data_cleaner.py # 数据清洗、去重、类型转换 ├── stats_calculator.py# 统计计算逻辑 ├── plotter.py # 可视化相关函数 ├── output/ # 程序运行导出的结果 │ └── report.txt └── data/ └── sales.csv这样的组织方式每个文件只关心一类事情。main.py读入数据调用清洗、统计、绘图整个流程一目了然。别觉得一个作业搞这个阵仗是小题大做从第二次作业开始建立的项目感在后面做毕业设计、做开源项目时价值会被不断放大。3. 数据获取链路从本地文件读到网页爬虫寒假第二次作业里数据获取占了整整一大块。单纯读一个写死的列表已经不算数了必须从一个真实的数据来源里把数据拿进来然后才能谈统计和展示。这也是很多同学卡住的地方逻辑全都对就是数据进不来。3.1 文件读写里的编码暗坑第一份数据是CSV文件我当时直接写了with open(data/sales.csv, r) as f: for line in f: print(line)结果Windows上跑得好好的把同样的代码放到Mac上直接报UnicodeDecodeError。原因很简单Windows中文环境下很多工具默认用GBK编码保存文件而Python 3里open函数默认用的是系统编码换一台机器就水土不服。正确的做法是在打开文件时就显式指定编码并且明确换行符的处理with open(data/sales.csv, r, encodingutf-8, newline) as f: reader csv.DictReader(f) for row in reader: print(row)用csv.DictReader能直接把每行转成字典按列名取值比如row[price]比手动split(,)安全得多能自动处理字段里的逗号和引号。JSON文件则直接用json.load()Python会帮你把结构解析成字典和列表但要注意如果文件特别大一次性加载可能会爆内存那时候得用ijson这类流式解析库。这里给一个我后来养成的习惯所有读文件的操作一律显式写encodingutf-8宁可写全不能省略。跨平台、跨机器的时候这个习惯能省掉一大堆玄学报错。3.2 requests加BeautifulSoup的温和爬虫实践作业里的爬虫题要求抓一个公开网页上的表格数据。爬虫在很多初学者眼里很酷但也很容易翻车。我当时的做法比较克制也比较稳妥。首先明确法律和道德边界只爬允许爬的公开数据优先看网站有没有提供API其次看清楚robots.txt允许哪些路径设置一个合理的请求间隔不做任何对服务器有压力的事情。一个合格的学习型爬虫不应该对目标站点造成任何负担。核心代码分三步走。第一步用requests获取页面import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding utf-8这里有三件小事特别重要。headers里带个User-Agent是为了不要一上来就用默认的python-requests标识很多站点对陌生UA请求直接拒绝。timeout10是防止某个请求卡死整个作业hang住。resp.encoding要主动声明否则遇到中文网页Python自动检测编码偶尔会翻车页面一上来就是乱码。第二步用BeautifulSoup解析HTMLfrom bs4 import BeautifulSoup soup BeautifulSoup(resp.text, html.parser) rows soup.select(table tr)第三步把表格里的每行数据提取出来清洗后再进入统计模块。这就接上文件数据处理的同一套逻辑了。我当时写完后把爬下来的数据和本地CSV数据跑同一个统计函数一对比发现两边结论一致那一瞬间真的理解了数据流统一的意义。3.3 脏数据清洗让程序开始捡垃圾数据拿回来并不等于能用因为真实数据里充满了各种意外缺了某个字段的值、数字列里混进了单位、同一项有两个完全不同的写法。清洗数据的过程几乎占了我整个作业开发时间的三分之一这一点都不夸张。去重是最基础的操作。热搜词里有一条python筛选一样的说的就是这个场景。如果只是找出重复项可以用集合seen set() duplicates set() for item in items: if item in seen: duplicates.add(item) else: seen.add(item)如果数据更复杂比如是按多个字段判断是否重复可以直接用pandasdf df.drop_duplicates(subset[date, product_id], keepfirst)类型转换也是高频需求。从CSV里读出来的数字默认是字符串总和、平均值算出来全是错的。我的踩坑记录是没有转换类型之前算出来的平均值特别离谱检查了很久才想起来CSV读出来全是字符串。给字段统一做转换时可以用一个函数包一下def parse_price(raw): if isinstance(raw, str): raw raw.replace(元, ).strip() return float(raw)对于缺失值常见的策略包括直接删除、填充平均值、填充为0。选择哪种策略取决于业务背景和分析目的不能无脑套。我当时把缺失值处理也封装成了函数并且用注释写明此处选择填充平均值的理由这份注释后来被老师单独标红说是整个作业最亮眼的地方。4. 用pandas和matplotlib完成统计与可视化Python数据分析与可视化作为搜索热词不是没有原因的。寒假第二次作业里统计部分用纯手写循环也能做但一旦数据量上到几百上千行手写循环的效率和表达能力都捉襟见肘。pandas在这一步几乎是必选项。4.1 用pandas把统计结果算出来读取CSV变成DataFrame一行import pandas as pd df pd.read_csv(data/sales.csv, encodingutf-8) print(df.describe())describe()会直接输出各数值列的计数、均值、标准差、最小值、四分位数、最大值作业要求里的统计量基本全覆盖了。如果还要按类别统计比如按商品分组求和summary df.groupby(category)[amount].sum().sort_values(ascendingFalse) print(summary)这一步的爽感在于我原本写的五六个自定义统计函数用pandas不到十行就实现了。但这里必须提醒一句如果作业明确要求手写统计函数建议还是按要求实现一遍然后在进阶部分用pandas对照验证。两头你都做既保证了作业得分点又展示了工具运用能力。作业里如果涉及到商品销量、评分这类连续型数据还可以算一个分组均值来观察趋势。除此之外pandas还有一个很好用的方法value_counts()可以统计一个字段的频次分布比如最热销的商品Top10一行代码就出来。4.2 matplotlib中文乱码和横坐标密集的现场修复可视化的第一道坎是中文和负号乱码。网上能找到各种版本的答案我实际操作下来最可靠的方案是设置字体为系统自带的黑体或宋体import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # Windows # plt.rcParams[font.sans-serif] [Arial Unicode MS] # macOS plt.rcParams[axes.unicode_minus] False这里有一个细节字体设置必须放在绘图代码之前而且如果在Jupyter里跑建议放到第一个单元格。设置完字体后负号依然乱码就是因为没有设置axes.unicode_minus为False。第二个高频问题就是python画图横坐标太密集。当数据点有100个横轴标签挤成一团黑线根本没法看。我做作业时的解决办法是按步长抽样显示标签数据点保留import matplotlib.ticker as ticker ax plt.gca() ax.xaxis.set_major_locator(ticker.MaxNLocator(nbins10))MaxNLocator会自动帮你在坐标轴上挑大约10个位置显示刻度既不会把标签挤成黑条又能保留数据分布的整体趋势。如果时间序列很长还可以用pd.date_range生成指定间隔的刻度位置配合ax.set_xticklabels格式化日期显示。4.3 作业要求之外的可视化加分项完成基本柱状图和折线图之后我顺手加了一个箱线图展示每个类别下销售额的分布情况。这一步让统计结果变得更立体柱状图告诉我平均值箱线图则让我看到每一组数据的离散程度和离群点。df.boxplot(columnamount, bycategory, figsize(10, 6)) plt.xticks(rotation45) plt.savefig(output/boxplot.png, dpi300, bbox_inchestight)bbox_inchestight这个参数我很推荐保存图片时会自动裁掉多余的留白放进作业报告里比默认效果好看不少。OBS截图和直接plt.show()保存分辨率也不一样用dpi300保存出来的图即使放大看也足够清楚。5. 寒假深夜排错实录完整排查链路按我的经验这份作业真正的老师不是教材而是那些半夜弹出的报错。以下三个问题是我和身边同学踩得最惨的每一个都有完整的排查链路分享出来希望大家不用重走这一遍。5.1 环境配置问题为什么装好了却不能用我有个同学卡在最开始明明按教程装了Python终端敲python --version却提示找不到命令。排查链路是这样的。第一步确认安装时是否勾选了Add Python to PATH。这一步默认不勾选没勾选的情况下Windows不会把Python的可执行文件路径加入环境变量。最简单的验证方式where python如果这条命令什么都查不到说明PATH里没有Python。解决办法两种一是重装时勾选Add Python to PATH二是在系统设置里手动添加Python安装目录和Scripts子目录到PATH。第二步如果是多版本Python共存还要注意python和python3可能指向不同的解释器。我的建议是写项目时始终用虚拟环境避免全局环境里版本混乱。python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # macOS / Linux pip install pandas matplotlib requests beautifulsoup4虚拟环境能让你锁死项目依赖的Python版本和第三方库版本作业写完把requirements.txt一导出换台机器也能直接复现。这既是作业要求里的加分点也是真实项目里的基本素养。第三步国内网络环境下pip install慢到让人怀疑人生解决方案是换国内镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas或者直接配置全局默认源一劳永逸。这条经验在我后面装numpy、cv2等一堆库的时候帮我省了大量等待时间。5.2 UnicodeDecodeError一朝解决终身感谢这个报错出现的时候我的代码本身没有任何逻辑问题纯粹是读取方式和文件保存方式不一致。排查链路如下。首先看报错信息指向哪一个文件、哪一个open调用。我当时的报错是UnicodeDecodeError: gbk codec cant decode byte 0x80 in position 123: illegal multibyte sequence说的是Python默认用GBK去解码这个文件但文件里有一个字节不符合GBK规则。接下来我需要知道文件本身是什么编码。用VS Code或者Notepad打开文件右下角就能看到当前文件的编码格式。如果是UTF-8那问题就清楚了我读取文件时没有告诉Python这是UTF-8它自作主张用了Windows默认的GBK。修复方式就是我前面提到的那一行with open(filepath, r, encodingutf-8) as f:如果文件本身就是GBK保存的那就把encoding改成gbk。最稳妥的是在保存文件时统一用UTF-8这样跨平台不会出问题。爬虫拿到HTML文本后也一样要检查resp.encoding是否和页面实际编码一致不一致就手动指定。5.3 爬虫超时与空数据的层层排查爬虫题翻车概率最高的两个现象一是请求超时二是解析结果为空。超时的排查链路先用浏览器手动访问目标网站看看是否正常。如果浏览器正常而脚本超时原因多半出在请求头缺失或者出杀软拦截。加上完整的User-Agent还是不行就试一下直接带Cookie或者增加重试逻辑for attempt in range(3): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() break except requests.RequestException as e: print(f第{attempt 1}次请求失败: {e}) time.sleep(2)解析结果为空的排查链路第一步是打印响应内容print(resp.text[:500])看看HTML里是不是真的有目标表格。如果有说明选择器写错了。我一开始用的是soup.find(table)但页面里有很多个table取到的那一个恰好是别的模块数据当然为空。改成soup.select_one(table#sales-table)之后问题立刻解决。选不中元素的时候优先检查id和classbs4对动态加载的JS内容是无能为力的这也是一个排查方向如果表格是JavaScript异步渲染出来的requests拿到的HTML里根本没有这些数据要么找接口要么用Playwright之类的无头浏览器。6. 交作业前的自检清单与未来延伸完成了功能和可视化并不意味着作业可以交了。我花了大概两个小时做了一遍自检和优化这些步骤看起来琐碎却在最终评价里起了决定性作用。6.1 代码可读性自检让另一个自己看一遍我交作业前做的事是隔一天再打开自己的代码模拟一个完全陌生的人去阅读。问自己几个问题每个函数一眼能看出职责吗变量名是否一看就懂有没有多余的重复代码注释我会写但不会每行都写。我倾向于在快函数上面写三行docstring说明这个函数的输入、输出、做了什么。比如def calculate_average_price(df): 计算商品均价。 参数: df: 包含price列的DataFrame。 返回: 均价数值保留两位小数。 真正有价值的注释是解释为什么这样做的注释而不是这一行在做加法的废话注释。把意图写清楚代码自己的逻辑用可读的函数名已经能表达两者配合才高效。6.2 异常处理与边界测试能跑和健壮之间的差距就差在异常处理和边界测试上。比如统计函数传入一个空列表、文件路径不存在、网络请求超时程序是优雅退出还是直接抛出一大段栈溢出疑云我的做法是给关键入口加上try-exceptdef main(): try: df load_and_clean_data(data/sales.csv) except FileNotFoundError: print(找不到数据文件请检查路径) return except Exception as e: print(f数据加载失败: {e}) return report generate_report(df) print(report)边界测试则是想一些刁钻的输入只有一个数据点怎么算方差所有字段都是空值怎么处理日期格式五花八门怎么统一这些问题提前想清楚作业的质量会比正常输入能跑通高出一个档次。这也顺便对应了热搜词里01背包动态规划python这类算法的本质要求边界条件永远是算法题和工程题共同的核心。6.3 从作业到作品后续还能怎么演进寒假第二次作业做完之后我的体会是作业只是起点这句话不是口号。代码已经拆成了清晰的模块换数据源就能跑出完全不同的分析结果这种可扩展性本身就意味着潜力。如果还有时间值得尝试的几个方向包括把控制台输出的报告改成自动生成的HTML文件用简单的模板拼接就能实现把所有关键步骤用argparse做成命令行参数实现改一改参数就能换文件路径和图表配置或者更进一步把清洗和统计逻辑写成一个小型工具包供后续课程反复使用。甚至还可以把这几百行代码整理整理写成一篇带说明文档的开源小项目放上GitHub。从作业到作品很多时候只是多走半步的距离。这次寒假作业给我最大的教训是Python编程的进阶从来不是记住更多函数而是学会组织代码、处理不可控的外部输入、在报错面前保持系统性的排查思路。这些东西在第一次作业里学不到却在实战里处处要用。如果你也正在跟这份寒假作业较劲别着急慢一点拆题、稳一点排查把它当成第一个真正的项目来做你会收获远比分数更多的东西。