数据采集与融合技术第一次作业
第一次作业:Python 网络爬虫实践
本文记录使用
requests获取网页、使用lxml解析 HTML 的三个练习。运行前请在该目录执行pip install -r requirements.txt。
作业一:抓取软科 2020 中国大学排名
实验思路:在当前抓取首页的基础上,可以进一步研究排名页面的分页参数,抓取多页学校数据并合并为完整 CSV。随后使用 pandas 按省市或学校类型分组,统计各省市上榜学校数量、不同类型学校的平均分,并绘制柱状图。通过这项拓展,可以练习分页抓取、数据去重和简单的数据分析
1)实验代码与运行结果
本程序解析软科 2020 中国大学排名页当前展示的 30 条记录,提取排名、学校名称、省市、学校类型和总分,并将结果写入 UTF-8-SIG 编码的 CSV 文件,便于用 Excel 查看。脚本完整代码如下:
"""抓取软科 2020 中国大学排名首页,并保存 CSV。"""
import csv
from pathlib import Path
from time import time_ns
import requests
from lxml import html
URL = "https://www.shanghairanking.cn/rankings/bcur/2020"
HEADERS = {
# 请求未压缩页面,兼容没有安装 Brotli 解码库的 Python 环境。
"Accept-Encoding": "identity",
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36"
)
}
OUTPUT_FILE = Path(__file__).with_name("大学排名_2020.csv")
def get_university_rows(url: str = URL) -> list[dict[str, str]]:
"""请求排名页,解析当前展示页的排名、校名、省市、类型和总分。"""
# 给 URL 添加时间戳,避免 CDN 返回 Brotli 压缩的旧缓存变体。
response = requests.get(url, params={"_": time_ns()}, headers=HEADERS, timeout=20)
response.raise_for_status()
# 该站页面使用 UTF-8;requests 在部分环境下会误判为 ISO-8859-1。
response.encoding = "utf-8"
document = html.fromstring(response.text)
results = []
for row in document.xpath("//table//tbody/tr"):
cells = [" ".join(cell.xpath(".//text()" )).strip() for cell in row.xpath("./td")]
if len(cells) < 5 or not cells[0].strip():
continue
# 学校名称所在单元格同时包含英文名和标签,优先取中文校名链接的文本。
school_names = row.xpath("./td[2]//a[contains(@class, 'name-cn')]/text()")
school = " ".join(school_names).strip() if school_names else cells[1].split("\n")[0].strip()
results.append(
{
"排名": cells[0],
"学校名称": school,
"省市": cells[2],
"学校类型": cells[3],
"总分": cells[4],
}
)
if not results:
raise RuntimeError("没有解析到排名数据,网站页面结构可能已经变化。")
return results
def save_csv(rows: list[dict[str, str]], path: Path = OUTPUT_FILE) -> None:
"""将排名结果写入 UTF-8-SIG CSV,方便 Excel 正确显示中文。"""
with path.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=["排名", "学校名称", "省市", "学校类型", "总分"])
writer.writeheader()
writer.writerows(rows)
def main() -> None:
rows = get_university_rows()
save_csv(rows)
print(f"共抓取当前页面 {len(rows)} 所学校,结果已保存到:{OUTPUT_FILE}")
print("排名\t学校名称\t省市\t学校类型\t总分")
for item in rows:
print("\t".join(item.values()))
if __name__ == "__main__":
main()
运行后,首条数据为清华大学、北京、综合、852.5,与题目示例相符。输出文件:大学排名_2020.csv。

2)心得体会
这次练习让我熟悉了用 requests.get 请求网页、检查 HTTP 状态,以及用 XPath 定位表格行列。页面返回的编码有时会被自动识别错,因此需要结合网页实际编码处理。将提取结果保存为 CSV 后,数据更便于复查和整理。当前代码抓取的是排名页面当前展示的一页;若网站调整 HTML 结构,XPath 也需要随之更新。
作业二:抓取当当网“书包”搜索商品
实验思路:可以分别用“书包”“儿童书包”“双肩包”等关键词抓取商品,并将不同关键词的结果合并,再按价格从低到高排序,找出最低价商品和价格区间。也可以清洗价格字段中的货币符号,计算每个关键词的平均价格,并比较不同搜索词下的商品价格分布。进行多关键词搜索时应控制请求频率,并保留关键词列以便区分数据来源
1)实验代码与运行结果
我选择当当网作为商城,以“书包”为关键词搜索,提取商品名称和页面显示价格,并保存为 CSV。脚本代码如下:
"""使用 requests 和 lxml 抓取当当网“书包”搜索结果。"""
import csv
from pathlib import Path
from urllib.parse import urlencode
import requests
from lxml import html
BASE_URL = "https://search.dangdang.com/"
KEYWORD = "书包"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36"
),
"Referer": "https://www.dangdang.com/",
}
OUTPUT_FILE = Path(__file__).with_name("当当书包搜索结果.csv")
def get_products(keyword: str = KEYWORD) -> list[dict[str, str]]:
"""获取搜索结果并提取商品名称和当前显示价格。"""
url = f"{BASE_URL}?{urlencode({'key': keyword, 'act': 'input'})}"
response = requests.get(url, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
document = html.fromstring(response.text)
products = []
for item in document.xpath('//ul[contains(@class, "bigimg")]/li'):
title = item.xpath('string(.//a[@name="itemlist-title"]/@title)').strip()
if not title:
title = " ".join(item.xpath('.//a[@name="itemlist-title"]//text()')).strip()
price = item.xpath('string(.//span[contains(@class, "price_n")])').strip()
if title and price:
products.append({"价格": price, "商品名": title})
if not products:
raise RuntimeError("没有解析到商品,请检查网络或当当网页面结构。")
return products
def save_csv(products: list[dict[str, str]], path: Path = OUTPUT_FILE) -> None:
"""保存商品名称和价格。"""
with path.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=["序号", "价格", "商品名"])
writer.writeheader()
for number, product in enumerate(products, 1):
writer.writerow({"序号": number, **product})
def main() -> None:
products = get_products()
save_csv(products)
print(f"关键词“{KEYWORD}”共抓取 {len(products)} 件商品,结果已保存到:{OUTPUT_FILE}")
print("序号\t价格\t商品名")
for number, product in enumerate(products, 1):
print(f"{number}\t{product['价格']}\t{product['商品名']}")
if __name__ == "__main__":
main()
本次运行解析到 60 件商品,示例结果为“书包女新款初中生高中学生大容量轻便减负护脊简约学院风双肩包”,价格 ¥89.99。输出文件:当当书包搜索结果.csv。


