数据采集与融合技术第一次作业
数据采集与融合技术 第一次作业
一、作业①:定向爬取软科2020中国大学排名
(1)实验代码与运行结果
任务要求:用 requests 和 BeautifulSoup 库定向爬取软科2020中国大学排名数据,屏幕打印爬取的大学排名信息,输出字段为:排名、学校名称、省市、学校类型、总分。
爬取思路:
- 目标网址
https://www.shanghairanking.cn/rankings/bcur/2020的数据以服务端渲染的 HTML 表格形式存在,每条记录对应<tbody>下的一个<tr>。 - 第 1~5 个
<td>依次是排名、学校名称、省市、学校类型、总分。 - 学校名称单元格中同时包含中英文名和“双一流/985/211”标签,需要通过
class="name-cn"精确提取中文名。 - 控制台输出使用
unicodedata计算字符显示宽度(中文算 2、英文数字算 1),实现中英文混合对齐。
代码实现:
"""作业①:requests + BeautifulSoup 定向爬取软科2020中国大学排名
输出格式:排名 学校名称 省市 学校类型 总分
"""
import re
import unicodedata
import requests
from bs4 import BeautifulSoup
URL = "https://www.shanghairanking.cn/rankings/bcur/2020" # 目标网址:软科2020大学排名
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/", # 声明请求来源,模拟从首页跳转
}
# 网络请求模块
def fetch_html(url):
resp = requests.get(url, headers=HEADERS, timeout=20) # 发起请求,设置20秒超时
resp.raise_for_status() # 状态码非200时抛异常
resp.encoding = resp.apparent_encoding # 自动识别编码,避免中文乱码
return resp.text
#数据解析模块
def parse_ranking(html):
soup = BeautifulSoup(html, "html.parser") # 创建 BeautifulSoup 对象
tbody = soup.find("tbody") # 定位表格主体
if not tbody:
return [] # 页面结构异常,返回空列表
rows = []
for tr in tbody.find_all("tr"): # 遍历表格的每一行
tds = tr.find_all("td") # 获取该行的所有单元格
if len(tds) < 5:
continue # 跳过列数不足的无效行
rank = tds[0].get_text(strip=True) # 第1列:排名
# 过滤掉表头等非纯数字行
if not re.fullmatch(r"\d+", rank):
continue
# 第2列:学校名称——精确定位中文名所在 span,避免混入英文名和 985/211 标签
name_span = tds[1].find("span", class_="name-cn")
school = name_span.get_text(strip=True) if name_span else tds[1].get_text(strip=True)
province = tds[2].get_text(strip=True) # 第3列:省市
school_type = tds[3].get_text(strip=True) # 第4列:学校类型
score = tds[4].get_text(strip=True) # 第5列:总分
rows.append([rank, school, province, school_type, score])
return rows
# 显示宽度计算模块
def get_display_width(s):
width = 0
for ch in s:
# east_asian_width() 返回 'W'(宽) 或 'F'(全角) 时按 2 个宽度计算
width += 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
return width
def pad_string(s, target_width, align="left"):
padding = target_width - get_display_width(s) # 计算需要补充的空格数
if padding <= 0:
return s # 超出列宽,不做处理
if align == "left":
return s + " " * padding # 左对齐:右侧补空格
if align == "right":
return " " * padding + s # 右对齐:左侧补空格
left_pad = padding // 2 # 居中:两侧平分空格
return " " * left_pad + s + " " * (padding - left_pad)
def print_table(rows):
col_widths = [6, 24, 10, 10, 8] # 每列的显示宽度
headers = ["排名", "学校名称", "省市", "学校类型", "总分"]
col_gap = " " # 列与列之间的间隔
header_str = col_gap.join(pad_string(h, w, "center") for h, w in zip(headers, col_widths))
print(header_str)
print("-" * (sum(col_widths) + len(col_gap) * (len(col_widths) - 1)))
for row in rows:
row_str = col_gap.join(
pad_string(val, w, "right" if i == 0 else "left")
for i, (val, w) in enumerate(zip(row, col_widths))
)
print(row_str)
# 主程序入口
if __name__ == "__main__":
html = fetch_html(URL) # 1. 请求网页
data = parse_ranking(html) # 2. 解析数据
if data:
print_table(data) # 3. 打印表格
else:
print("未获取到排名数据,请检查页面结构是否变化。")
运行结果截图:

(2)心得体会
① BeautifulSoup 定位要“精确到子标签”
最初我直接对 <td> 调用 get_text(strip=True),结果学校名称输出成了“清华大学Tsinghua University双一流/985/211”这样混在一起的内容。原因是一个 <td> 里嵌套了多个 <span>,get_text() 会把它下面所有子孙节点的文本拼接起来。后来改用 tds[1].find("span", class_="name-cn") 精确定位到存放中文名的那个 span,才得到干净的“清华大学”。这让我意识到,解析 HTML 时要先看清 DOM 结构再决定用哪一级标签提取。
② 中英文混合输出的对齐难题
用 \t(制表符)分隔各列时,控制台输出看起来参差不齐。原因是中文和英文在控制台里占的显示宽度不同——一个汉字占两个字符位,一个英文/数字只占一个。后来通过 unicodedata.east_asian_width() 计算每个字符的显示宽度,再根据目标列宽进行空格填充,才实现完美对齐。
③ 正则筛选有效数据行
页面的 <tbody> 里并非所有 <tr> 都是排名数据,可能混入表头、广告等。用 re.fullmatch(r"\d+", rank) 判断第一列是否为纯数字,可以快速过滤掉无效行。
④ 请求头不能少
软科网站会检查 User-Agent,不加浏览器标识会被拒绝访问。加上 User-Agent 和 Referer 后请求就稳定了。
二、作业②:商城商品比价定向爬虫
(1)实验代码与运行结果
任务要求:用 requests 和 re 库方法设计某个商城商品比价定向爬虫,以关键词“书包”搜索页面的数据,爬取商品名称和价格,输出格式为:序号、价格、商品名。
爬取思路:
- 目标商城选择当当网,搜索关键词“书包”。
- 当当网搜索页使用 GBK 编码,必须显式设置
resp.encoding = "gbk"。 - 通过诊断发现,价格使用 HTML 实体
¥表示(不是¥字符),类名为price_n。 - 商品名在
<p class="name">内的<a>标签的title属性中。
代码实现:
"""作业②:requests + re 爬取当当网“书包”搜索页
输出格式:序号 价格 商品名
"""
import re
import time
import requests
KEYWORD = "书包" # 搜索关键词
PAGES = 3 # 抓取页数
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"
),
"Accept-Language": "zh-CN,zh;q=0.9", # 声明接受中文内容
"Referer": "http://search.dangdang.com/", # 请求来源
}
# 正则表达式模块
# 当当网价格使用 HTML 实体 ¥ 表示,不是 ¥ 字符
RE_PRICE = re.compile(r'<span class="price_n">(?:¥|[¥¥])?([\d.]+)</span>', re.S)
# 商品名:<p class="name" name="title"><a title="商品名"
RE_NAME = re.compile(r'<p class="name"[^>]*>\s*<a[^>]*title="([^"]+)"', re.S)
# 网络请求模块
def fetch(keyword, page_index):
url = "http://search.dangdang.com/"
params = {"key": keyword, "act": "input", "page_index": page_index}
for attempt in range(1, 4): # 最多重试 3 次
try:
resp = requests.get(url, params=params, headers=HEADERS, timeout=15)
resp.raise_for_status() # 状态码非200时抛异常
resp.encoding = "gbk"
return resp.text
except Exception as e:
print(f" [重试 {attempt}/3] {type(e).__name__}: {e}")
time.sleep(5) # 失败后等待 5 秒再重试
return None
# 数据解析模块
def parse(html):
if not html:
return []
names = [n.strip() for n in RE_NAME.findall(html)] # 提取所有商品名
prices = RE_PRICE.findall(html) # 提取所有价格
items = []
# 按出现顺序 zip 配对:第 n 个名称对应第 n 个价格
for name, price in zip(names, prices):
name = re.sub(r"\s+", " ", name).strip() # 合并多余空白
try:
items.append((name, float(price))) # 价格转 float,便于统计
except ValueError:
continue
return items
# 主程序入口
def main():
"""主流程:多页抓取 → 解析 → 打印 → 统计"""
print("=" * 78)
print(f"商城商品比价定向爬虫 商城:当当网 关键词:{KEYWORD}")
print("=" * 78)
all_items = []
for page in range(1, PAGES + 1): # 逐页抓取
print(f"\n[第 {page} 页]")
html = fetch(KEYWORD, page)
if not html:
print(" 抓取失败,跳过本页")
continue
items = parse(html)
print(f" 抓取 {len(items)} 条")
all_items.extend(items)
time.sleep(3) # 礼貌性延时,避免触发反爬
# 打印所有商品
print("\n" + "=" * 78)
print(f"共获取 {len(all_items)} 条商品信息")
print("=" * 78)
print(f"{'序号':<6}{'价格':>10} 商品名")
print("-" * 78)
for i, (name, price) in enumerate(all_items, 1):
short = name if len(name) <= 50 else name[:49] + "…" # 过长名称截断
print(f"{i:<8}{price:>8.2f} {short}")
# 价格统计
if all_items:
prices = [p for _, p in all_items]
print("-" * 78)
print(f"价格统计:最低 ¥{min(prices):.2f} 最高 ¥{max(prices):.2f} "
f"平均 ¥{sum(prices)/len(prices):.2f}")
if __name__ == "__main__":
main()
运行结果截图:


