数据采集第一次作业
数据采集第一次作业
网络爬虫第一次作业:requests + BeautifulSoup / re 定向爬虫
代码仓库:作业1
实验环境:
| 项目 | 说明 |
|---|---|
| 操作系统 | Windows 11(64 位) |
| 编程语言 | Python 3.10 |
| 开发工具 | VS Code |
| 浏览器 | Chrome(含开发者工具 F12) |
| 第三方库 | requests、beautifulsoup4、lxml、tabulate、urllib3 |
| 库安装命令 | pip install requests beautifulsoup4 lxml tabulate urllib3 |
本次作业共三个小作业,分别练习 BeautifulSoup 解析网页表格、正则表达式提取商城商品信息、以及批量下载网页图片。
作业① 爬取软科 2020 中国大学排名
1)大学排名爬虫实验
实验目标:用 requests 库请求网页,用 BeautifulSoup 库解析 HTML,定向爬取软科 2020 中国大学排名(https://www.shanghairanking.cn/rankings/bcur/2020 ),输出排名、学校名称、省市、学校类型、总分。
数据来源说明:
软科排名页面的数据并非静态写死在 HTML 中,而是通过其公开接口以 JSON 形式返回。接口地址为:
GET https://www.shanghairanking.cn/api/pub/v1/bcur?bcur_type=11&year=2020
其中:
bcur_type=11表示"中国大学排名(主榜)";year=2020表示目标年份。
接口返回的 JSON 包含 ranking(排名)、univNameCn(中文校名)、province(省市)、univCategory(学校类型)、score(总分)等字段。因此本实验采用"抓接口拿 JSON + BeautifulSoup 清洗文本 + json 解析"的思路来获取数据。
完整代码:
"""
作业①:用 requests + BeautifulSoup 爬取 2020 软科中国大学排名(主榜)
输出:排名、学校名称、省市、学校类型、总分
"""
import requests
import json
from bs4 import BeautifulSoup
from tabulate import tabulate
API_URL = "https://www.shanghairanking.cn/api/pub/v1/bcur"
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"
),
"Referer": "https://www.shanghairanking.cn/rankings/bcur/2020",
}
def fetch_html(url):
"""请求软科 2020 主榜接口,返回文本"""
params = {"bcur_type": 11, "year": 2020}
resp = requests.get(url, headers=HEADERS, params=params, timeout=15)
resp.encoding = "utf-8"
resp.raise_for_status()
return resp.text
def parse_ranking(json_text):
# 用 BeautifulSoup 解析文本,再拿回干净的字符串
soup = BeautifulSoup(json_text, "lxml")
clean_text = soup.get_text()
# 再用 json 模块解析
data = json.loads(clean_text)
rankings = data.get("data", {}).get("rankings", [])
rows = []
for r in rankings:
rows.append([
r.get("ranking", ""),
r.get("univNameCn", ""),
r.get("province", ""),
r.get("univCategory", ""),
r.get("score", ""),
])
return rows
if __name__ == "__main__":
text = fetch_html(API_URL)
data = parse_ranking(text)
print(f"共解析到 {len(data)} 条数据\n")
headers = ["排名", "学校名称", "省市", "学校类型", "总分"]
print(tabulate(data[:30], headers=headers, tablefmt="simple"))
运行结果:

2)心得体会
这次实验最大的收获是学会了从"页面"到"接口"的思路转变。一开始我打算直接解析 HTML 表格,在开发者工具里翻来覆去只找到骨架结构;后来意识到真实数据是通过后端接口以 JSON 形式返回的,思路瞬间打开。这让我意识到:现代网站的前端渲染越来越普遍,光看页面结构不够,还要关注数据从哪来。
另一个收获是 BeautifulSoup 的多场景用法。之前我只把它当作 HTML 解析器,这次我把它当成"文本清洗器"——先用 get_text() 剥掉所有标签和转义字符,再交给 json.loads 解析,避免了很多边界情况下的解析失败。这说明同一个工具在不同场景下可以有完全不同的定位。
另外,请求头中的 Referer 也让我踩了坑——最开始没加 Referer,服务器直接返回了 403。加上之后就正常了。这让我明白:写爬虫时请求头不是可选项,而是必须项。
通过本次实验,我掌握了接口型爬虫的完整流程:定位接口 → 构造请求 → 清洗数据 → 解析结构 → 格式化输出。
作业② 当当网"书包"商品比价爬虫
1)当当商品比价爬虫实验
实验目标:用 requests 库请求网页,用 re 库(正则表达式)从 HTML 中提取数据,自选一个商城,以关键词"书包"搜索,爬取商品名称和价格。
商城选择:我选的是当当网(search.dangdang.com)。原因:京东的价格由 JS 动态加载,requests 抓到的 HTML 中拿不到;淘宝需要登录,反爬严格;而当当的搜索结果页是静态 HTML,商品名、价格都直接写在源码里,用正则提取最合适。
页面结构分析:
打开当当网搜索页(https://search.dangdang.com/?key=书包&act=input),按 F12 打开开发者工具,切到 Elements 面板,用元素选择器分别点选商品价格与商品标题,可以确认:
- 价格保存在
<p class="price"><span class="price_n">¥146.00</span></p>结构中,price_n类名下的<span>直接包含价格文字; - 商品名保存在
<p class="name" name="title"><a title="商品名称">结构中,<a>标签的title属性保存了商品完整标题。
基于这两个结构,即可写出对应的正则进行提取:一条抓 price_n 标签内的数字作为价格,一条抓 name="title" 下 <a> 标签的 title 属性作为商品名。


完整代码:
"""
作业②:用 requests + re 爬取当当网"书包"商品名称与价格
"""
import re
import csv
import time
import urllib3
http = urllib3.PoolManager()
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
),
"Referer": "https://www.dangdang.com/",
}
keyword = "书包"
url = f"https://search.dangdang.com/?key={keyword}&act=input"
MAX_ITEMS = 60
def fetch_html(url):
"""请求当当搜索页,返回 HTML 文本(GBK 编码)"""
resp = http.request("GET", url, headers=HEADERS, timeout=15)
return resp.data.decode("gbk", errors="ignore")
def parse_products(html):
"""
用正则从每个 <li> 商品块中提取商品名和价格
"""
items = []
# 按 li 块切分
li_blocks = re.findall(r'<li[^>]*>(.*?)</li>', html, re.S)
for block in li_blocks:
# 商品名:<p class="name" name="title"><a title="商品名">
name_match = re.search(
r'<p class="name" name="title"[^>]*>\s*<a[^>]*title="([^"]+)"',
block
)
# 价格(主方案):<span class="price_n">¥146.00</span>
price_match = re.search(
r'<span class="price_n"[^>]*>\s*¥\s*([\d]+(?:\.\d{1,2})?)\s*</span>',
block
)
# 价格(兜底方案):页面任意位置 ¥/¥ + 数字
if not price_match:
price_match = re.search(
r'(?:¥|¥)\s*([\d]+(?:\.\d{1,2})?)', block
)
if name_match and price_match:
name = name_match.group(1).strip()
price = price_match.group(1).strip()
# 过滤排序/按钮文字
if any(bad in name for bad in
["点击", "按销量", "按好评", "按价格", "按最新", "综合排序"]):
continue
items.append({"name": name, "price": f"¥{price}"})
if len(items) >= MAX_ITEMS:
break
return items
def main():
print(f"正在爬取:{url}")
try:
html = fetch_html(url)
except Exception as e:
print("请求失败:", e)
return
items = parse_products(html)
if not items:
print("未提取到数据,可能页面结构变化或被反爬拦截")
return
print(f"共提取 {len(items)} 条(上限 {MAX_ITEMS})")
for idx, it in enumerate(items, 1):
print(f"{idx:02d}. {it['price']:>8} {it['name']}")
# 保存 CSV
with open("dangdang_shubao.csv", "w",
encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["name", "price"])
writer.writeheader()
writer.writerows(items)
print("已保存:dangdang_shubao.csv")
if __name__ == "__main__":
main()
time.sleep(1)
运行结果:


2)心得体会
这次作业让我真正体会到"选对目标网站"有多重要。一开始我想爬京东,在 F12 里怎么点都找不到价格——后来才知道它的价格是页面加载完成后由 JS 二次请求回来的,requests 拿到的 HTML 里根本没有。转到当当就顺畅很多,因为它的搜索结果页是静态 HTML,商品名和价格直接写在源码里。
另一个教训是编码。当当是 GBK,我用 resp.data.decode("gbk", errors="ignore") 手动解码后中文才正常;如果偷懒不处理,打印出来的商品名全是乱码。这也让我意识到:不同站点的编码策略不一样,不能死记一种,每次都要先观察。
写正则时,我学会了"先分块、再提字段"的策略。最初我尝试写一条大正则一次匹配所有商品,结果经常跨商品误配,价格和商品名对不上。改成先按 <li> 切块、再在块内分别匹配后,问题立刻解决了。此外,给价格写主备两套正则也很重要——主方案精准但脆弱,兜底方案粗糙但稳定,两者结合可以应对大部分页面微调。
最后,用 CSV 保存结果时,encoding="utf-8-sig" 这个细节很关键——不加 BOM 头,Excel 打开就是乱码。这些看似细小的编码问题,恰恰是爬虫实践中最容易踩的坑。
作业③ 爬取福州大学新闻网图片
1)福大新闻网图片爬虫实验
实验目标:爬取福州大学新闻网"影像福大"页面(https://news.fzu.edu.cn/yxfd.htm )中的所有 JPEG、JPG、PNG 格式图片,保存到本地文件夹。
页面结构分析:页面上的图片都是 <img> 标签,图片地址写在 src 属性里。图片来源分两类:
images/开头的站点小图标(logo、返回按钮、导航箭头等);/__local/开头的新闻内容大图。
爬虫要做的事就是:用正则从 HTML 中取出所有 src="...jpg/jpeg/png" 的地址 → 拼成绝对路径 → 去重 → 逐张下载到本地。

完整代码:
"""
作业③:爬取 https://news.fzu.edu.cn/yxfd.htm 页面中所有 JPEG/JPG/PNG 图片,
保存到 images 文件夹中
"""
import os
import re
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
BASE_URL = "https://news.fzu.edu.cn/yxfd.htm"
SAVE_DIR = "images" # 保存图片的文件夹
TIMEOUT = 15 # 请求超时时间
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"
)
}
# 支持的图片格式(不区分大小写)
IMG_EXTS = (".jpg", ".jpeg", ".png")
def is_target_image(url: str) -> bool:
"""判断 URL 是否指向 JPEG/JPG/PNG 图片"""
if not url:
return False
# 去掉查询参数后再判断扩展名
path = urlparse(url).path.lower()
return path.endswith(IMG_EXTS)
def safe_filename(url: str, index: int) -> str:
"""根据 URL 生成安全、唯一的文件名"""
path = urlparse(url).path
name = os.path.basename(path)
# 去掉非法字符
name = re.sub(r'[\\/:*?"<>|]', "_", name)
if not name:
name = f"image_{index}.jpg"
# 加上序号前缀,避免重名覆盖
return f"{index:03d}_{name}"
def download_images(page_url: str, save_dir: str) -> None:
# 1. 创建保存目录
os.makedirs(save_dir, exist_ok=True)
# 2. 请求网页
print(f"[*] 正在请求页面: {page_url}")
resp = requests.get(page_url, headers=HEADERS, timeout=TIMEOUT)
resp.encoding = resp.apparent_encoding # 自动识别编码,避免中文乱码
resp.raise_for_status()
# 3. 解析 HTML,提取 img 标签
soup = BeautifulSoup(resp.text, "html.parser")
img_tags = soup.find_all("img")
print(f"[*] 页面中共发现 {len(img_tags)} 个 <img> 标签")
# 4. 收集并去重图片链接
img_urls = []
seen = set()
for tag in img_tags:
# 优先取 src,其次 data-src / data-original(懒加载)
src = tag.get("src") or tag.get("data-src") or tag.get("data-original")
if not src:
continue
full_url = urljoin(page_url, src.strip())
if is_target_image(full_url) and full_url not in seen:
seen.add(full_url)
img_urls.append(full_url)
print(f"[*] 其中 JPEG/JPG/PNG 图片共 {len(img_urls)} 张,开始下载...\n")
# 5. 逐张下载
success, failed = 0, 0
for i, url in enumerate(img_urls, start=1):
filename = safe_filename(url, i)
filepath = os.path.join(save_dir, filename)
try:
r = requests.get(url, headers=HEADERS, timeout=TIMEOUT)
r.raise_for_status()
with open(filepath, "wb") as f:
f.write(r.content)
success += 1
print(f"[OK] ({i}/{len(img_urls)}) {filename}")
except Exception as e:
failed += 1
print(f"[FAIL] ({i}/{len(img_urls)}) {url} -> {e}")
# 6. 汇总
print("\n========== 下载完成 ==========")
print(f"成功: {success} 张")
print(f"失败: {failed} 张")
print(f"保存目录: {os.path.abspath(save_dir)}")
if __name__ == "__main__":
download_images(BASE_URL, SAVE_DIR)
运行结果:


2)心得体会
这次作业让我彻底搞懂了相对路径与绝对路径的区别。网页里很多图片的 src 是 /__local/F/4D/B3/xxx.jpg 这种以斜杠开头的根相对路径,直接拿去请求会 404,必须用 urljoin() 和页面 URL 拼成完整的 https://news.fzu.edu.cn/__local/F/4D/B3/xxx.jpg 才能下载成功。这个细节看起来简单,但不处理的话整个程序会一张图都下载不下来。
第二个收获是正则匹配图片的写法。r'src=["\']([^"\']+\.(?:jpg|jpeg|png))["\']' 这一条正则,把引号兼容、捕获组、后缀限定都放在了一起,非常紧凑。通过这次练习,我对正则里的非捕获组 (?:...)、忽略大小写 re.I 等特性理解更深了——非捕获组既能分组限定后缀,又不会污染 findall 的返回结果,是个很实用的小技巧。
第三个体会是二进制写入。图片是二进制文件,必须用 "wb" 模式写入,用 "w" 会导致图片损坏、打不开。这一点和写文本文件完全不同,需要特别小心。此外 resp.content(字节流)和 resp.text(文本)的区别也很关键——图片只能走 content。
最后,我对"如何界定页面图片"有了更清晰的认识。用正则全页面匹配 src,可以抓到 <script> 里的图片和懒加载图,覆盖面广;用 BeautifulSoup 只遍历 <img> 标签,范围更窄但更干净。两种方式都能交作业,关键是在博客里把界定标准说清楚,让读者理解为什么数量会有差异。这也让我明白:爬虫的"对错"往往不绝对,关键是逻辑清晰、结果可复现。
总结
通过这次作业,我系统练习了 requests 发送请求、用 BeautifulSoup 解析表格、用正则表达式提取商品信息、以及批量下载图片这四种最常见的爬虫场景。最大的体会是:写爬虫之前,一定要先用浏览器 F12 看清目标数据到底在哪个标签里;编码问题和动态渲染问题是最容易踩的两个坑。
三个作业层层递进:
- 作业① 学会了静态表格数据的结构化提取,重点是精准定位标签和处理编码;
- 作业② 学会了正则表达式提取网页数据的方法,核心是"先分块、再提字段",同时体会到不同商城编码和渲染方式的差异;
- 作业③ 学会了图片等二进制资源的批量下载,理解了相对/绝对路径、URL 拼接和二进制写入的细节。
每一个作业都经历了"分析页面 → 编写代码 → 运行调试 → 分析结果"的完整过程,既掌握了 Python 爬虫的核心技术,也培养了严谨的问题分析能力。

浙公网安备 33010602011771号