2)心得体会
商城搜索结果页比普通静态网页更容易变化,商品卡片的 XPath 应尽量围绕稳定的属性编写。本次通过商品标题链接的 name 属性和价格元素的 class 获取数据。价格代表抓取时页面显示值,搜索排序和库存价格可能随时间改变;如果网站改版或限制请求,也需要重新检查页面结构和请求条件。
作业三:下载福州大学网页图片
实验思路:下载完成后,可以继续统计图片的文件格式、文件大小和分辨率,并按 JPG、PNG 等格式分类保存。还可以根据图片链接或文件内容计算哈希值,识别重复图片;将过小的图标与较大的新闻配图分开,观察页面中不同图片资源的用途。拓展时可使用 Python 的 Pillow 库读取图片尺寸,并将统计结果保存为 CSV
1)实验代码与运行结果
本程序抓取福州大学“影像福大”页面,读取图片标签中的常见图片地址属性,筛选 .jpg、.jpeg、.png 格式,下载到独立目录。脚本代码如下:
"""下载福州大学“影像福大”页面中 JPEG/JPG/PNG 图片。"""
import re
from pathlib import Path
from urllib.parse import unquote, urljoin, urlparse
import requests
from lxml import html
PAGE_URL = "https://news.fzu.edu.cn/yxfd.htm"
IMAGE_DIR = Path(__file__).with_name("福大网页图片")
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36"
)
}
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png"}
def get_image_urls(page_url: str = PAGE_URL) -> list[str]:
"""解析页面图片链接,兼容懒加载属性及相对路径。"""
response = requests.get(page_url, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
document = html.fromstring(response.text)
image_urls = set()
for image in document.xpath("//img"):
for attribute in ("src", "data-src", "data-original", "data-lazy-src"):
value = image.get(attribute, "").strip()
if not value or value.startswith("data:"):
continue
full_url = urljoin(page_url, value)
extension = Path(unquote(urlparse(full_url).path)).suffix.lower()
if extension in ALLOWED_EXTENSIONS:
image_urls.add(full_url)
return sorted(image_urls)
def download_images(image_urls: list[str], target_dir: Path = IMAGE_DIR) -> int:
"""下载图片到指定目录,保留原扩展名并避免重名覆盖。"""
target_dir.mkdir(parents=True, exist_ok=True)
saved = 0
with requests.Session() as session:
session.headers.update(HEADERS)
for index, image_url in enumerate(image_urls, start=1):
try:
response = session.get(image_url, timeout=20)
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").lower()
if not content_type.startswith("image/"):
continue
extension = Path(unquote(urlparse(image_url).path)).suffix.lower()
if extension not in ALLOWED_EXTENSIONS:
continue
filename = re.sub(r"[^\w.-]", "_", Path(urlparse(image_url).path).name)
if not filename or filename == extension:
filename = f"image_{index}{extension}"
output_path = target_dir / filename
if output_path.exists():
output_path = target_dir / f"{index}_{filename}"
output_path.write_bytes(response.content)
saved += 1
print(f"已保存:{output_path.name}")
except requests.RequestException as error:
print(f"下载失败:{image_url}({error})")
return saved
def main() -> None:
urls = get_image_urls()
count = download_images(urls)
print(f"页面解析到 {len(urls)} 个图片链接,成功下载 {count} 张,目录:{IMAGE_DIR}")
if __name__ == "__main__":
main()
本次从页面解析并成功下载 19 张图片,均保存在 福大网页图片 文件夹。


2)心得体会
图片下载需要先把相对地址转换成绝对地址,再逐个请求并检查响应类型。页面可能采用懒加载,所以程序同时检查了 src、data-src 等属性,并对重复链接去重。把图片统一写入单独文件夹后,便于核对下载结果。网页内容和图片地址可能变化,运行失败时可以先检查页面是否仍能访问,再检查图片标签属性。

浙公网安备 33010602011771号