
上周我们班交第五次python作业题目本身不复杂但一收上来我就发现很多人到了这一关才真正把前面学的语法、函数、文件操作串起来。这次的作业要求是做一个完整的小流程抓取网页数据、做清洗、统计、出图最后还要打包成exe能直接交差。看题目容易真正动手才发现处处是坑。我把这几次批改作业遇到的高频问题、学生折腾半天没搞定的细节以及我推荐的写法和理由全部整理出来了。如果你是刚开始学Python、正在做类似课后作业的“第五次作业”或者你要带人做作业这份内容比直接搜“python源码大全”有用得多。咱们直接进入正题。1. 作业拆解题目要求与整体方案选型1.1 题目原貌与考察目标原作业题目大概是这样的给定一个静态网页的数据列表请使用Python编写程序完成以下功能第一采集目标页面的数据第二对获取的数据进行清洗和类型转换第三使用pandas完成至少一次groupby分组统计第四用matplotlib绘制结果图第五将最终脚本打包成exe文件。附加要求里还有一道递归题“李白打酒”以及要求爬虫函数具备异常重试机制。很多同学一看就懵了觉得是五件事。但别急着写代码先拆一下考察点。这套作业其实是在检验前四周的教学成果requests做网络请求、BeautifulSoup做解析、pandas做数据处理、matplotlib做可视化、递归函数解决经典问题、装饰器做重试逻辑、PyInstaller做打包交付。每一样看起来都是基础操作连起来就是一个微型数据项目。我批改的时候发现大家的问题不是某个单点不会而是不知道这些模块怎么串。比如有人单独写爬虫没问题但数据一进pandas就变成object类型统计结果全错有人图表画得出来但中文全变方块有人打包完一运行就闪退。这些问题单独问都能查到答案但组合在一起就非常劝退。这篇我就按完整流程走一遍顺便点名哪些地方最容易翻车。1.2 为什么选requestsBeautifulSoup而不是Scrapy先说技术选型。作业里要抓的数据是静态页面量级就几十条完全没必要上Scrapy。Scrapy是完整爬虫框架适合大规模爬取、分布式部署、管道处理对作业来说属于“杀鸡用牛刀”学习曲线还陡。requests加BeautifulSoup足够代码量小、逻辑直白、出了错也好定位。另一个常见选择是selenium。如果目标页面是动态渲染的也就是数据靠JavaScript加载确实需要selenium模拟浏览器。但这道题明确是静态页面用selenium纯属自找麻烦——又重又慢还容易被网页检测到是自动化工具。正确做法是先用requests获取HTML源码用BeautifulSoup直接解析。如果之后遇到必须动态渲染的页面再在作业后来单独研究selenium。我自己衡量一个方案好不好就看两个指标性能和代码可读性。requests加bs4在这道题里性能足够代码5分钟能写完后面维护也不头疼。1.3 整体代码结构设计作业里没有强制要求模块化但我不建议把所有逻辑塞进一个main.py。按功能拆分至少分四个文件到后面打包和排查问题都轻松很多project/ ├── main.py # 主流程串起所有步骤 ├── crawler.py # 爬虫模块封装requests请求和解析 ├── analysis.py # 数据处理和统计 ├── visual.py # 可视化输出 ├── requirements.txt # 依赖清单 └── data/ ├── raw.csv # 原始数据 └── result.png # 结果图为什么这么拆核心原因是让每个文件只干一件事。爬虫模块改了不影响分析模块分析逻辑改了也不影响主流程。另外作业后期要打包exe模块化代码的依赖关系更清晰PyInstaller处理起来也少了很多莫名其妙的缺失模块问题。不要为了显得简单而把所有代码写成单文件代码是给下一个自己看的尤其是过一两个星期之后。2. 环境准备Python多版本、虚拟环境与依赖安装2.1 多版本Python共存与venv隔离开始写代码前先把环境捋清楚。现在很多机器上可能同时装了多个Python版本比如默认的3.8后来又装了3.10还有PyCharm自带的解释器。版本混乱是很多人第一道坎明明pip install装好了包运行cmd却提示ModuleNotFoundError。我的建议是作业项目一律用venv创建独立虚拟环境。在项目目录下执行python -m venv venvWindows下激活venv\Scripts\activateLinux或macOS下激活source venv/bin/activate激活之后终端前面的提示符会出现一个(venv)标记这样你就能确认当前用的是虚拟环境。之后所有pip安装包都会装到这个虚拟环境里不会再污染系统全局Python也不会出现“另一个版本装了包、当前环境找不到”的情况。环境变量这块也顺带说一下。很多教程会让人手动配置PATH但其实在Windows安装Python时勾选“Add Python to PATH”就能搞定。如果你已经在cmd里敲python没反应大概率是安装时没勾选重新安装或手动把Python安装目录加进PATH就行。有了venv之后项目内的依赖和Python解释器都由虚拟环境管理全局环境变量反而不用频繁动。2.2 依赖安装细节与镜像源加速在这次作业中需要安装的库主要有这些包名用途安装命令requestsHTTP请求pip install requestsbeautifulsoup4HTML解析pip install beautifulsoup4pandas数据处理与统计pip install pandasmatplotlib数据可视化pip install matplotlibopenpyxlExcel文件读写pip install openpyxlpyinstallerexe打包pip install pyinstalleropencv-python图像处理可选pip install opencv-python安装时最容易遇到的问题就是下载速度慢或超时。这种情况直接换国内镜像源比如清华、阿里云的PyPI镜像。举个实际命令pip install requests beautifulsoup4 pandas matplotlib openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple为了以后不用每次都敲镜像源可以配置全局pip源pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple另外提醒一句网上很多教程的安装提示是拿特定工具里的比如“要安装缺失的节点请先在你的 python 环境中运行 pip install -u --pre comfyui-m”这种命令看着像万能解决方案但请先看清楚它到底是给哪个项目用的。作业环境里不要乱复制这类命令装多了库反而容易把环境搞得一团糟。2.3 下载cv2时的常见误解有个热搜词特别典型“python下载cv2”。很多人一查教程说“导入cv2之前要先下载cv2”于是直接执行pip install cv2结果报错找不到这个包。原因很简单PyPI上没有叫cv2的包OpenCV的Python对应包名是opencv-python。正确命令是pip install opencv-python安装完成后import时导入的是cv2import cv2这次的作业如果只做表格数据和图表其实用不到OpenCV。但如果附加题里有图片数据或你想把图表做更复杂的处理再装它也不迟。不要把“看到教程就装”当成习惯先确认项目到底需要什么再逐个添加依赖。3. 核心代码实现从爬虫到可视化全流程3.1 爬虫模块requests加BeautifulSoup解析先说通数据从哪来。作业的页面是个简单的列表页表格里有名称、分类、数量、价格等字段。静态页面的话直接requests就能拿HTML然后用BeautifulSoup定位表格行。我的爬虫模块会写成这样import time import random import requests from bs4 import BeautifulSoup def fetch_html(url, headersNone, timeout10): if headers is None: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeouttimeout) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def parse_table(html): soup BeautifulSoup(html, html.parser) rows [] for tr in soup.select(table tbody tr): cells tr.find_all(td) if len(cells) 4: continue rows.append({ name: cells[0].get_text(stripTrue), category: cells[1].get_text(stripTrue), count: cells[2].get_text(stripTrue), price: cells[3].get_text(stripTrue), }) return rows这里有几个细节值得展开讲。resp.encoding resp.apparent_encoding这一步很多人会漏但非常关键。网页有时候声称自己的编码是UTF-8实际内容是GBK直接解析就会出现乱码。apparent_encoding是requests根据内容推断出来的编码虽然偶尔慢一丢丢但能最大程度避免中文乱码。raise_for_status()是让HTTP错误直接抛出异常。很多初学者不看状态码直接解析html一旦页面404拿到的是错误页面的代码解析出来全是空列表。加这一行之后请求失败会立刻报错排查起来快很多。解析时我用了CSS选择器table tbody tr而不是soup.find_all(tr)原因是如果页面顶部有别的表格find_all(tr)会把无关行也抓进来。限定在目标表格的范围内选行更稳妥。如果作业页面没有tbody就换成table tr具体以实际HTML为准。随机UA也可以加个列表每次随机取一个减小被反爬策略拦截的概率。3.2 数据清洗类型转换与空值处理网页上抓回来的数据几乎全是字符串。比如数量是“”这种写法price里可能有“1,299”这种格式。如果不处理pandas虽然能算出groupby但统计结果一定是错的。清洗这一步建议在进入DataFrame之后就立刻做。import pandas as pd df pd.DataFrame(raw_rows) # 清洗空缺 df df.dropna(subset[name, category]) # 数量转为数值counts里可能是整数直接转int df[count] pd.to_numeric(df[count], errorscoerce).fillna(0).astype(int) # 价格去符号转float df[price] ( df[price] .str.replace(, , regexFalse) .str.replace(,, , regexFalse) .str.strip() ) df[price] pd.to_numeric(df[price], errorscoerce).fillna(0.0)errorscoerce是pandas类型转换的经典玩法。它的意思是一旦遇到无法转成数字的值就把那个位置变成NaN之后再用fillna统一填充。这样不会因为个别脏数据导致整个转换报错。有个细节很多人做字符串清洗时喜欢用apply(lambda x: ...)不是不行但.str.replace()这种方式对Series整体操作性能更好代码也更简洁。作业数据量小感受不出来但习惯要往高效方向养。类型转换做完后再用df.dtypes确认一下每个字段的类型这一步能预防百分之九十的“统计结果不对”。清洗完成后可以把原始数据存一份CSV方便后续排错df.to_csv(data/raw.csv, indexFalse, encodingutf-8-sig)这里编码用utf-8-sig而不是utf-8是因为Excel直接打开无BOM的UTF-8文件会中文乱码utf-8-sig能解决这个问题。3.3 李白打酒递归与循环的经典练习附加题“李白打酒”是这次作业里很有意思的一道小题。题目原文是“李白街上走提壶去买酒。遇店加一倍见花喝一斗。三遇店和花喝光壶中酒。试问壶中原有多少酒”这题如果从头正向推导会涉及两个未知量反而绕。我推荐用倒推法不管最后一步是遇到店还是遇到花把每一步的逆运算反过来推回去就行。def drink_back(ops, wine0): ops里依次记录的是店还是花 从最后一个动作往前倒推 for op in reversed(ops): if op 花: wine 1 # 正着是喝一斗倒着就是加一斗 elif op 店: wine / 2 # 正着是加一倍倒着就是减半 return wine ops [店, 花, 店, 花, 店, 花] initial_wine drink_back(ops) print(initial_wine) # 0.8757/8斗这里容易搞混的点是循环顺序。reversed(ops)从最后一个动作开始遇到“花”就加回一斗遇到“店”就除以2。最终结果是0.875也就是7/8斗。你可以拿这个答案验算一开始0.875斗遇店变成1.75见花喝掉1斗剩0.75再遇店变1.5见花喝掉剩0.5再遇店变1.0见花喝掉正好为0。完全对得上。题目里要求用递归实现也可以改成递归写法def drink_recursive(ops, idx, wine): if idx 0: return wine if ops[idx] 花: return drink_recursive(ops, idx - 1, wine 1) return drink_recursive(ops, idx - 1, wine / 2)两种写法本质一样但递归更能体现“从结果反推初始状态”的思维。交代码时建议两种都写顺便在注释里说明时间复杂度和思路这个附加题基本就是满分。3.4 数据分析pandas groupby分组统计的正确姿势主作业要求“使用pandas完成至少一次groupby分组统计”最常见的统计需求是按category聚合求数量和价格的平均值再按平均值排序。代码长这样summary ( df.groupby(category, as_indexFalse) .agg( total_count(count, sum), avg_price(price, mean), item_nums(name, count), ) .sort_values(avg_price, ascendingFalse) ) print(summary)这里踩坑最多的地方是把groupby和itertools.groupby搞混。itertools.groupby是Python标准库里的函数它要求数据必须先按分组键排序否则分出来的组是相邻重复项的合并结果和你想要的“按分类去重统计”完全两码事。pandas的groupby就没有这个限制底层会按组键重新分组。另一个常见问题是groupby之后直接打印看到一堆像pandas.core.groupby.generic.DataFrameGroupBy object at 0x...的东西就慌了。其实groupby之后必须接聚合操作比如sum()、mean()、agg()否则返回的是懒加载的分组对象不是结果表。用agg()的好处是可以一次性对不同列做不同聚合比如数量求总和、价格求平均、名称计数一行代码全部搞定。as_indexFalse这个参数也很实用。默认情况下pandas会把分组键当成索引如果你后续想继续处理summary表还是希望category是一个普通列就显式设置成False。作业里如果要求按统计结果排序输出上面代码里的sort_values就是必须的。3.5 装饰器重试与多进程扩展作业附加要求里提到“爬虫函数要具备异常重试机制”这其实就是让你写一个装饰器。简单的重试装饰器长这样import time from functools import wraps def retry(max_retries3, delay1): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: print(f第 {attempt 1} 次请求失败: {e}) if attempt max_retries - 1: raise time.sleep(delay) return wrapper return decorator使用的时候在爬虫函数上面加一行retry(max_retries3, delay1)这个函数失败后会自动重试。很多初学者第一次接触装饰器会觉得它很神但核心就两步装饰器接收函数返回一个包装函数包装函数里在调用原函数前后加逻辑。用functools.wraps是为了保留原函数的元信息比如__name__、__doc__不然调试时打印函数名会变成wrapper而不是真实的函数名。多进程这块作业本身不要求但如果你想扩展一下可以用multiprocessing.Pool同时抓取多个URLfrom multiprocessing import Pool def crawl_one_url(url): return parse_table(fetch_html(url)) if __name__ __main__: urls [http://example.com/page1, http://example.com/page2] with Pool(processes4) as pool: results pool.map(crawl_one_url, urls)需要注意if __name__ __main__保护在Windows下是必须的否则多进程会无限创建子进程报错。如果爬的是接口而不是页面requests的post方式也可以套用同样的重试逻辑区别只是把requests.get换成requests.post并把参数传进json或data字段。不过项目本身只有一两个页面单线程就够没必要为了炫技加多进程反而增加复杂度。3.6 可视化输出matplotlib中文乱码与图表保存最后是matplotlib画图。作业通常会要求画柱状图看分类的数量分布或者画饼图看价格占比。代码比较常规import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 6)) ax.bar(summary[category], summary[total_count]) ax.set_title(各分类数量统计) ax.set_xlabel(分类) ax.set_ylabel(数量) plt.savefig(data/result.png, dpi150, bbox_inchestight) plt.close(fig)没有plt.rcParams这两行你画出来的图中文全是方块交了作业基本等于白做。axes.unicode_minus设置为False是为了正常显示坐标轴上的负号不然负号也会变成乱码。plt.close(fig)很多人会漏掉。在脚本里连续出图时不关图内存会一路涨尤其在jupyter里反复运行图会越积越多。把图保存成文件之后立刻close这是个好习惯。如果想让图更有说服力可以在柱子上方加数值标签用ax.bar_label(ax.containers[0])就能搞定这是matplotlib 3.4之后的新特性比手动遍历柱子坐标简单得多。4. 打包成exe把Python脚本变成可交付程序4.1 PyInstaller基础参数与使用命令作业要求最后将程序打包成exe。PyInstaller是目前最成熟的打包工具没有之一。安装很简单pip install pyinstaller然后在项目根目录执行pyinstaller -F -w main.py三个参数的含义要搞清楚。-F表示打包成单个exe文件方便交作业-w表示运行时不弹出黑色的控制台窗口如果程序是命令行程序想去掉日志输出可以把-w去掉。还有一个常用参数是--namepyinstaller -F -w -n fifth_python_homework main.py这样生成的exe叫fifth_python_homework.exe而不是默认的main.exe。打包完后在dist目录下能看到exe文件。但这里要注意程序里如果有相对路径直接双击exe时data/result.png可能保存不到dist\data目录下因为工作目录变了。一个稳妥的做法是在代码里用绝对路径定位文件from pathlib import Path base_dir Path(__file__).resolve().parent output_dir base_dir / data output_dir.mkdir(exist_okTrue)不过打包成单文件后__file__指向的是临时解压目录而不是exe所在目录所以更合理的方式是取当前工作目录或exe所在目录。作业里用的数据都是动态抓取的不涉及额外的资源文件问题不大。如果要附带静态资源建议用--add-data参数指定或者把资源读取逻辑改成打包后的路径格式这个属于进阶内容。打包最直观的感受就是慢尤其项目里用了pandas和matplotlib。第一次打包可能要好几分钟这不是卡死是在收集依赖和写文件等就完了。打包完产生的build目录和spec文件也别直接删后面二次打包会用到。4.2 打包过程中最常见的坑第一个坑是exe运行时闪退。双击exe直接退出看不到错误信息。解决办法是在没加-w参数的控制台版exe里运行或者在入口代码最外层加一个异常捕获并把错误信息写入文件import traceback if __name__ __main__: try: main() except Exception: with open(error.log, w, encodingutf-8) as f: f.write(traceback.format_exc())这样即使闪退看error.log也能定位问题。第二个坑是杀毒软件误报。PyInstaller打包出来的exe因为带有可执行解压逻辑经常被某些杀毒软件标记为风险程序。这个在作业提交时提前说明一下或者打包时用--noupx参数减小被误报的概率pyinstaller -F -w --noupx main.py第三个坑是依赖库版本不一致。开发环境跑得好好的打包后exe提示找不到某个DLL或模块多半是把新版本的pandas和旧版numpy混在一起。解决方法是保持虚拟环境干净只装项目需要的依赖不要装一个库再来一个库最后环境里几十个包全被打进去体积和兼容性都会出问题。5. 常见问题与排查技巧实录5.1 pip安装报错与镜像源调整pip报错最常见的是网络超时和找不到版本。超时优先换镜像源具体命令上文已经给了。找不到版本则要看是不是Python版本太老或太新。比如pandas新版本要求Python 3.9以上如果你的默认Python还是3.7pip就会提示找不到满足要求的版本。这时候不是硬磕pip而是要检查解释器版本。5.2 编码问题UnicodeDecodeError与乱码解析网页出现乱码先看resp.encoding是否和页面内容一致直接用resp.apparent_encoding解决。读取CSV时出现UnicodeDecodeError可以在pd.read_csv里指定encodingutf-8或encodinggbk取决于原始文件保存时的编码。写文件时统一用utf-8-sig这样Excel、记事本都能正常打开。5.3 groupby用错库与KeyError写了from itertools import groupby之后再用pandas的groupby容易张冠李戴。解决办法是不混用分组统计用到pandas时直接df.groupby不要引入itertools的groupby。出现KeyError先确认列名是否一致清洗前后的列名很可能变了比如原来叫price清洗后叫avg_price再拿price去聚合就报错。5.4 cv2安装失败opencv-python安装失败通常是因为包体积大、网络不稳定。解决办法是换镜像源重装。如果安装成功但import cv2还是失败检查是不是虚拟环境没激活或者pip装到了另一个Python环境里。在终端执行pip list | findstr opencv能看到说明装成功了。5.5 问题排查速查表现象可能原因解决办法pip安装超时默认源慢换清华或阿里云镜像源pandas导入报错numpy版本不兼容升级或降级pip包保持环境干净CSV中文乱码编码不是utf-8-sig写入时用utf-8-sig图表中文乱码未设置中文字体设置plt.rcParams字体打包后闪退入口异常未捕获在main外层写try-except并输出error.logexe体积巨大matplotlib/pandas依赖多属于正常现象用venv控制依赖数量groupby结果不对字符串类型未转数值先清洗再统计用dtypes检查cv2安装失败包名错误使用pip install opencv-python最后再分享一个小技巧按这个流程把第五次作业完整走一遍你会发现自己对“模块化”“数据流”的理解比刷一百道题都管用。我个人在批改里发现凡是老老实实先花十分钟拆题、把输出文件路径都规划好的同学后面基本不会翻车一上来就写代码的基本都在某个地方卡住又回头改。建议你交作业前用一台没有Python环境的机器或虚拟机试跑一下exe确认它能独立运行。这一步能避免很多交付时的尴尬。如果还想继续深入可以试试把统计结果存入SQLite用sqlalchemy建个模型把每次跑批的数据记录下来这就是一条很自然地从作业过渡到生产项目的路径。先把眼前这版跑通比什么都强。