news 2026/9/16 16:39:40

Python爬虫大作业全攻略:从静态页面到动态渲染的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫大作业全攻略:从静态页面到动态渲染的完整实现

简介:介绍一下这份资源:这是2020-2021学年上学期Python大作业——爬虫项目,适合需要完成类似课设或想学习爬虫与GUI结合开发的Python初学者参考。项目以爬取古诗词名句网为目标,模拟了网站的7种搜索方式,并基于PyQt5制作了桌面界面,运行main.py即可弹出操作窗口,点击保存数据会生成相应文件,在作者模式下还能生成词云图,功能较为完整。资源包共6个文件,以Python脚本为主(3个py文件),另含1个界面ui文件、1个Markdown说明文档与1个停用词txt文件,整体仅9KB,结构紧凑,非常适合快速阅读和二次修改。目前已有9478人学习下载,热度较高。通过这份资料,读者可以拿到一份可直接运行的爬虫大作业源码,了解requests解析、多搜索模式切换、PyQt5界面搭建以及词云生成等关键知识点,尤其适合期末赶工或入门实战。

1. 大作业不是“能跑就行”,而是“能讲清楚”

每年毕业季或学期末,CS 专业的学生都会遇到一类“看起来简单,交上去被挑刺”的作业:用 Python 写一个爬虫。很多人以为爬虫大作业就是把 requests.get() 和 BeautifulSoup 拼在一起,跑通就完事。但真正的评分点往往藏在你看不见的地方——异常处理是否完整、请求头是否仿真、分页逻辑是否健壮、数据是否落地、以及老师现场改一个 URL 或加一个字段时你的代码能不能撑住。这篇文章不是给你一份“直接交”的代码包,而是把大作业里最常见的三道坎(静态页面、动态渲染、登录态)的解题思路和可复用代码讲透,让你既能跑通,也能在答辩时把每个参数为什么这么设说清楚。适合正在做课设、实训或毕业设计爬虫模块的同学,也适合想把爬虫基础打得扎实一点的从业者。

2. 爬虫大作业的“地基”:requests + BeautifulSoup 的组合逻辑

2.1 先搞清楚你的目标网页是“静态”还是“动态”

大作业选题里,最容易翻车的就是拿到一个网页就开爬,结果发现解析出来的列表是空的。原因很简单:你看到的页面内容有两种来源。一种是在 HTML 源码里直接存在的静态内容,另一种是浏览器通过 JavaScript 异步加载后动态渲染出来的内容。区分方法很简单:在浏览器里右键查看网页源代码(不是检查元素),Ctrl+F 搜你目标数据里的一个独特关键词。如果源代码里能搜到,就是静态页面,requests 直接可以处理;如果搜不到,说明数据是 AJAX 请求加载的,你需要找到那个真正返回数据的接口。这个判断步骤决定你后面用什么策略,值得在答辩时主动讲出来,是加分项。

2.2 最小可用的爬虫骨架:从 URL 到结构化数据

一个能应付大作业的爬虫,最少要包含四个部分组成:请求头伪装、会话保持、解析提取、异常兜底。下面这个例子针对一个静态列表页(比如某个公开的新闻列表),抓取标题和链接。

import requests from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_list_page(url): try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() # 状态码不是 2xx 时抛出异常 resp.encoding = resp.apparent_encoding # 让编码自动匹配 return resp.text except requests.RequestException as e: print(f"请求失败:{e}") return None def parse_list(html): soup = BeautifulSoup(html, "html.parser") items = [] for li in soup.select("ul.news-list li"): # 换成目标页面的实际选择器 title_tag = li.find("a") if title_tag: items.append({ "title": title_tag.get_text(strip=True), "href": title_tag.get("href"), }) return items if __name__ == "__main__": html = fetch_list_page("https://example.com/news") if html: for idx, item in enumerate(parse_list(html), 1): print(idx, item["title"], item["href"])

这段代码里的关键参数值得反复琢磨。timeout=10不是随便写的,它防止程序因为某个响应卡死而无限等待,大作业的 URL 通常不止一页,没有超时控制会导致整个爬虫挂在某个坏链接上。resp.encoding = resp.apparent_encoding是为了解决中文乱码问题,因为很多教务系统或新闻站的页面没有声明 charset,或者声明与实际不符。raise_for_status()的作用是快速失败,避免把 404 页面当正常页面解析。选择器.select("ul.news-list li")是 CSS 选择器写法,比find_all更简洁,也更容易在答辩时解释。

