数据采集第一次作业 —— 定向爬虫
数据采集与融合技术 第一次作业
姓名:姜思源 学号:102402110
我的gitee仓库:aarctic
课程:数据采集与融合技术
本次共三个小作业:①用 requests + BeautifulSoup 爬取软科中国大学排名;②用 requests + re 设计当当网"书包"商品比价爬虫;③爬取指定网页中的所有 JPEG/JPG/PNG 图片并保存到本地。
一、作业①:爬取软科2020中国大学排名
1.1 作业代码与运行结果
(1)实现思路
目标网址为 https://www.shanghairanking.cn/rankings/bcur/2020。排名数据以服务端渲染的 HTML 表格形式存在于页面中:每一条排名记录对应 <tbody> 下的一个 <tr>,其中第 1~5 个 <td> 依次是排名、学校名称、省市、学校类型、总分。

流程分为三步:
- 用
requests携带User-Agent请求头发起 GET 请求,拿到网页源码; - 用
BeautifulSoup以lxml解析器解析 HTML,定位tbody并遍历每个tr; - 从每个
tr中按列提取五个字段,整理成字典后格式化打印。
(2)完整代码
import requests
from bs4 import BeautifulSoup
# 目标网页地址
URL = "https://www.shanghairanking.cn/rankings/bcur/2020"
# 请求头:User-Agent模拟浏览器访问,避免被服务器拒绝
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 get_html(url, timeout=30):
try:
response = requests.get(url, headers=HEADERS, timeout=timeout)
response.raise_for_status() # 状态码非 200 时抛出异常
response.encoding = "utf-8" # 软科页面为 UTF-8 编码
return response.text
except requests.RequestException as exc:
print("网络请求失败:", exc)
return None
def parse_ranking(html):
#用 BeautifulSoup 解析排名表格,返回字典列表。
soup = BeautifulSoup(html, "lxml")
# 排名数据放在 <tbody> 的每一个 <tr> 中
tbody = soup.find("tbody")
if tbody is None:
return []
rankings = []
for tr in tbody.find_all("tr"):
tds = tr.find_all("td")
if len(tds) < 5: # 跳过表头等异常行
continue
try:
rank = tds[0].get_text(strip=True) # 排名
name = tds[1].find("span", class_="name-cn").get_text(strip=True) #校名
province = tds[2].get_text(strip=True) # 省市
univ_type = tds[3].get_text(strip=True) # 学校类型
score = tds[4].get_text(strip=True) # 总分
except AttributeError:
continue
rankings.append({
"排名": rank,
"学校名称": name,
"省市": province,
"学校类型": univ_type,
"总分": score,
})
return rankings
def print_table(data, top=30):
header = "{:<6}{:<18}{:<10}{:<10}{:<10}".format(
"排名", "学校名称", "省市", "学校类型", "总分")
line = "-" * 56
print(line)
print(header)
print(line)
for item in data[:top]:
print("{:<6}{:<18}{:<10}{:<10}{:<10}".format(
item["排名"], item["学校名称"], item["省市"],
item["学校类型"], item["总分"]))
print(line)
print("共爬取到 {} 所高校的排名数据。".format(len(data)))
def main():
html = get_html(URL)
if html is None:
return
rankings = parse_ranking(html)
print("【软科2020中国大学排名top30】")
print_table(rankings, top=30)
if __name__ == "__main__":
main()
(3)运行结果
程序成功打印出排名、学校名称、省市、学校类型、总分五列对齐的表格。
截图如下:
1.2 作业心得
写这个作业最大的体会是:爬虫的三要素是"请求—解析—提取",三者必须各自独立、职责清晰。
一开始我把解析逻辑直接写在主函数里,后来发现 parse_ranking() 单独抽出来以后,既方便单独调试,也方便以后换成别的排名页面复用。第二个体会是异常处理不能省:软科服务器偶尔会返回结构不完整的行(比如表头行、空行),如果不加 len(tds) < 5 的判断和 try/except,程序跑一半就会因为 NoneType 报错中断。
调试中还踩到一个很有意思的坑:同样的代码有时能取到完整 HTML,有时却取到一段乱码。查了响应头才发现服务器返回了 Brotli 压缩的内容,而 requests 默认只支持 gzip/deflate,装上 brotli 库后问题解决。这让我明白,爬虫出问题不一定在代码逻辑上,HTTP 协议层面的细节(编码、压缩、请求头)同样需要关注。
二、作业②:当当网"书包"商品比价爬虫
2.1 作业代码与运行结果
(1)实现思路
自选商城为当当网,搜索接口是 http://search.dangdang.com/?key=书包&act=input。该页面由服务端直接渲染商品列表,每个商品是一个 <li> 节点,价格和名称分别位于 <p class="price"> 和 <p class="name"> 中。