(2)心得体会
① 编码问题是中文爬虫的第一道坎
当当网页面使用 GBK 编码,如果不显式设置 resp.encoding = "gbk",requests 会根据响应头里的 Content-Type 自动推断,有时会误判成 ISO-8859-1,导致中文全部变成乱码。最可靠的方法是查看页面 <meta charset> 标签。
② 价格标签会随网站改版而变化
最初从网上教程里看到的当当网价格类名是 search_now_price,但实际页面中该类名已经弃用。通过统计页面中所有含 price 的类名,发现真正在用的是 price_n。这告诉我,写爬虫不能照抄教程,要用诊断手段验证实际结构。
③ HTML 实体是正则匹配的“隐形陷阱”
诊断显示 price_n 出现 60 次,但价格正则还是匹配不到。最后打印 HTML 片段才发现,当当网价格用的是 ¥(HTML 实体),而不是 ¥ 字符!修正为 (?:¥|[¥¥])? 后立刻成功抓取了 60 条数据。这个坑很典型,日后遇到货币符号、版权符号、箭头等特殊字符时,一定要考虑 HTML 实体的可能性。
④ 频率控制与反爬
当当网对短时间内的频繁请求比较敏感,访问间隔太短会遇到 502 Bad Gateway。把请求间隔设为 3 秒,并加上 time.sleep() 礼貌性延时后,请求就稳定多了。
三、作业③:批量下载网页图片
(1)实验代码与运行结果
任务要求:爬取给定网页或自选网页中所有 JPEG、JPG、PNG 格式的图片文件,保存在一个文件夹中。
爬取思路:
- 目标网页选择福州大学新闻中心“院系风采”栏目。
- 用 BeautifulSoup 解析所有
<img>标签,提取 src 或 data-src 属性(兼容懒加载)。 - 用 urljoin 把相对路径转为绝对路径,用集合去重。
- 用 urlparse 去掉查询串后再提取文件名,用正则清理非法字符。
- 以二进制模式("wb")逐张保存到 images 文件夹,每张间隔 0.5 秒。
代码实现:
"""作业③:爬取指定网页所有 JPEG、JPG、PNG 图片并保存到本地文件夹
目标网页:https://news.fzu.edu.cn/yxfd.htm
输出:所有图片保存到 images 文件夹,并打印每张图片的保存结果
"""
import os
import re
import sys
import time
from urllib.parse import urljoin, urlparse
import requests
from bs4 import BeautifulSoup
DEFAULT_URL = "https://news.fzu.edu.cn/yxfd.htm" # 默认目标网页
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://news.fzu.edu.cn/", # 请求来源
}
# 保存目录:与本脚本同级
SAVE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "images")
# 匹配以 .jpg/.jpeg/.png 结尾(后面可能带查询串)
RE_IMAGE = re.compile(r"\.(jpe?g|png)(?:\?.*)?$", re.I)
# 网络请求模块
def fetch_html(url):
"""请求网页,返回 HTML 文本
url: 目标网址
成功返回 HTML 文本,失败重试 3 次后返回 None
"""
for attempt in range(1, 4): # 最多重试 3 次
try:
resp = requests.get(url, headers=HEADERS, timeout=20)
resp.raise_for_status() # 状态码非200时抛异常
resp.encoding = resp.apparent_encoding or "utf-8"
return resp.text
except Exception as e:
print(f" [重试 {attempt}/3] {type(e).__name__}: {e}")
time.sleep(2)
return None
# 图片 URL 收集模块
def collect_image_urls(html, base_url):
"""从 HTML 中提取所有 jpg/jpeg/png 图片的完整地址
html: HTML 文本
base_url: 页面 URL,用于将相对路径转为绝对路径
去重后的图片 URL 列表(保持页面中出现的先后顺序)
"""
soup = BeautifulSoup(html, "lxml")
urls, seen = [], set() # seen 集合用于去重
for img in soup.find_all("img"):
# 优先取 src,没有则取 data-src(懒加载常见)
src = (img.get("src") or img.get("data-src") or "").strip()
if not src or not RE_IMAGE.search(src):
continue # 跳过非目标格式
full_url = urljoin(base_url, src) # 相对路径 → 绝对路径
if full_url not in seen:
seen.add(full_url)
urls.append(full_url)
return urls
#文件名生成模块
def safe_filename(url, index):
"""根据 URL 生成安全的本地文件名
url: 图片 URL
index: 序号(避免重名,同时保留下载顺序)
形如 01_logo.png 的文件名
"""
path = urlparse(url).path # 去掉查询串
name = os.path.basename(path)
# 替换文件名中的非法字符(保留中文、英文、数字、点、下划线、连字符)
name = re.sub(r"[^\w.\-\u4e00-\u9fff]", "_", name)
if not name or not RE_IMAGE.search(name):
name = "image.png" # 兜底文件名
return f"{index:02d}_{name}"
#图片下载模块
def download(url, path):
"""下载单张图片并保存到本地
url: 图片 URL
path: 本地保存路径
下载的字节数
"""
resp = requests.get(url, headers=HEADERS, timeout=25)
resp.raise_for_status()
with open(path, "wb") as f: # 二进制模式写入
f.write(resp.content)
return len(resp.content)
# 主程序入口
def main():
"""主流程:请求网页 → 收集图片 URL → 逐张下载 → 汇总结果"""
# 支持命令行传入自选网页
page_url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URL
print("=" * 78)
print(f"目标网页:{page_url}")
print(f"保存目录:{SAVE_DIR}")
print("=" * 78)
# 创建保存目录(不存在时才创建)
if not os.path.exists(SAVE_DIR):
os.makedirs(SAVE_DIR)
# 1. 请求网页
html = fetch_html(page_url)
if not html:
print("网页抓取失败,请检查网络或 URL")
return
# 2. 收集图片 URL
image_urls = collect_image_urls(html, page_url)
print(f"\n共找到 {len(image_urls)} 张图片,开始下载...\n")
# 3. 逐张下载
success, failed = 0, 0
for idx, img_url in enumerate(image_urls, 1):
filename = safe_filename(img_url, idx)
filepath = os.path.join(SAVE_DIR, filename)
try:
size = download(img_url, filepath)
print(f"[{idx:>2}/{len(image_urls)}] ✓ {filename} ({size} 字节)")
success += 1
except Exception as e:
print(f"[{idx:>2}/{len(image_urls)}] ✗ {img_url} —— {e}")
failed += 1
time.sleep(0.5) # 礼貌性延时
# 4. 汇总结果
print("\n" + "=" * 78)
print(f"下载完成:成功 {success} 张,失败 {failed} 张")
print(f"图片保存位置:{SAVE_DIR}")
print("=" * 78)
if __name__ == "__main__":
main()
运行结果截图:


(2)心得体会
1、urljoin 是处理相对路径的利器
福大新闻页的图片地址很多是相对路径,如 /upload/images/xxx.jpg。如果不处理直接拼接,会得到错误的地址。用 urllib.parse.urljoin(page_url, src) 可以自动处理所有情况(相对路径、绝对路径、协议相对路径),是图片爬虫必备的工具。
2、 懒加载图片要用 data-src
现代网站为优化首屏加载,往往把 <img> 的 src 设置为一个空白占位图,真实的图片地址放在 data-src 属性中,滚动到可见区域时才由 JavaScript 替换。因此提取时应该 src or data-src 双保险。
3、 查询串会破坏文件名的扩展名判断
有些图片地址是 xxx.png?e=.png 这种带防盗链参数的形式,如果用 os.path.splitext 直接处理,扩展名会变成 png?e=.png,导致文件类型识别错误。正确做法是先用 urlparse 去掉查询串,再提取文件名。
4、 二进制文件必须用 wb 模式
图片是二进制数据,如果误用文本模式 open(path, "w") 打开,write() 遇到 \x00 等特殊字节会报错或导致文件损坏。用 "wb" 打开后写入 resp.content(字节流)才正确。
5、 集合去重能省不少事
福大新闻页的页头页脚有重复的 logo,如果不去重会重复下载。用 seen = set() 记录已经出现过的 URL,可以避免重复请求和重复保存。
6、 礼貌性延时
每张图片之间加 time.sleep(0.5) 是爬虫的基本礼仪。如果爬取大型图片站,不加延时会因为请求频繁被对方封 IP。
7、 完整流程总结
这次作业让我完整实践了“请求网页 → 解析 HTML → 提取 URL → 下载文件 → 保存本地”这一整套流程。之前的作业都是提取文本数据,这次是下载二进制文件,对文件 I/O、编码、异常处理的细节要求更高。整体跑通后,爬取 32 张图片全部成功。
浙公网安备 33010602011771号