2.3 数据落地:CSV 还是 JSON,怎么选

大作业要求“保存数据”时,很多同学直接 print 到控制台就算完事。正确的做法是把数据落盘,常见格式是 CSV 和 JSON。CSV 适合表格型数据,Excel 能直接打开;JSON 保留嵌套结构,更适合后续二次处理。抓取结果不确定时,JSON 更安全——字段缺失时不会像 CSV 那样出现列错位。下面这段代码把列表数据存成 JSON 文件:

import json def save_to_json(items, filename="output.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(items, f, ensure_ascii=False, indent=2)

ensure_ascii=False让中文以明文形式写入文件,而不是转成\uXXXX的转义序列,这点不写的话,老师打开 JSON 看到的全是一堆斜杠编码,印象分直接打折。

3. 动态页面的处理:requests 拿不到的,交给 Selenium 或逆向接口

3.1 何为动态渲染:了解一下为什么会拿不到数据

静态页面的思路适用于很多课程网站、新闻门户和公开数据集。但大作业一旦选了带有筛选条件或实时刷新的站点(比如商品列表、招聘岗位、社交平台话题页),requests 直接请求往往一无所获。因为服务器返回的 HTML 只是一个空壳,真正的内容由浏览器里的 JavaScript 脚本发起异步请求后填充。此时摆在你面前有两条路:一是用 Selenium 或 Playwright 模拟浏览器操作,二是用抓包工具找到接口地址。两者的取舍在于:接口逆向性能好、代码轻,但遇到加密参数时需要花时间分析 JS;Selenium 简单粗暴、稳定,但启动浏览器吃内存,循环翻页时容易慢。大多数大作业的场景下,用 Selenium 先做出功能,再用接口重写第二版,是能体现技术深度的做法。

3.2 Selenium 方案:一个能“自动驾驶”的浏览器

Selenium 3.x 或 4.x 的核心思路是让浏览器按照你的脚本去加载页面、等待渲染、提取内容。大作业选这条路时,最常踩的坑是元素还没加载出来就去抓,结果抓到空列表。解决办法用显式等待,别用time.sleep()硬等。下面是抓取动态加载列表页的示例:

from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.options import Options chrome_options = Options() chrome_options.add_argument("--headless=new") # 无头模式,不弹出浏览器窗口 chrome_options.add_argument("--disable-gpu") chrome_options.add_argument("--no-sandbox") driver = webdriver.Chrome(options=chrome_options) try: driver.get("https://example.com/dynamic-list") # 显式等待最多 10 秒,直到目标元素出现在 DOM 中 wait = WebDriverWait(driver, 10) items = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.job-item"))) for item in items: title = item.find_element(By.CSS_SELECTOR, "a.job-title").text company = item.find_element(By.CSS_SELECTOR, "span.company-name").text print(title, company) finally: driver.quit()

这个方案里最有技术含量的是EC.presence_of_all_elements_located,它告诉 WebDriver:“等页面里出现至少一个匹配该选择器的元素再继续”。放在大作业答辩里,你要能解释为什么不用driver.find_elements立刻抓——因为那时候元素还没生成。另一个细节是--headless=new,这是 Chrome 109 之后的新无头模式,兼容性和性能都比老的 headless 好。如果实验室电脑上 Selenium 报WebDriverException,优先检查 Chrome 版本和 chromedriver 版本是否匹配。

3.3 接口逆向思路:找到数据真正的“源头”

Selenium 方案虽然稳,但是翻页 20 次之后速度会明显变慢。如果你愿意多花一小时,用 Chrome 开发者工具里的 Network 面板,刷新页面,找到 XHR 类型中返回 JSON 数据的请求,模拟它才是更“正规军”的做法。接口通常长这样:

GET https://api.example.com/jobs?page=1&size=20

只要在请求头里带上RefererUser-Agent,用 requests 就能拿到干净的数据。对付一些加了基础加密参数的网站,常见的做法是全局搜索混淆代码里的“sign”或“token”,看看它是怎么拼出来的。通常你会发现它就是把几个参数排序后做了 MD5 或 SHA1。如果能讲清楚这个过程,大作业的深度已经超出大部分同学了。如果你在答辩时被问到“接口挂了怎么办”,可以答“接口参数加密升级时,我会降级到 Selenium 方案作为兜底”——这个回答几乎是满分。

4. 大作业必考的三个实战场景:分页、登录、频率控制

4.1 分页爬取:别写死页码,要用循环 + 终止条件

分页是大作业里最容易被老师追问的部分。很多同学写for i in range(1, 10)把前 10 页抓下来就交差了。但老师往往喜欢现场把目标 URL 换成一个总页数不同的站点,这时候你的代码就露馅了。正确的思路是不预设页数,用“当前页是否还有数据”来判断是否继续。常见做法是设置一个最大页数上限(避免死循环),但当某页解析出来为空列表时立即跳出。下面是用 requests 方案实现的分页循环:

def crawl_pages(base_url, max_pages=50): all_items = [] for page in range(1, max_pages + 1): url = base_url.format(page=page) html = fetch_list_page(url) if not html: break items = parse_list(html) if not items: print(f"第 {page} 页无数据,停止翻页") break all_items.extend(items) time.sleep(1) # 控制请求间隔,避免给服务器造成压力 return all_items

这段代码里base_url.format(page=page)要求你传入的 base_url 形如https://example.com/list?page={page}break的语义是“数据断层即停止”,这比固定页数更稳健。特别强调time.sleep(1)——这不是为了慢,而是给服务器台阶下。大作业评分标准里有一条隐性的要求:不要给目标站点制造访问压力。如果你交上来的代码没有请求间隔,老师只会在评语里写“稳”,但背地里会直接扣分。

4.2 登录后才能看的内容:Session 与 Cookie 的配合

很多“有点东西”的网站,数据要登录才能看到。大作业遇到这种需求时,你需要把“用户名密码登录”这个动作模拟出来。最稳妥的做法是带着 Session 先 POST 登录接口,再维持同一个 Session 去 GET 目标数据页。用 requests 的话是这样的:

login_data = { "username": "your_account", "password": "your_password" } session = requests.Session() session.headers.update(headers) # 先 GET 登录页,拿到必要的 Cookie(很多站点会先种一个 sessionid) session.get("https://example.com/login", timeout=10) # POST 登录凭据 login_resp = session.post("https://example.com/login/action", data=login_data, timeout=10) if login_resp.status_code == 200 and "登录成功" in login_resp.text: # 此时 session 已经持有了登录态的 Cookie protected_page = session.get("https://example.com/profile", timeout=10) print(protected_page.text[:200]) else: print("登录失败,请检查账号密码或验证码策略")

核心逻辑在于requests.Session会自动管理 Cookie,你不需要手动把浏览器的 Cookie 字符串复制到代码里。但要注意:有些站点登录时有校验Referer或者隐藏字段csrfmiddlewaretoken,你需要先从登录页的 HTML 里把它取出来再带上。session.get登录页的意义就在于此——它是为了拿到动态生成的 token。答辩时这段逻辑值得展开讲,因为这体现的是对 HTTP 状态管理的理解,而不是单纯会用函数。

4.3 频率控制与反爬:headers、代理与重试策略

大作业级别的爬虫,遇到 403 或 429 状态码很常见,这是服务器在提醒你“请求太快”或“你不是浏览器”。最常见的规避方案有三个:加强 Headers 使其更像真实浏览器;设置重试机制应对偶发 429;必要时通过代理池切换 IP。但大作业里不建议你引入复杂的代理池——这会让项目变得不可控,而且很多免费代理本身就不稳定。更好的做法是把重试做好。比如用requests.adapters.HTTPAdapter来配置重试策略:

from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry_strategy = Retry( total=3, status_forcelist=[429, 500, 502, 503], allowed_methods=["GET"], backoff_factor=1 ) adapter = HTTPAdapter(max_retries=retry_strategy) session = requests.Session() session.mount("https://", adapter) session.mount("http://", adapter)

total=3表示最多重试三次。status_forcelist指定哪些状态码触发重试,429 和 5xx 都是服务端问题,值得重试,但 404 不应该重试,因为那是客户端请求地址的问题。backoff_factor=1的意思是第一次重试前等待 1 秒,第二次 2 秒,第三次 4 秒,指数退避的节奏可以让服务器的限流窗口慢慢恢复。这段代码在答辩时拿出来讲,老师会认为你理解反爬不只是伪造 User-Agent。你也可以做成一个 Excel 表格放在报告里,列清楚哪些状态码重试、哪些状态码放弃,这种工程素养非常加分。

5. 数据清洗与结构化存储:让大作业从“能爬”升到“能用”

5.1 用 pandas 把抓下来的“半成品”变成干净表格

爬虫大作业交上去,数据一股脑存进 JSON 或 CSV 还不够。老师大概率会抽查一条数据,看字段是否完整、格式是否统一。抓下来的数据里经常出现的问题有:字符串首尾带空格、发布时间格式不统一、金额字段混入了货币符号、缺失字段直接缺席。这些问题的处理思路就是清洗。用 pandas 处理它们非常高效。

比如你抓到了一个包含图书信息的列表,字段有titlepricepublish_daterating,典型的清洗步骤是这样的:

import pandas as pd df = pd.DataFrame(raw_items) df["title"] = df["title"].str.strip() # 把 "¥89.90" 或 "¥89.90" 统一转成浮点数 df["price"] = ( df["price"].str.replace(r"[¥¥]", "", regex=True) .str.strip() .astype(float) ) # 统一日期格式:'2024年3月15日' → '2024-03-15' df["publish_date"] = pd.to_datetime(df["publish_date"], format="%Y年%m月%d日").dt.strftime("%Y-%m-%d") # 处理缺失评分:没有评分则用 0 填充 df["rating"] = df["rating"].fillna(0) df.to_csv("books_cleaned.csv", index=False, encoding="utf-8-sig")

有几个参数值得在报告里写清楚。str.replace(r"[¥¥]", "", regex=True)用的正则表达式同时匹配全角和半角货币符号。pd.to_datetime(..., format=...)指定了解析格式,避免 pandas 猜错日期顺序。encoding="utf-8-sig"utf-8多了一个 BOM 头,这样生成的 CSV 用 Excel 打开时中文不会乱码。这个细节很多实训报告都没写,属于你能拿出来讲的“增量知识点”。

5.2 结构化存储:什么时候用 SQLite

如果抓取量达到了上千条,且老师要求做数据统计或筛选,那就不应该只停留在 CSV 层面。用 SQLite 不用安装额外服务,一个.db文件就能搞定。Python 标准库自带sqlite3,大作业里用它来存储和查询非常合适。下面这段代码把清洗后的数据写入 SQLite:

import sqlite3 conn = sqlite3.connect("books.db") df.to_sql("books", conn, if_exists="replace", index=False) # 一个小查询示例:统计每个评分档位的图书数量 query = """ SELECT rating, COUNT(*) AS cnt FROM books GROUP BY rating ORDER BY cnt DESC """ stats = pd.read_sql_query(query, conn) print(stats) conn.close()

if_exists="replace"会让每次运行都重建表,适合开发调试阶段,但如果你要在同一批数据上反复追加,应该改成if_exists="append"。这里值得留意的是pd.to_sql依赖于 pandas 内置的 SQL 写入逻辑,字段名会自动映射成表的列名,省去了手写CREATE TABLE的繁琐。答辩时你可以强调,选择 SQLite 是因为它零配置、单文件、容易打包提交,而 MySQL 需要额外环境,评审老师未必愿意在你的机器上装。这种选型理由比“因为我会用”更有说服力。

5.3 去重:避免数据翻倍的小技巧

分页爬取和多轮调试最容易遇到的坑是数据重复。比如你调试时跑了两次脚本,第二次会把同一批数据再抓一遍。大作业里处理去重最常见的方式是按某个唯一字段做指纹,比如新闻链接或者商品 ID。你可以维护一个集合,每抓到一个新数据先判断 URL 是否已经见过。

seen_urls = set() unique_items = [] for item in items: if item["href"] in seen_urls: continue seen_urls.add(item["href"]) unique_items.append(item)

这段逻辑的核心是用空间换时间:set的查找是 O(1),比列表的in判断快得多。如果数据量上万且 URL 特别长,你可以只存储它的 MD5 值来节省内存,但大作业层面没必要做这个优化。真正的意义在于让老师看到你有“重复数据会拉低数据质量”的意识。

6. 验收前的自查:让大作业从“能跑”到“挑不出毛病”

大作业的评分往往分两层:第一层是能不能跑,第二层是跑得对不对。很多同学第一层能过,第二层翻车。这里给你一组自查清单,按顺序过一遍,比临时改 bug 靠谱得多。

先检查代码里有没有写死的绝对路径。很多同学在自己电脑上把输出文件写到C:\Users\自己的名字\Desktop\...,交到老师那里或换一台机器就报错。正确做法是使用相对路径或者os.path.join(os.path.dirname(__file__), "data.csv")这种方式,确保换机器不用改代码。再检查你的 User-Agent 是否带上了操作系统标识。一个不完整的 UA 可能会在答辩现场被反爬拦截,而你在自己电脑上因为有浏览器缓存所以没察觉。

接下来验证编码问题。所有open()函数中的encoding参数是否显式声明为utf-8?所有to_csv是否指定了utf-8-sig?所有 print 输出中文字符时终端会不会乱码?乱码不是功能bug,但在答辩演示中非常打眼。如果时间充裕,再检查一下你的异常处理是否覆盖了网络超时、JSON 解析失败、字段缺失三种情况。只需要三个try-except,别多写,代码保持清爽。

还有一个容易被忽略的点:爬虫脚本是否把请求间隔放在了循环内部而不是外部。放在了外部意味着每次请求之间都有间隔,放在了循环外则是一次循环结束才休息,导致某些页面的请求间隔会突然变长。虽然不是大问题,但答辩时如果有老师让你指着一行代码解释“这里为什么 sleep(1)”,你要能说清楚它的位置意义。

跑完一遍后,打开你的输出文件,随机挑三条数据,对着目标网站手工核验。标题是否一致,链接是否有效,日期是否对得上。如果抽查三条数据中有任何一条和网页对不上,立刻顺着代码流程排查,不要以为只是偶然。这一步是真正的底线保障——老师大概率也会随机抽查,他的抽查跟你做的抽查没有区别。

最后给你的代码加上一个简单的命令行入口,让使用者可以用一句话启动程序:

python main.py --pages 10 --output data.json

用标准库argparse实现,不需要任何第三方库。这样不管是谁拿到项目,都能快速上手运行。一个“完美应付大作业”的爬虫项目,本质上是让任何人拿过来,不需要阅读你的任何说明就能跑出结果,同时你还能把自己写的每一个参数、每一个选择器的理由讲明白。做到这点,你的大作业就不只是应付评分的工具,而是你真的掌握了一部分工程能力的有力证明。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:39:10

SSM+微信小程序物业系统实战:从数据库建模到前后端数据同步

简介:这是一套基于SSM(SpringSpring MVCMyBatis)与微信小程序双端协同的物业管理系统实战项目,面向Java初学者及全栈开发入门者,解决社区服务数字化场景下的公告管理、报修响应、信息采集、生活缴费与二手置换等核心需…

作者头像 李华
网站建设 2026/9/16 16:37:36

微信小程序服务端开发实战:登录态与鉴权全解析

简介:适合微信小程序初学者与服务端开发者,这是一份可直接运行的服务端开发示例,演示了后端接口的基础写法与静态资源托管逻辑。资源包共11个文件,以6个JavaScript源码文件为主,另含依赖清单、转译配置、说明文档及测试…

作者头像 李华
网站建设 2026/9/16 16:37:18

光纤FP干涉仪COMSOL建模与优化实践

1. 光纤FP干涉仪基础与COMSOL建模价值光纤FP干涉仪作为高精度光学测量的核心器件,其原理源于多光束干涉效应。在实际工程应用中,我们需要精确控制干涉条纹的间距和对比度来满足不同场景的测量需求。传统实验室调试方法耗时耗力,而COMSOL Mult…

作者头像 李华
网站建设 2026/9/16 16:37:08

Kibana入门实操:从日志检索到可视化大盘的完整指南

很多人第一次接触Kibana,是团队里突然多了一套ELK,运维扔过来一个网址,端口5601。打开页面,满屏图表,左侧一圈菜单,第一反应大概率是:这不就是个日志查看器?但如果你只把它当日志界面…

作者头像 李华
网站建设 2026/9/16 16:36:49

DIYGWUI:可视化生成UNIAPP与小程序可上线源码

简介:这是一款面向设计师与前端开发者的零代码可视化开发工具,基于DIYGWUI框架,专为快速构建微信小程序、H5页面及UNIAPP应用而设计,无需编程基础即可通过拖拽完成界面搭建与逻辑配置。资源包共1516个文件,总大小12.04…

作者头像 李华
网站建设 2026/9/16 16:36:38

STM32F030F4P6 TIM1 PWM稳定输出三大硬件陷阱与寄存器级配置

简介:本资源是面向嵌入式初学者与STM32开发者的STM32F030F4P6微控制器PWM功能实战例程包,聚焦低功耗Cortex-M0芯片的脉宽调制应用,解决电机控制、LED调光及DHT11传感器协同驱动等典型场景中的定时器配置与占空比调节难题。压缩包共149个文件&…

作者头像 李华