1024021116蔡靖熹-数据采集第一次作业
一、作业①:基于 requests 与 BeautifulSoup 的大学排名爬虫
1.1 作业代码与运行截图
实验目标: 使用 requests 请求 软科中国大学排名(2020),使用 BeautifulSoup 解析页面中的表格数据,并在终端输出排名、学校名称、省市、学校类型和总分。另将结果保存为 CSV 文件,便于后续查看。
实现思路: 先向目标页面发送 HTTP GET 请求;请求成功后,以 html.parser 解析返回的 HTML;利用 CSS 选择器定位表格行,提取对应列的文本;最后输出到终端并写入 CSV。对于网络失败或页面结构变化的情况,给出提示,避免误认为抓取成功。
源代码:01_university_ranking.py
import csv
from pathlib import Path
import requests
from bs4 import BeautifulSoup
URL = "https://www.shanghairanking.cn/rankings/bcur/2020"
BASE_DIR = Path(__file__).resolve().parent.parent
OUTPUT_FILE = BASE_DIR / "data" / "university_ranking.csv"
def fetch_page(url):
"""请求目标网页并返回 HTML 文本。"""
headers = {
"User-Agent": "Mozilla/5.0",
"Accept-Language": "zh-CN,zh;q=0.9",
}
response = requests.get(url, headers=headers, timeout=20)
response.raise_for_status()
response.encoding = "utf-8"
return response.text
def parse_ranking(html):
"""解析排名表格,提取要求的五个字段。"""
soup = BeautifulSoup(html, "html.parser")
universities = []
rows = soup.select("table.rk-table tbody tr")
for row in rows:
cells = row.find_all("td")
if len(cells) < 5:
continue
rank_tag = cells[0].select_one(".ranking")
name_tag = cells[1].select_one(".name-cn")
rank = (rank_tag.get_text(strip=True)
if rank_tag else cells[0].get_text(strip=True))
name = name_tag.get_text(strip=True) if name_tag else ""
province = cells[2].get_text(strip=True)
school_type = cells[3].get_text(strip=True)
score = cells[4].get_text(strip=True)
if rank and name:
universities.append([rank, name, province, school_type, score])
return universities
def print_ranking(universities):
"""将排名信息输出到终端。"""
print(f"{'排名':<6} {'学校名称':<16} {'省市':<8} {'类型':<8} {'总分'}")
print("-" * 65)
for rank, name, province, school_type, score in universities:
print(f"{rank:<6} {name:<16} {province:<8} {school_type:<8} {score}")
def save_csv(universities):
"""保存抓取结果,采用 utf-8-sig 方便表格软件打开。"""
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
with OUTPUT_FILE.open("w", encoding="utf-8-sig", newline="") as file:
writer = csv.writer(file)
writer.writerow(["排名", "学校名称", "省市", "学校类型", "总分"])
writer.writerows(universities)
print(f"\nCSV 已保存:{OUTPUT_FILE}")
def main():
try:
html = fetch_page(URL)
universities = parse_ranking(html)
if not universities:
print("未解析到排名数据,请检查网页结构。")
return
print_ranking(universities)
print(f"\n成功爬取 {len(universities)} 所大学。")
save_csv(universities)
except requests.RequestException as error:
print(f"网络请求失败:{error}")
if __name__ == "__main__":
main()
运行结果: 程序成功输出排名前 30 所大学,包含五项要求的字段。前三名为清华大学(852.5 分)、北京大学(746.7 分)、浙江大学(649.2 分)。此次抓取的是当前页面展示的 30 条记录,没有实现全部分页抓取。
运行截图:

