数据采集与融合技术第一次作业
作业①:上海软科中国大学排名爬取
1)、大学排名爬取实验
实验代码
# -*- coding: utf-8 -*-
"""
作业①:爬取上海软科中国大学排名
使用 requests + BeautifulSoup 爬取 2020 中国最好大学排名,
将排名、学校名称、省市、学校类型、总分以表格形式打印到屏幕。
作者:邓杰 学号:102402122
"""
import requests
from bs4 import BeautifulSoup
# 目标 URL(2020 中国最好大学排名)
URL = "http://www.shanghairanking.cn/rankings/bcur/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"
}
def fetch_html(url):
"""获取指定 URL 的网页源码并按实际编码解码返回。"""
response = requests.get(url, headers=HEADERS, timeout=20)
response.encoding = response.apparent_encoding # 自动识别网页编码
response.raise_for_status() # 状态码非 2xx 时抛出异常
return response.text
def parse_rankings(html):
"""
解析排名页面,返回学校信息列表。
每条记录为一个元组:(排名, 学校名称, 省市, 学校类型, 总分)。
学校名称取自 <span class="name-cn"> 节点,过滤掉英文名与标签。
"""
soup = BeautifulSoup(html, "lxml")
tbody = soup.find("table").find("tbody") # 排名数据存放在 <tbody> 中
rows = tbody.find_all("tr")
result = []
for row in rows:
cells = row.find_all("td")
if len(cells) < 5:
continue # 跳过不完整的行
rank = cells[0].get_text(strip=True) # 排名
# 学校中文名存放在 name-cn 节点内,过滤英文名和 985/211 等标签
name_node = cells[1].find("span", class_="name-cn")
name = name_node.get_text(strip=True) if name_node else cells[1].get_text(strip=True)
province = cells[2].get_text(strip=True) # 省市
school_type = cells[3].get_text(strip=True) # 学校类型
score = cells[4].get_text(strip=True) # 总分
result.append((rank, name, province, school_type, score))
return result
def print_table(records):
"""以对齐的表格形式打印排名结果。"""
headers = ("排名", "学校名称", "省市", "学校类型", "总分")
def display_width(text):
"""计算字符串显示宽度:中文字符占 2,其余占 1。"""
return sum(2 if ord(c) > 127 else 1 for c in text)
col_widths = [display_width(h) for h in headers]
for rec in records:
for i, val in enumerate(rec):
col_widths[i] = max(col_widths[i], display_width(val))
def pad(text, width):
return text + " " * (width - display_width(text))
line = " ".join(pad(h, w) for h, w in zip(headers, col_widths))
print(line)
print("-" * (len(line) + 8))
for rec in records:
print(" ".join(pad(v, w) for v, w in zip(rec, col_widths)))
def main():
"""主流程:获取 -> 解析 -> 打印。"""
print("正在获取排名数据...")
html = fetch_html(URL)
print(f"页面获取成功,长度:{len(html)} 字节\n")
records = parse_rankings(html)
print(f"共解析出 {len(records)} 所高校\n")
print_table(records)
if __name__ == "__main__":
main()
运行结果
正在获取排名数据...
页面获取成功,长度:343217 字节
共解析出 30 所高校
排名 学校名称 省市 学校类型 总分
-----------------------------------------
1 清华大学 北京 综合 852.5
2 北京大学 北京 综合 746.7
3 浙江大学 浙江 综合 649.2
4 上海交通大学 上海 综合 625.9
5 南京大学 江苏 综合 566.1
6 复旦大学 上海 综合 556.7
7 中国科学技术大学 安徽 理工 526.4
8 华中科技大学 湖北 综合 497.7
9 武汉大学 湖北 综合 488.0
10 中山大学 广东 综合 457.2
11 西安交通大学 陕西 综合 452.5
12 哈尔滨工业大学 黑龙江 理工 450.2
13 北京航空航天大学 北京 理工 445.1
14 北京师范大学 北京 师范 440.9
15 同济大学 上海 理工 439.0
16 四川大学 四川 综合 435.7
17 东南大学 江苏 综合 432.7
18 中国人民大学 北京 综合 409.7
19 南开大学 天津 综合 402.1
20 北京理工大学 北京 理工 395.6
21 天津大学 天津 理工 390.3
22 山东大学 山东 综合 387.9
23 厦门大学 福建 综合 383.3
24 吉林大学 吉林 综合 379.5
25 华南理工大学 广东 理工 379.4
26 中南大学 湖南 综合 378.6
27 大连理工大学 辽宁 理工 365.1
28 西北工业大学 陕西 理工 359.6
29 华东师范大学 上海 师范 358.0
30 中国农业大学 北京 农业 351.5