流程:
requests请求搜索页(注意当当搜索页是 GB2312 编码,需手动指定response.encoding);- 用正则先按
<li ... id="商品编号">...</li>切出每一个商品块; - 在每个商品块内再分别用正则匹配价格
<span class="price_n">¥xx.xx</span>和名称<a title="...">; - 清洗名称中的高亮
<font>标签与 HTML 实体,把价格转成浮点数从小到大排序,实现"比价"功能。
(2)完整代码
import os
import re
import sys
import html as html_lib
import requests
# 防止 Windows GBK 控制台遇到生僻字/表情时抛 UnicodeEncodeError,
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
sys.stdout.reconfigure(errors="replace")
# 搜索接口地址与关键词
SEARCH_URL = "http://search.dangdang.com/"
KEYWORD = "书包"
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"
)
}
# 商品条目:<li ... id="商品编号" ...> ... </li>
ITEM_RE = re.compile(r'<li\s[^>]*id="(\d+)"[^>]*>(.*?)</li>', re.S)
# 商品价格:<span class="price_n">¥89.99</span>
PRICE_RE = re.compile(r'<span class="price_n">\s*(.*?)\s*</span>', re.S)
# 商品名称:<p class="name" ...><a title="商品名" ...>
NAME_RE = re.compile(r'<p class="name"[^>]*>\s*<a[^>]*title="([^"]*)"', re.S)
def get_search_page(keyword, page_index=1, timeout=30):
params = {
"key": keyword,
"act": "input",
"page_index": page_index,
}
try:
response = requests.get(SEARCH_URL, params=params,
headers=HEADERS, timeout=timeout)
response.raise_for_status()
response.encoding = "gb2312" # 当当网搜索页为 GB2312 编码
return response.text
except requests.RequestException as exc:
print("网络请求失败:", exc)
return None
def parse_products(html):
"""用正则表达式从搜索页源码中提取商品名称与价格。
:param html: 搜索页 HTML 文本
:return: [{'序号': 1, '价格': '¥89.99', '商品名': '...'}, ...]
"""
products = []
for index, match in enumerate(ITEM_RE.finditer(html), start=1):
item_html = match.group(2)
name_match = NAME_RE.search(item_html)
price_match = PRICE_RE.search(item_html)
if not (name_match and price_match):
continue # 价格或名称缺失的条目跳过
name = re.sub(r"<[^>]+>", "", name_match.group(1))
name = html_lib.unescape(name).strip()
price = html_lib.unescape(price_match.group(1)).strip()
products.append({
"序号": index,
"价格": price,
"商品名": name,
})
return products
def print_products(products, top=20):
line = "-" * 78
print(line)
print("{:<6}{:<12}{}".format("序号", "价格", "商品名"))
print(line)
for item in products:
# 商品名过长时截断,保证表格整齐
name = item["商品名"]
if len(name) > 40:
name = name[:40] + "…"
print("{:<6}{:<12}{}".format(item["序号"], item["价格"], name))
print(line)
print("共爬取到 {} 件商品,已按价格从低到高排序。".format(len(products)))
def save_to_file(products, path="dangdang_shubao.txt"):
with open(path, "w", encoding="utf-8") as fp:
for item in products:
fp.write("{:<6}{:<12}{}\n".format(
item["序号"], item["价格"], item["商品名"]))
fp.write("共爬取到 {} 件商品,已按价格从低到高排序。\n".format(len(products)))
print("完整结果已保存到文件:", os.path.abspath(path))
def main():
html = get_search_page(KEYWORD)
if html is None:
return
products = parse_products(html)
# 把价格字符串转成浮点数后从小到大排序
def to_price(item):
"""把 '¥89.99' 转换成 89.99,无法解析时返回无穷大。"""
number = re.search(r"[\d.]+", item["价格"])
return float(number.group()) if number else float("inf")
products.sort(key=to_price)
for order, item in enumerate(products, start=1):
item["序号"] = order
print("【当当网“{}”搜索结果比价】".format(KEYWORD))
print_products(products)
save_to_file(products)
if __name__ == "__main__":
main()
(3)运行结果
程序共爬取到 60 件"书包"商品,并按价格从低到高排序输出,符合作业要求的"序号 / 价格 / 商品名"输出格式。

