Python第五次作业全解析:函数、文件读写与爬虫入门实战

发布时间:2026/9/17 4:13:54
Python第五次作业全解析:函数、文件读写与爬虫入门实战 看到python第五次作业这个标题点进来的朋友我猜你多半是在赶Python课程的作业或者刚交完作业想看看别人怎么写的。别急这次我把自己做第五次作业的全过程——从读题、写代码、踩坑到提交前的优化——完完整整记录下来了。这门课的第五次作业恰好是承上启下的一次前面几次还在讲print、if、for这些基础语句这次开始正式接触函数封装、文件读写、异常处理还顺带引出了爬虫的入门写法。如果你正在学Python基础或者刚把这几块知识学完想练手这份记录应该能给你一些参考。我把作业原题、每道题的解题思路、完整代码、运行结果以及我在调试过程中遇到的四个典型坑全部整理了出来。有些坑确实藏得挺深比如中文编码问题、爬虫请求头被服务器拒绝、Windows路径分隔符导致文件找不到这些不实际跑一遍根本发现不了。文末我还会放两个可以直接改改就用的代码骨架下次写类似作业能省不少事。1. 作业要求长什么样这次任务到底要考什么先说这次作业的背景。课程是面向全校开的Python公共选修课前面四次作业分别覆盖了变量与数据类型、流程控制、字符串操作、列表与字典。第五次作业恰好是个分水岭老师明确说这次作业之后课程重心会从语言语法转向用语言解决问题所以这次的题目有一个很明显的特点每道题都不只考一个知识点而是把两到三个知识点串在一起。1.1 题目回顾与考点分布我们班拿到的作业一共五道题我先把题目原文的大意写出来方便后面对照编写一个函数filter_words(word_list, min_len)接收一个单词列表和一个最小长度返回列表中所有长度不小于min_len的单词要求保持原有顺序。这道题考函数定义、参数传递、列表推导式。读取本地文件scores.txt文件里每行是姓名,成绩的格式要求统计平均分、最高分和不及格人数并把统计结果追加写入该文件末尾。这道题考文件读写、字符串分割、异常处理。给定一段HTML文本用BeautifulSoup提取其中所有a标签的href属性和链接文本输出格式为文本 - 链接。这道题考第三方库的基本使用、属性提取。用requests请求某个公开的JSON接口解析返回数据筛选出满足特定条件的记录并输出。这道题考网络请求、json模块解析、条件筛选。这是一个选做题用递归实现斐波那契数列并统计递归调用了多少次再对比用循环实现的版本看哪个快。选做题不计入总分但老师鼓励做说为后面学算法打基础。从考点分布能看出来这次作业的隐藏主题是把零散语法组装成小工具。函数是第一题的核心第二题把函数和文件操作结合第三题引入第三方库第四题模拟真实开发中的请求-解析-筛选三步走。我当时的判断是第二题和第四题最关键因为它们最接近实际工作中的开发模式。1.2 拿到题目后的第一轮技术方案判断我没有急着写代码而是先把每道题的输入-处理-输出理了一遍第一题的输入是列表整数处理逻辑是过滤输出是新列表。最简单可以用列表推导式一行写完重点在函数封装。第二题的输入是磁盘文件中间要处理文件不存在格式不规范这些边界情况输出要写回文件。这里我第一反应是必须用with open语句不能让文件句柄泄漏。第三题和第四题都涉及第三方库那就要先确认当前的Python环境里装没装requests和beautifulsoup4。没装的话得先pip install。第五题递归版斐波那契我预估n35左右就会明显卡顿因为递归调用次数是恐怖的指数级。统计调用次数正好能验证这个结论。另外我给自己定了一条纪律每个函数都要写docstring每个脚本都要加if __name__ __main__:入口。这不是老师硬性要求的但后面代码一长没有这两样东西调试起来会非常痛苦。2. 动手前的地基环境配置和必备知识点很多零基础的同学跳过这一步直接抄代码然后跑出一堆莫名其妙的错误。这里我建议多花十分钟做两件事确认Python版本和第三方库是否就绪以及把本次作业需要的核心函数过一遍。我自己就在环境上吃过亏当时电脑里同时装了Python 3.8和3.11结果在3.8的环境里运行3.11的语法直接报错。2.1 开发环境为什么强烈建议用虚拟环境我这次作业用的是venv虚拟环境。原因很简单这门课的电脑上已经装了Python 3.11但系统里还有Anaconda带的Python 3.8如果不区分环境pip install装包时很容易装到错误的环境里然后import时报错ModuleNotFoundError。操作流程非常固定Windows和Linux/macOS的命令略有区别# Windows python -m venv py5env py5env\Scripts\activate # Linux/macOS python3 -m venv py5env source py5env/bin/activate进入虚拟环境后命令行前面会出现(py5env)字样这时候再安装依赖就不会污染全局环境。第三代作业里需要用到requests和beautifulsoup4安装命令如下。如果下载慢可以临时指定国内镜像源比如清华源或者阿里源速度会快很多pip install requests beautifulsoup4 -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后务必验证一下安装结果python -c import requests; print(requests.__version__) python -c import bs4; print(bs4.__version__)能正常打印版本号说明环境没问题。这一步看起来多余但能省掉后面明明装了却报no module的排查时间。2.2 本次作业必须提前掌握的API清单第五次作业涉及的核心知识点我用一张表列出来方便边写边查知识点涉及题目关键函数/语法注意事项函数定义第1、2题def、return、默认参数参数传的是引用列表修改会作用到原对象匿名函数第1、4题lambda、sorted(key...)适合简单逻辑复杂逻辑用普通函数文件读写第2题open()、with、readlines()、write()注意编码参数encodingutf-8字符串处理第2题split()、strip()、f-stringstrip()能去掉行尾换行符第三方解析库第3题BeautifulSoup、find_all()、get(href)先pip install beautifulsoup4网络请求第4题requests.get()、response.json()必须设置User-Agent请求头递归第5题函数自身调用、终止条件注意递归深度限制约1000层模块化全部if __name__ __main__:让脚本既能被导入又能直接运行这些语法单独拿出来都不难但组合在一起就容易乱。我的经验是先每个知识点单独写一段10行以内的小demo跑通再做作业题。比如文件读写先随便建一个txt文件用with open读一遍、写一遍确认编码没问题再开始做第二题效率会高很多。3. 逐题拆解思路、代码与运行效果这一部分是最核心的我把五道题全部做完并调试通过的代码贴出来每道题都会先讲为什么这么写再讲运行效果。注意这些代码不是最优解但一定是逻辑最清晰、最适合交给老师检查的版本。3.1 第一题函数封装与列表推导式第一题要求写一个函数过滤掉长度不够的单词。我用列表推导式一行搞定核心逻辑但函数边界条件要写完整def filter_words(word_list, min_len): 返回列表中所有长度不小于min_len的单词保持原有顺序。 参数: word_list: 原始单词列表 min_len: 最小长度阈值 返回: 过滤后的新列表 if not isinstance(word_list, list): raise TypeError(word_list必须是列表) if min_len 0: raise ValueError(min_len不能为负数) return [word for word in word_list if len(str(word)) min_len] if __name__ __main__: words [python, java, go, rust, c, javascript] print(filter_words(words, 3))这里有两个值得注意的细节。第一我用str(word)先做一次类型转换是因为列表里万一混入数字比如123它的长度不是3而是4不对len根本不能直接作用于整数会报TypeError。所以先转成字符串哪怕转出来的结果不是预期的至少程序不会崩。第二我主动抛了TypeError和ValueError这是课程前面没有重点强调的但实际开发中很常用。运行结果[python, java, rust, javascript]go长度是2被过滤c长度是1被过滤符合预期。3.2 第二题文件读写与统计数据第二题是我花时间最多的一道。先构造一份scores.txt内容如下张三,78 李四,85 王五,60 赵六,45 钱七,92题目要求统计平均分、最高分、不及格人数并追加到文件末尾。我第一版写的代码是这样的def load_scores(filepath): 从文件中读取成绩返回姓名列表和成绩列表。 names [] scores [] with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue name, score_str line.split(,) names.append(name) scores.append(int(score_str)) return names, scores def compute_stats(scores): 输入成绩列表返回(平均分, 最高分, 不及格人数)。 avg sum(scores) / len(scores) max_score max(scores) failed sum(1 for s in scores if s 60) return avg, max_score, failed def append_stats(filepath, avg, max_score, failed): 把统计结果追加到文件末尾。 with open(filepath, a, encodingutf-8) as f: f.write(\n统计结果\n) f.write(f平均分: {avg:.1f}\n) f.write(f最高分: {max_score}\n) f.write(f不及格人数: {failed}\n) if __name__ __main__: scores_file scores.txt _, scores_list load_scores(scores_file) avg, max_score, failed_count compute_stats(scores_list) append_stats(scores_file, avg, max_score, failed_count) print(f平均分 {avg:.1f}, 最高分 {max_score}, 不及格 {failed_count} 人)这段代码有几个细节值得讲。line.strip()去掉的是行尾的换行符\n如果不去掉split(,)之后成绩字符串可能带着\rWindows系统导致int()转换时报错。if not line: continue是为了跳过文件里的空行。写入时用a模式代表追加而不是覆盖原文件。运行结果平均分 72.0, 最高分 92, 不及格 1 人scores.txt文件末尾追加了统计结果文件内容变成张三,78 李四,85 王五,60 赵六,45 钱七,92 统计结果 平均分: 72.0 最高分: 92 不及格人数: 1一个容易忽略的点是如果scores.txt不存在open用r模式会直接报FileNotFoundError。这个问题我在第4章的踩坑部分详细讲。3.3 第三题BeautifulSoup解析HTML链接第三题给了一段HTML文本要求用BeautifulSoup提取所有a标签的href和文本。题目给的HTML片段像这样html_doc htmlbody h1测试页面/h1 a hrefhttps://example.com/1第一个链接/a a hrefhttps://example.com/2第二个链接/a a classnav hrefhttps://example.com/3导航链接/a /body/html 我的实现分两步先解析再遍历from bs4 import BeautifulSoup def extract_links(html_text): 从HTML文本中提取所有a标签的href和文本。 soup BeautifulSoup(html_text, html.parser) results [] for a_tag in soup.find_all(a): href a_tag.get(href) text a_tag.get_text(stripTrue) if href: results.append(f{text} - {href}) return results if __name__ __main__: html_doc 省略与上面一致 for line in extract_links(html_doc): print(line)两个细节第一find_all(a)不带任何其他条件会把HTML里所有a标签都找出来。如果只想找特定class的可以传class_nav。第二get_text(stripTrue)能自动去掉标签内部的换行和空格这是我在处理真实网页时学到的不传strip经常会在文本前后带一堆空白字符。运行结果第一个链接 - https://example.com/1 第二个链接 - https://example.com/2 导航链接 - https://example.com/3第三题本身不难但它是后面第四题的铺垫——第四题不只是解析HTML而是请求一个真实的远程接口再解析JSON。3.4 第四题requests请求JSON接口并筛选数据第四题我用的公开接口是https://jsonplaceholder.typicode.com/posts这是个专门供开发者测试的假数据接口返回一长串JSON数组每条记录包含userId、id、title、body四个字段。题目要求是筛选出userId等于某个特定值的记录并输出id和title。import requests def fetch_posts(url): 请求接口并返回JSON解析后的数据。 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() return resp.json() def filter_by_user(posts, user_id): 从帖子列表中筛选出指定userId的记录。 return [post for post in posts if post[userId] user_id] if __name__ __main__: url https://jsonplaceholder.typicode.com/posts all_posts fetch_posts(url) filtered filter_by_user(all_posts, 1) for post in filtered[:3]: print(f#{post[id]}: {post[title]})这一步我一开始没加headers结果返回的状态码是403服务器拒绝访问。原因很简单很多接口会检查请求头里的User-Agent如果发现是Python的默认标识会视为爬虫直接拒绝。加上一个常见的浏览器User-Agent就正常了。这个问题专门放在第4章讲因为它是网络编程新手最容易踩的坑。运行结果#1: sunt aut facere repellat provident occaecati excepturi optio reprehenderit #2: qui est esse #3: ea molestias quasi exercitationem repellat qui完整数据是100条我只打印了前3条验证结果。注意resp.raise_for_status()这行它的作用是当HTTP状态码不是200时主动抛出异常避免后续用空数据继续运算。3.5 第五题递归斐波那契对比循环实现选做题要求用递归实现斐波那契数列并统计调用次数同时和循环版做对比。这个题很有意思因为递归版本一旦n稍微大一点运行时间的差距会非常直观。def fib_recursive(n, call_countNone): 递归版斐波那契返回(结果, 调用次数)。 if call_count is None: call_count [0] call_count[0] 1 if n 1: return n, call_count[0] # 注意这里拆成两行调用避免递归时重置计数 r1, _ fib_recursive(n - 1, call_count) r2, _ fib_recursive(n - 2, call_count) return r1 r2, call_count[0] def fib_loop(n): 循环版斐波那契返回(结果, 循环次数)。 a, b 0, 1 count 0 for _ in range(n): a, b b, a b count 1 return a, count if __name__ __main__: n 30 res_r, calls fib_recursive(n) res_l, loops fib_loop(n) print(fn{n} 递归结果: {res_r}, 调用次数: {calls}) print(fn{n} 循环结果: {res_l}, 循环次数: {loops})这里有一个非常隐蔽的坑我用了一个列表call_count来计数而不是整数变量因为整数在函数内部是值传递递归返回后计数会丢失。而列表是引用传递所有递归层共享同一个计数器。我专门用call_count[0]这种写法就是为了避免UnboundLocalError。运行结果n30 递归结果: 832040, 调用次数: 1664079 n30 循环结果: 832040, 循环次数: 30结果一出来差距一目了然。递归算n30要调用166万次函数循环只用30次。这个直观对比比老师讲十遍递归效率低都管用。不过话说回来递归的优势是代码简洁、思路清晰理解它对于后面学习树、图、分治算法仍然很重要不能因为效率低就不学。4. 调试过程实录那些没写进答案的坑如果说前面这部分是标准答案那这部分就是真实战场。我写作业的过程中遇到了四个问题每一个都花了不少时间定位。把这些记录下来的价值不亚于代码本身。4.1 中文编码UnicodeDecodeError第一次运行就翻车第二题我第一次运行就报错UnicodeDecodeError: gbk codec cant decode byte 0x9a in position 12: illegal multibyte sequence当时我的scores.txt是UTF-8编码而Windows系统上Python的文件默认编码是GBK。open函数如果不传encoding参数在Windows上会用系统默认编码去解码读到UTF-8的中文字符就直接崩了。解决办法就是在open里显式指定编码with open(filepath, r, encodingutf-8) as f:这个坑几乎所有Windows用户都会踩。Linux和macOS系统的默认编码是UTF-8所以不会报错这也是很多教程没强调这个参数的原因。我的建议是只要处理文本文件一律显式写encodingutf-8养成习惯跨平台才不会出问题。4.2 爬虫吃闭门羹缺少User-Agent请求头第四题第一次请求时resp.status_code返回的是403不是200。我当时有点懵明明地址在浏览器里能打开怎么代码里就403后来排查发现requests库默认的User-Agent是python-requests/2.x.x很多服务器见到这个直接拒绝。浏览器正常访问时带的User-Agent是一长串包含浏览器版本、操作系统信息等。解决方法是构造headers参数伪装成浏览器headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } resp requests.get(url, headersheaders, timeout10)加上之后状态码变成200数据正常返回。这里顺便说明一下jsonplaceholder这种测试接口其实不校验请求头但我在练习时故意模拟了真实场景。如果你做爬虫作业时也遇到403、418这些状态码第一反应就该去检查请求头。另外timeout10一定要设不设的话如果服务器不响应程序会一直挂在那里非常难受。4.3 路径里的反斜杠转义问题文件找不到的另一个原因有一段时间我写了一个读取同目录文件的小测试路径写成with open(D:\python\test\scores.txt, r, encodingutf-8) as f:结果报错SyntaxError: (unicode error) unicodeescape codec cant decode bytes in position 2-3原因是\t被解析成了制表符\s被解析成了转义字符路径整个废了。三种解决办法任选其一# 方式一用正斜杠 path D:/python/test/scores.txt # 方式二用原始字符串 path rD:\python\test\scores.txt # 方式三用双反斜杠 path D:\\python\\test\\scores.txt我建议尽量用方式一正斜杠在Windows和Linux都能用跨平台最安全。这个坑本质上不是Python特有而是反斜杠在字符串里有特殊含义导致的属于新手高频错误。4.4 递归计数器总归零可变对象作计数器的技巧第五题统计递归调用次数时我最开始写的是count 0 def fib(n): global count count 1 ...虽然能跑但用了全局变量一旦在同一个脚本里连续调用多次fibcount不会自动归零结果会累积。后来改用列表call_count [0]作为计数器传进函数问题就解决了。原理是整数是不可变类型函数内count 1实际是创建一个新整数并绑定到局部变量除非声明global否则不会影响外层变量而列表是可变对象在任意一层递归中修改call_count[0]所有层都能看到最新值。这个技巧在处理递归过程中需要记录某种状态的场景非常常用比如统计递归深度、跟踪访问路径等。比全局变量干净得多也比定义一个大字典简单。5. 提交前的自查从能跑到像样作业写完能跑出正确结果只能算完成了一半。老师批改作业时给的评语不会只看结果对不对还会看代码风格、可读性和健壮性。我提交前做了一轮自查流程大概半小时这里分享几个核心动作。5.1 用pylint做静态检查我先用pylint扫描了一遍全部代码装上之后在虚拟环境里执行pip install pylint pylint homework5.py扫描结果告诉我两个问题line too long (85/79)一行代码超过了79个字符这是PEP8的规范。解决办法是把长的表达式拆成多行或者在运算符后换行。missing-final-newline文件末尾没有换行符。解决办法很简单编辑器里在最后一行按个回车。静态检查虽然不能发现所有问题但能规范格式、查漏补缺。不过我后来实际用过一遍发现pylint很多提示是风格建议比如变量名用了单字母、缺失docstring之类不用全部照单全收它更像一个提示器哪些改哪些不改由你判断。5.2 把重复代码抽成函数第一次答题时第二题和第五题都各自写了一段打印统计信息的代码有重复。提交前我把它抽成公共函数def show_report(title, content): 打印一条带标题的报告信息。 print(f--- {title} ---) print(content)你可能觉得就重复了两三次没必要抽吧。但作业的意义在于训练习惯——一段代码如果在同一个文件里出现了两遍就应该考虑抽取。后面做项目时这种复制粘贴的代码会越来越多现在养成随手抽象的习惯成本最低。5.3 用列表推导式替代显式循环第一题我用for循环加append写了一个版本result [] for word in word_list: if len(word) min_len: result.append(word)然后改成列表推导式result [word for word in word_list if len(word) min_len]两行变一行不仅代码更短运行速度也更快。这里有个教训是不要为了炫技硬用列表推导式。如果逻辑复杂到一眼看不明白老老实实写for循环更好。列表推导式适合简单的循环简单的条件这种场景。5.4 给每个函数补docstring和类型提示给代码加docstring这个习惯开始会觉得麻烦但看代码的人包括未来的自己会说感谢。我的注释不算啰嗦每个函数三行就够功能说明、参数说明、返回值说明。Python 3.11还支持类型提示我也顺手加上def filter_words(word_list: list[str], min_len: int) - list[str]: ...这个不算强制要求但好处是代码的可读性大幅提升IDE的自动补全也能正常工作。老师不一定会看这个但对自己长期有帮助。6. 作业之外可以沉淀的两个代码骨架第五次作业做完之后我觉得最有价值的不是分数而是可以从这次作业里抽出两个代码骨架——下次遇到类似任务直接改改就能用。这里分享出来给正在赶Python作业的同学一个参考。6.1 通用的文件处理骨架几乎所有的文件读写作业都可以套用这个结构def read_file_to_lines(filepath: str) - list[str]: 读取文本文件返回去除空行和换行符后的行列表。 with open(filepath, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] return lines def process_lines(lines: list[str]) - list[str]: 核心处理逻辑子类继承或替换。 return [line.upper() for line in lines] def write_lines_to_file(filepath: str, lines: list[str], mode: str w) - None: 将行列表写入文件。mode为w表示覆盖a表示追加。 with open(filepath, mode, encodingutf-8) as f: for line in lines: f.write(line \n) if __name__ __main__: input_file input.txt output_file output.txt data read_file_to_lines(input_file) result process_lines(data) write_lines_to_file(output_file, result)这个骨架的核心思想是读取-处理-写出三段式。不管题目是统计成绩、过滤单词、还是格式转换你只需要改中间的process_lines函数其他部分基本不用动。6.2 通用的爬虫作业模板如果作业里要求爬取网页或调用接口下面这个模板足够应付大多数情况import requests from bs4 import BeautifulSoup def fetch_page(url: str, **kwargs) - str: 请求URL并返回HTML文本。 headers kwargs.get(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 resp.apparent_encoding return resp.text def parse_html(html: str, selector: str) - list[tuple[str, str]]: 根据CSS选择器解析HTML返回(文本, href)元组列表。 soup BeautifulSoup(html, html.parser) return [(a.get_text(stripTrue), a.get(href)) for a in soup.select(selector) if a.get(href)] if __name__ __main__: url https://example.com html_content fetch_page(url) links parse_html(html_content, a) for text, href in links[:5]: print(f{text} - {href})模板里有一个容易忽略的细节resp.encoding resp.apparent_encoding。有的网站没有在响应头里声明编码requests会用默认的ISO-8859-1去解码结果中文全是乱码。apparent_encoding是从内容本身推测编码能解决这个问题但代价是速度稍慢。如果知道目标网站的编码直接指定更可靠。6.3 给零基础同学的三条实在建议最后说几句掏心窝的话都是我自己从零基础一路走过来的体会。第一作业一定要自己动手打一遍代码不要复制粘贴。复制粘贴看起来省事但你会错过很多亲手把代码敲出来时才能发现的细节——比如括号不匹配、缩进错误、中英文标点混用。这些错误处理过一次之后印象极其深刻。第二报错信息是你的朋友不是敌人。我见过太多同学一看到红字就慌了其实只要从上往下读第一行英文90%的错误都能找到线索。英文不好就复制到翻译工具里久而久之你就能看懂常见的报错类型。第三每天写代码的时间不用太长但尽量连续。零基础学Python最难的是高频词的熟悉度和语感。一周只在周末学一次下次上课基本上忘光了每天坚持三四十分钟反而比周末突击一整天效果好得多。这次第五次作业是我觉得课程进入干货区的开始也是从学语言到用语言的转折点。上面写的这些内容有一部分是老师讲过的但更多是我在跑代码过程中自己撞出来的。如果你现在正在写这门课的某个作业希望能对你有帮助。