2)、心得体会
本次实验让我初步掌握了 requests + BeautifulSoup 这一爬虫组合的使用。一开始我以为爬到的就是肉眼看到的整洁表格,但实际解析 cells[1] 时发现一个单元格里混进了学校英文名和 "双一流/985/211" 标签,导致输出冗长。通过查看 HTML 结构,定位到学校中文名其实存放在 <span class="name-cn"> 节点里,于是改用 find("span", class_="name-cn") 精准提取,输出立刻清爽了。
另一个收获是编码问题:中文网页常常不显式声明编码,直接 response.text 可能乱码。用 response.encoding = response.apparent_encoding 让 requests 自动识别,能很好地兼容这类页面。同时我也意识到 User-Agent 请求头的重要性——不带它很多站点会直接拒绝。对齐表格时遇到中英文宽度不一致导致列错位,自己写了 display_width 按显示宽度填充空格,加深了对字符编码与显示的理解。
作业②:电商网站商品价格比价爬取
1)、电商比价爬取实验
实验代码
# -*- coding: utf-8 -*-
"""
作业②:电商网站商品价格比价爬虫
使用 requests + re(正则表达式)爬取当当网搜索"书包"的商品结果,
提取商品名称与价格,按 序号 / 价格 / 商品名 表格输出。
作者:邓杰 学号:102402122
"""
import re
import requests
# 当当网搜索"书包"的结果页(key 参数为 URL 编码后的"书包")
URL = "http://search.dangdang.com/?key=%E4%B9%A6%E5%8C%85"
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"
}
def fetch_html(url):
"""获取网页源码并按实际编码解码。"""
response = requests.get(url, headers=HEADERS, timeout=20)
response.encoding = response.apparent_encoding
response.raise_for_status()
return response.text
def extract_products(html):
"""
使用正则表达式提取商品名称与价格。
当当商品列表结构:
<p class="name" ...><a ... title="商品名">...</a></p>
<span class="price_n">¥价格</span>
本函数分别用两条正则匹配 name 与 price,并按下标一一配对。
"""
# 匹配 <p class="name" ...> 内 <a> 标签的 title 属性(完整商品名)
name_pattern = re.compile(
r'<p class="name"[^>]*>\s*<a[^>]*title="([^"]+)"'
)
# 匹配 <span class="price_n">价格</span>,价格前缀可能是
# 字符 ¥、HTML 实体 ¥ 或 ¥,统一兼容
price_pattern = re.compile(
r'<span class="price_n">(?:¥|¥|¥)?\s*([\d.]+)</span>'
)
names = name_pattern.findall(html)
prices = price_pattern.findall(html)
names = [n.strip() for n in names] # 名称前后可能含多余空白
pair_count = min(len(names), len(prices)) # 取较小者保证配对正确
return [(names[i], prices[i]) for i in range(pair_count)]
def print_table(products):
"""以对齐表格输出 序号 / 价格 / 商品名。"""
headers = ("序号", "价格", "商品名")
def display_width(text):
return sum(2 if ord(c) > 127 else 1 for c in text)
col_data = [
[str(i + 1) for i in range(len(products))],
[f"¥{p[1]}" for p in products],
[p[0] for p in products],
]
col_widths = []
for i, head in enumerate(headers):
w = display_width(head)
for val in col_data[i]:
w = max(w, display_width(val))
col_widths.append(w)
def pad(text, width):
return text + " " * (width - display_width(text))
head_line = " ".join(pad(h, w) for h, w in zip(headers, col_widths))
print(head_line)
print("-" * (len(head_line) + 8))
for i in range(len(products)):
row = [col_data[0][i], col_data[1][i], col_data[2][i]]
print(" ".join(pad(v, w) for v, w in zip(row, col_widths)))
def main():
print('正在搜索当当网"书包"商品...\n')
html = fetch_html(URL)
print(f"页面获取成功,长度:{len(html)} 字节\n")
products = extract_products(html)
print(f"共提取到 {len(products)} 件商品\n")
print_table(products)
if __name__ == "__main__":
main()
运行结果
正在搜索当当网"书包"商品...
页面获取成功,长度:224548 字节
共提取到 60 件商品
序号 价格 商品名
--------------------------------------------------------------------------------------------------------------
1 ¥146.00 迪士尼书包小学生女童一三六年级公主女孩轻便儿童护脊减负双肩包邮1
2 ¥89.99 书包女新款初中生高中学生大容量轻便减负护脊简约学院风双肩包
3 ¥39.99 书包幼儿园女孩 潮小兔子书包3-6岁大中小班男童儿童卡通可爱韩版
4 ¥224.00 儿童书包护脊减负大容量2026新款男生一二三到六年级男孩超轻书包
5 ¥99.80 书包男女生 1-6年级大容量新款小学生耐磨透气书包儿童双肩包小学生男女孩双肩书包
6 ¥75.80 软书包女孩小学生一二三到六年级可爱卡通超轻便减负儿童双肩背包
7 ¥132.00 迪士尼幼儿园书包大班女童流沙卡通可爱女孩学前班儿童宝宝双肩包
8 ¥119.00 书包男女生 小学生书包男孩女孩一二三四五六年级初中生大容量轻便休闲双肩包
9 ¥239.00 瑞牌小学生书包男生减负护脊轻便儿童书包四到六年级女生高颜值书包新款1-6年级耐磨背包
10 ¥93.50 书包女初中生轻便小学生可爱原创三到六年级大容量双肩背包
...(共 60 件,此处节选前 10 条)