2.2 作业心得
这个作业让我真正体会到了正则表达式的威力和局限。
在完成作业②时,我最初选择了京东作为目标,但发现京东反爬机制非常严格,requests 请求返回的是风险拦截页面而非真实数据,导致正则匹配全部失败。后来我改用当当网作为目标商城,并针对当当网的实际 HTML 结构编写正则表达式,终于成功提取到商品名称和价格。
威力在于:当当网的商品列表 HTML 规整且层级不深,用三段正则(切条目 → 取价格 → 取名称)就能干净利落地拿到数据,比一层层写 DOM 查找还直观。局限在于:正则对结构变化非常敏感,比如商品名称里混着 <font class="skcolor_ljg">书包</font> 这样的关键词高亮标签,如果不先做 re.sub(r"<[^>]+>", "", ...) 清洗,名称里就会带着一串 HTML 标签;再比如价格实体写的是 ¥ 而不是 ¥,必须用 html.unescape() 反转义。这些都是"拿到数据"和"拿到干净数据"之间的差距。
另外两个印象深刻的点:一是编码问题,当当搜索页是 GB2312,如果直接用默认编码解码,中文全是乱码,response.encoding = "gb2312" 一行代码解决了大问题;二是"比价"的本质其实就是把非结构化的价格文本转成可计算的数值再排序,re.search(r"[\d.]+", price) 这个小函数虽然只有两行,却是整个爬虫从"展示"走向"分析"的关键一步。
三、作业③:爬取网页中的所有图片并保存到本地
3.1 作业代码与运行结果
(1)实现思路
目标网页为福州大学新闻网"院系动态"栏目 https://news.fzu.edu.cn/yxfd.htm。页面中的图片以 <img src="..."> 标签引用,既有相对路径(如 images/logo.png)也有站点根路径(如 /__local/.../xxx.jpg)。

流程:
requests获取网页源码;- 用正则
r'<img[^>]+src=["\']([^"\']+)["\']'提取所有src属性; - 用
urllib.parse.urljoin把相对地址补全为绝对地址,再按扩展名过滤出 .jpg / .jpeg / .png(过滤时先去掉?e=.png这类查询参数),并用集合去重; - 逐个下载图片,以
wb二进制模式写入./images文件夹(目录不存在时用os.makedirs(..., exist_ok=True)自动创建)。
(2)完整代码
import os
import re
import requests
from urllib.parse import urljoin, urlparse
# 目标网页与图片保存目录
PAGE_URL = "https://news.fzu.edu.cn/yxfd.htm"
SAVE_DIR = "images"
# 支持的图片格式
IMG_EXTS = (".jpg", ".jpeg", ".png")
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 ... src="..."> 中的 src 属性
IMG_SRC_RE = re.compile(r'<img[^>]+src=["\']([^"\']+)["\']', re.I)
def get_html(url, timeout=30):
"""请求网页并返回 HTML 文本。
:param url: 网页地址
:param timeout: 超时时间(秒)
:return: HTML 文本;失败返回 None
"""
try:
response = requests.get(url, headers=HEADERS, timeout=timeout)
response.raise_for_status()
response.encoding = response.apparent_encoding
return response.text
except requests.RequestException as exc:
print("网页请求失败:", exc)
return None
def collect_image_urls(html, base_url):
"""从 HTML 中提取所有 jpg/jpeg/png 图片的绝对地址(自动去重)。
:param html: 网页 HTML 文本
:param base_url: 网页地址,用于把相对路径补全为绝对路径
:return: 去重后的图片绝对地址列表
"""
urls = []
seen = set()
for src in IMG_SRC_RE.findall(html):
absolute = urljoin(base_url, src)
path = urlparse(absolute).path.lower()
if path.endswith(IMG_EXTS) and absolute not in seen:
seen.add(absolute)
urls.append(absolute)
return urls
def download_images(urls, save_dir=SAVE_DIR):
"""逐一下载图片并保存到本地目录。
:param urls: 图片绝对地址列表
:param save_dir: 保存目录
:return: 成功保存的文件名列表
"""
os.makedirs(save_dir, exist_ok=True) # 目录不存在则自动创建
saved = []
for index, url in enumerate(urls, start=1):
filename = os.path.basename(urlparse(url).path) or "image_{}.jpg".format(index)
filepath = os.path.join(save_dir, filename)
try:
response = requests.get(url, headers=HEADERS, timeout=30)
response.raise_for_status()
with open(filepath, "wb") as fp:
fp.write(response.content)
saved.append(filename)
print(" [%2d] 已保存 %-45s (%d 字节)" % (index, filename, len(response.content)))
except requests.RequestException as exc:
print(" [%2d] 下载失败 %-45s %s" % (index, url, exc))
return saved
def main():
"""主函数:抓取网页 → 提取图片地址 → 下载保存。"""
print("正在请求网页:", PAGE_URL)
html = get_html(PAGE_URL)
if html is None:
return
urls = collect_image_urls(html, PAGE_URL)
print("共发现 {} 张 JPEG/JPG/PNG 图片,开始下载……".format(len(urls)))
saved = download_images(urls)
print("下载完成,成功保存 {} 张图片到目录:{}".format(len(saved), os.path.abspath(SAVE_DIR)))
if __name__ == "__main__":
main()
(3)运行结果
程序从页面中识别出 19 张 JPEG/JPG/PNG 图片,全部成功下载并保存到 images 文件夹中。
运行结果:

图片保存结果:

3.2 作业心得
这个作业收获主要有三点。
第一是相对路径的处理。网页里的图片地址五花八门:images/logo.png 是相对当前页面的路径,/__local/... 是相对站点根目录的路径,还有的图片地址后面带着 ?e=.png 这种查询参数。一开始我只用字符串拼接,结果拼出一堆 404;后来改用 urljoin(base_url, src) 自动补全,并在判断扩展名时先用 urlparse() 去掉查询参数,问题才彻底解决。
第二是图片必须以二进制方式保存。最开始我下意识地用 response.text 再 encode() 写文件,结果大图全部损坏;改用 response.content 配合 open(filepath, "wb") 之后,所有图片都能正常打开。这让我对"文本"和"二进制"两种资源在爬虫里的区别有了实感。
第三是去重和容错。同一个 logo 在页眉页脚出现多次,如果不去重就会重复下载;而下载过程中任何一张图失败都不应该影响其他图片,所以我把异常处理放到了循环内部,保证"单张失败,整体继续"。这种批量任务中的局部容错思想,以后做大规模采集时一定用得上。
四、总结
三个作业分别对应了定向爬虫最核心的三个点:
| 作业 | 技术组合 | 关键难点 |
|---|---|---|
| ①大学排名 | requests + BeautifulSoup | HTML 结构定位、Brotli 压缩 |
| ②商品比价 | requests + re | 正则提取、编码转换、数据清洗 |
| ③图片爬取 | requests + re + 文件 IO | 相对路径补全、二进制保存、去重容错 |
做完这三个作业,我对爬虫的理解发生了不小的变化。
以前我觉得爬虫就是"把网页上的东西抓下来",做完之后才明白,爬虫的本质是一场与目标网站的持续博弈。你写代码的同时,对方也在不断更新反爬机制、调整页面结构、更换编码格式。作业②里我一开始选京东,结果请求返回的是一个风险拦截页面——这就是最直观的一课:选对目标,比写对代码更重要。写爬虫之前,先打开浏览器按 Ctrl+U 看看源码结构、判断页面是静态渲染还是动态加载、检查有没有反爬特征,这些"准备工作"反而比敲代码更关键。
另一个深刻的感受是:爬虫代码写起来不难,难的是让它稳定、可持续地运行。异常处理、超时设置、请求间隔、编码适配、路径补全、去重容错——这些看似"边角料"的东西,恰恰决定了一个爬虫能不能在实际场景中真正跑起来。这大概也是"数据采集与融合技术"这门课想让我们体会的东西:技术只是工具,工程思维才是核心。
这次作业给我最大的收获,不是学会了 requests 和 re 的用法,而是养成了一个习惯:写爬虫之前先看源码,写完之后先想异常。这个习惯,比任何一段代码都更有价值。
数据采集这条路,我才刚刚起步。下一次,我会带着这三个作业踩过的坑,继续往前走。


浙公网安备 33010602011771号