1.2 作业心得
这次实验让我熟悉了从网页请求到数据提取的基本流程。requests 负责获取网页内容,BeautifulSoup 负责把 HTML 转换为可以按标签和 CSS 选择器查询的结构。解析表格时不能仅依赖固定的文本位置,还需要检查 td 数量,并针对学校名称等字段选择对应标签,以减少页面结构差异带来的错误。
我也注意到终端中文字符的显示宽度与英文字符不同,仅使用字符串宽度格式化时,表格列可能不完全对齐,但不影响数据内容。后续可以改用表格格式化工具输出,或直接使用 CSV 查看。另一个改进方向是遍历排名的所有分页,获取更完整的数据集。
二、作业②:基于 requests 与 re 的书包商品比价爬虫
2.1 作业代码与运行截图
实验目标: 以当当网搜索结果为数据源,以“书包”为检索关键词,使用 requests 获取网页、re 正则表达式提取商品名称和价格,并按价格升序显示,同时保存 CSV 文件。
实现思路: 先下载搜索结果 HTML;通过正则表达式识别每个商品对应的 HTML 区块,再在同一商品区块中分别提取价格和名称,避免商品名称与价格错位;随后使用 Decimal 表示价格、去重并排序。保存网页原文用于出现匹配失败时进行排查。
源代码:02_schoolbag_prices.py
import csv
import html
import re
from decimal import Decimal
from pathlib import Path
import requests
URL = "https://search.dangdang.com/?key=%CA%E9%B0%FC&act=input"
BASE_DIR = Path(__file__).resolve().parent.parent
OUTPUT_FILE = BASE_DIR / "data" / "schoolbag_prices.csv"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 Chrome/120.0 Safari/537.36"
)
}
def fetch_page():
"""发送 GET 请求,获取书包搜索结果页。"""
response = requests.get(URL, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = "gb18030"
return response.text
def parse_products(page_html):
"""使用正则表达式提取商品名称和售价。"""
item_pattern = re.compile(
r'<li\b[^>]*\bddt-pit="[^"]*"[^>]*>.*?</li>', re.S | re.I
)
price_pattern = re.compile(
r'class=["\'][^"\']*\bprice_n\b[^"\']*["\']'
r'[^>]*>\s*(?:¥|¥|¥)?\s*'
r'(\d+(?:\.\d{1,2})?)',
re.S | re.I
)
name_pattern = re.compile(
r'<a\b(?=[^>]*\btitle=["\']([^"\']+)["\'])'
r'(?=[^>]*\bname=["\']itemlist-title["\'])'
r'[^>]*>',
re.S | re.I
)
products = []
seen = set()
for item in item_pattern.findall(page_html):
price_match = price_pattern.search(item)
name_match = name_pattern.search(item)
if not price_match or not name_match:
continue
price = Decimal(price_match.group(1))
name = html.unescape(name_match.group(1)).strip()
if not name or (name, price) in seen:
continue
seen.add((name, price))
products.append((price, name))
return sorted(products, key=lambda product: product[0])
def print_products(products):
"""按价格升序输出商品信息。"""
print(f"{'序号':<6}{'价格':<12}商品名称")
print("-" * 85)
for index, (price, name) in enumerate(products, start=1):
print(f"{index:<6}{str(price):<12}{name}")
print("-" * 85)
print(f"成功提取 {len(products)} 件商品。")
def save_csv(products):
"""保存商品数据。"""
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
with OUTPUT_FILE.open("w", encoding="utf-8-sig", newline="") as file:
writer = csv.writer(file)
writer.writerow(["序号", "价格", "商品名称"])
for index, (price, name) in enumerate(products, start=1):
writer.writerow([index, f"{price:.2f}", name])
print(f"CSV 保存位置:{OUTPUT_FILE}")
def main():
try:
page_html = fetch_page()
raw_file = BASE_DIR / "data" / "dangdang_schoolbag.html"
raw_file.write_text(page_html, encoding="utf-8")
products = parse_products(page_html)
if not products:
print("未提取到商品。请检查网页结构或访问限制。")
print(f"原始 HTML 已保存:{raw_file}")
return
print_products(products)
save_csv(products)
except requests.RequestException as error:
print(f"网络请求失败:{error}")
if __name__ == "__main__":
main()
运行结果: 实际抓取并显示 57 条商品记录,按照价格从低到高排列。截图所示最低价格为 26.50 元,最高价格为 289.00 元。检索结果中也存在与“书包”主题相关性较低的商品,说明商城搜索结果本身可能包含泛化或混合匹配内容,不能将搜索关键词等同于严格的品类筛选条件。
运行截图:

2.2 作业心得
与第一题使用 BeautifulSoup 不同,本题重点练习了正则表达式,包括利用 re.S 跨行匹配、捕获组提取字段以及通过正则条件约束匹配范围。最大的收获是认识到商品数据需要以“单个商品区块”为提取单位,如果先分别提取所有价格和名称再用序号配对,一旦某件商品缺少字段,就可能造成整批数据错位。
在结果分析时,我发现按关键词搜索得到的列表未必全部都是目标品类。因此,价格排序只能表示抓取结果中的价格次序,不能直接用于判断同类书包的性价比。后续可以增加商品类别或标题关键词过滤、分页抓取以及异常价格排查,使比价结果更有参考价值。
三、作业③:福州大学新闻网图片批量下载
3.1 作业代码与运行截图
实验目标: 爬取 福州大学新闻网“影像福大”页面 中引用的 JPEG、JPG 或 PNG 格式图片,并将图片保存到一个文件夹中。
实现思路: 利用 requests 获取网页;利用 BeautifulSoup 查找 <img> 标签,从 src、data-src、data-original 和 srcset 等属性中提取图片链接;借助 urljoin 处理相对链接;下载图片并检查文件头,只保留 JPEG 和 PNG;以图片内容哈希命名以避免重复保存。
源代码:03_image_downloader.py
import hashlib
import time
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
from bs4 import BeautifulSoup
URL = "https://news.fzu.edu.cn/yxfd.htm"
BASE_DIR = Path(__file__).resolve().parent.parent
IMAGE_DIR = BASE_DIR / "data" / "images"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 Chrome/120.0 Safari/537.36"
)
}
MAX_SIZE = 20 * 1024 * 1024
def fetch_page():
"""获取网页 HTML。"""
response = requests.get(URL, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
return response.text
def extract_image_urls(page_html):
"""从 img 元素中提取并补全图片链接。"""
soup = BeautifulSoup(page_html, "html.parser")
image_urls = set()
for img in soup.find_all("img"):
for attr in ("src", "data-src", "data-original"):
source = img.get(attr)
if not source:
continue
source = source.strip()
if source.startswith(("data:", "blob:")):
continue
parsed = urlparse(urljoin(URL, source))
if parsed.scheme in ("http", "https"):
image_urls.add(parsed._replace(fragment="").geturl())
srcset = img.get("srcset", "")
for candidate in srcset.split(","):
parts = candidate.strip().split()
if not parts:
continue
source = parts[0]
if source.startswith(("data:", "blob:")):
continue
parsed = urlparse(urljoin(URL, source))
if parsed.scheme in ("http", "https"):
image_urls.add(parsed._replace(fragment="").geturl())
return sorted(image_urls)
def detect_image_type(data):
"""根据文件头识别 JPG 或 PNG。"""
if data.startswith(b"\xff\xd8\xff"):
return ".jpg"
if data.startswith(b"\x89PNG\r\n\x1a\n"):
return ".png"
return None
def download_image(session, image_url, index):
"""下载、检查图片内容并保存到本地。"""
try:
with session.get(
image_url, timeout=20, stream=True,
headers={"Referer": URL}
) as response:
response.raise_for_status()
content = bytearray()
for chunk in response.iter_content(chunk_size=8192):
if not chunk:
continue
content.extend(chunk)
if len(content) > MAX_SIZE:
print(f"[跳过] 图片超过 20 MB:{image_url}")
return False
image_data = bytes(content)
extension = detect_image_type(image_data)
if extension is None:
print(f"[跳过] 非 JPEG/PNG:{image_url}")
return False
digest = hashlib.sha256(image_data).hexdigest()
filename = f"image_{digest[:16]}{extension}"
file_path = IMAGE_DIR / filename
if file_path.exists():
print(f"[重复] {filename}")
return False
file_path.write_bytes(image_data)
print(f"[成功 {index}] {filename} ({len(image_data) / 1024:.1f} KB)")
return True
except requests.RequestException as error:
print(f"[失败] {image_url}:{error}")
return False
def main():
"""执行网页图片批量下载任务。"""
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
print("=" * 60)
print("福州大学新闻网图片爬虫")
print(f"目标网址:{URL}")
print("=" * 60)
try:
page_html = fetch_page()
except requests.RequestException as error:
print(f"网页请求失败:{error}")
return
image_urls = extract_image_urls(page_html)
print(f"\n发现 {len(image_urls)} 个图片资源地址。\n")
if not image_urls:
print("未找到图片,请检查网页结构。")
return
success_count = 0
with requests.Session() as session:
session.headers.update(HEADERS)
for index, image_url in enumerate(image_urls, start=1):
if download_image(session, image_url, index):
success_count += 1
if index < len(image_urls):
time.sleep(0.3)
print("\n" + "=" * 60)
print("爬取完成")
print(f"发现资源:{len(image_urls)} 个")
print(f"本次新下载:{success_count} 张")
print(f"保存路径:{IMAGE_DIR}")
print("=" * 60)
if __name__ == "__main__":
main()
运行结果: 程序共发现 20 个图片资源地址,其中 19 张 JPEG/PNG 图片成功保存;另有 1 个资源为 loading.gif,不属于本次要求的图片格式,因此跳过。图片统一保存在 data/images/ 文件夹内。
运行截图:

3.2 作业心得
第三题让我了解到网页中的图片并不一定是完整的绝对 URL,有些图片使用相对路径,有些则通过懒加载属性提供地址。因此,爬虫除了遍历 <img> 标签,还需要补全链接并去重。下载环节也不能只根据 URL 的扩展名判断文件类型:部分链接没有扩展名,或者实际返回的是其他格式。检查图片文件头可以更可靠地筛选出 JPEG 和 PNG。
这次运行中,程序发现的 20 个资源并没有全部保存,其中 GIF 格式的加载占位图被正确排除,说明格式检测发挥了作用。同时,通过限制下载大小和适当控制请求间隔,可以降低异常资源和高频请求带来的影响。今后可以进一步扩展到 CSS 背景图、动态加载图片以及新闻详情页,但本次作业只处理指定页面中的图片引用。
浙公网安备 33010602011771号