2)、心得体会
这个实验的核心难点在于用正则表达式(re)从一团 HTML 里精确抠出商品名与价格。我最初写的价格正则是 <span class="price_n">¥([\d.]+)</span>,直接照搬网页"看到的样子",结果匹配数为 0。排查后才意识到:网页源码里价格符号其实是 HTML 实体 ¥ 而非字符 ¥,浏览器会把它渲染成 ¥,但正则面对的是原始源码。把正则改成 (?:¥|¥|¥)? 兼容三种写法后,60 条价格全部命中。
商品名同理:<a> 标签里的可见文字混着 <font class="skcolor_ljg">书包</font> 高亮标签,直接取文本会丢失或错乱,而 title 属性里存的就是干净完整的商品名,于是正则锁定 title="([^"]+)"。这让我深刻体会到"网页结构要以查看源码为准,不能只信浏览器渲染结果"。配对时取 min(len(names), len(prices)) 做兜底,保证名称与价格一一对应,不会因为数量不一致而错位,这是写爬虫必须具备的健壮性意识。
作业③:网页图片爬取与保存
1)、网页图片爬取实验
实验代码
# -*- coding: utf-8 -*-
"""
作业③:爬取网页图片
使用 requests + BeautifulSoup 爬取福州大学新闻网页上的所有
JPEG/JPG/PNG 图片,并保存到本地同一个文件夹。
作者:邓杰 学号:102402122
"""
import os
import re
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup
# 福州大学新闻页(院系风采)
URL = "https://news.fzu.edu.cn/yxfd.htm"
# 本地保存图片的文件夹名
SAVE_DIR = "fzu_images"
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"
}
IMAGE_EXTS = (".jpg", ".jpeg", ".png") # 合法的图片扩展名
def fetch_html(url):
"""获取网页源码并按实际编码解码。"""
response = requests.get(url, headers=HEADERS, timeout=20)
response.encoding = response.apparent_encoding
response.raise_for_status()
return response.text
def collect_image_urls(html, base_url):
"""
从 HTML 中提取所有 jpg/jpeg/png 图片的绝对 URL。
步骤:1. 用 BeautifulSoup 解析出全部 <img> 的 src;
2. 过滤扩展名(去掉查询参数后再判断后缀);
3. 用 urljoin 把相对路径补全为绝对路径。
"""
soup = BeautifulSoup(html, "lxml")
img_tags = soup.find_all("img")
urls = []
seen = set() # 去重,避免同一张图下载多次
for tag in img_tags:
src = tag.get("src", "").strip()
if not src:
continue
# 去掉查询字符串后再判断扩展名,例如 a.png?e=.png
path = re.sub(r"\?.*$", "", src).lower()
if not path.endswith(IMAGE_EXTS):
continue
abs_url = urljoin(base_url, src) # 相对路径转绝对路径
if abs_url not in seen:
seen.add(abs_url)
urls.append(abs_url)
return urls
def download_images(urls, save_dir):
"""下载图片到指定文件夹,文件名按序号 + 原扩展名命名。"""
os.makedirs(save_dir, exist_ok=True) # 文件夹不存在则创建
success = 0
for idx, img_url in enumerate(urls, start=1):
try:
resp = requests.get(img_url, headers=HEADERS, timeout=20)
resp.raise_for_status()
ext = os.path.splitext(re.sub(r"\?.*$", "", img_url))[1].lower()
if ext not in IMAGE_EXTS:
ext = ".png" # 兜底
filename = f"img{idx:02d}{ext}"
filepath = os.path.join(save_dir, filename)
with open(filepath, "wb") as f:
f.write(resp.content)
print(f" [{idx:02d}] 下载成功 -> {filename} ({len(resp.content)} 字节)")
success += 1
except Exception as e:
print(f" [{idx:02d}] 下载失败: {img_url} ({e})")
return success
def main():
"""主流程:获取页面 -> 收集图片 URL -> 下载保存。"""
print("正在获取目标网页...")
html = fetch_html(URL)
print(f"页面获取成功,长度:{len(html)} 字节\n")
img_urls = collect_image_urls(html, URL)
print(f"共发现 {len(img_urls)} 张 jpg/jpeg/png 图片:")
for u in img_urls:
print(f" {u}")
print()
print(f"开始下载到文件夹:{SAVE_DIR}/")
count = download_images(img_urls, SAVE_DIR)
print(f"\n下载完成,成功保存 {count}/{len(img_urls)} 张图片到 {SAVE_DIR}/ 文件夹")
if __name__ == "__main__":
main()
运行结果
正在获取目标网页...
页面获取成功,长度:37368 字节
共发现 19 张 jpg/jpeg/png 图片:
https://news.fzu.edu.cn/images/top_search.png
https://news.fzu.edu.cn/images/logo.png
https://news.fzu.edu.cn/images/footer_logo.png
...
https://news.fzu.edu.cn/__local/F/4D/B3/EE9C07C6486DEFAA9EAF683C37C_48D55A5D_F84E4.jpg
https://news.fzu.edu.cn/__local/7/B0/23/A81DCC70A190C301F32A2CD9710_57503CF4_12A9BA.png
...
开始下载到文件夹:fzu_images/
[01] 下载成功 -> img01.png (473 字节)
[02] 下载成功 -> img02.png (27193 字节)
[03] 下载成功 -> img03.png (25402 字节)
[08] 下载成功 -> img08.jpg (1017060 字节)
[09] 下载成功 -> img09.png (1223098 字节)
[13] 下载成功 -> img13.png (9143025 字节)
...
[19] 下载成功 -> img19.png (408 字节)
下载完成,成功保存 19/19 张图片到 fzu_images/ 文件夹
保存结果(fzu_images/ 文件夹):
| 文件名 | 大小 | 文件名 | 大小 |
|---|---|---|---|
| img01.png | 473 B | img11.png | 6.2 MB |
| img02.png | 26.6 KB | img12.jpg | 1.3 MB |
| img08.jpg | 993 KB | img13.png | 8.7 MB |
| img09.png | 1.2 MB | img16.jpg | 234 KB |
| img10.jpg | 176 KB | img19.png | 408 B |

2)、心得体会
这个实验相比前两个多了"二进制文件保存"和"相对 URL 处理"两个新点。一开始我用 src.lower().endswith(".jpg") 直接判断后缀,结果漏掉了一批图片——原因是有些图片 URL 形如 xxx.png?e=.png,带了查询参数,直接判断后缀会失败。后来用 re.sub(r"\?.*$", "", src) 先剥掉查询串再判断后缀,问题就解决了。这让我意识到 URL 的结构远比"路径 + 后缀"复杂,正则处理 URL 是爬虫的基本功。
另一个坑是相对路径:页面上 <img src="images/logo.png"> 是相对路径,不能直接拿来请求。借助 urllib.parse.urljoin(base_url, src) 就能把相对路径自动补成绝对 URL,省去自己拼字符串的麻烦还不会出错。下载时用 open(file, "wb") 写二进制、按序号 + 原扩展名命名,配合 os.makedirs(exist_ok=True) 自动建文件夹,整个流程很顺手。我还加了 seen 集合去重,避免同一张图被下载多次。通过这次实验,我对"抓取—解析—下载—落盘"这条完整爬虫链路有了更系统的理解。

浙公网安备 33010602011771号