数据采集第一次作业——requests 定向爬虫实践(软科排名 / 商城比价 / 网页图片)
学号:052404147  姓名:刘怀正

本次作业使用 Python 的 requests 库完成三个定向爬虫任务,分别配合 BeautifulSoup 与 re 完成解析。三个任务的代码、运行结果与心得体会如下。

一、作业①:定向爬取软科 2020 中国大学排名

1.1 作业代码与运行结果

题目要求:用 requests 和 BeautifulSoup 库方法定向爬取 http://www.shanghairanking.cn/rankings/bcur/2020 的数据,屏幕打印爬取的大学排名信息。

实现思路

用 requests.get() 携带 User-Agent 请求头获取网页源码,并手动指定 utf-8 编码(服务器默认返回 ISO-8859-1,不指定会导致中文乱码);
用 BeautifulSoup 解析 HTML,通过 soup.find("table", class_="rk-table") 定位排名表格;
跳过第一行表头,逐行取出 排名 / 学校名称 / 省市 / 学校类型 / 总分;
学校名称单元格里同时包含中文名、英文名与办学层次标签,因此定位到 span.name-cn 才能取到干净的中文校名。
完整代码(hw1_task1_ranking.py)

Python

-- coding: utf-8 --
"""
数据采集 第一次作业 —— 作业①
用 requests 和 BeautifulSoup 定向爬取软科 2020 中国大学排名,屏幕打印排名信息。

实现说明:
目标网址 http://www.shanghairanking.cn/rankings/bcur/2020 的首页静态 HTML 中
已经包含前 30 名的排名表格

,因此可以直接用
requests 获取网页、再用 BeautifulSoup 解析表格得到:排名、学校名称、省市、
学校类型、总分。

学号:052404147 姓名:刘怀正
"""
import sys
import requests
from bs4 import BeautifulSoup

本次作业要爬取的网址
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 Safari/537.36")
}

结果同时输出到屏幕与文件
OUT = open("out_hw1_task1.txt", "w", encoding="utf-8")

def log(*args):
"""同时打印到屏幕和文件"""
s = " ".join(str(a) for a in args)
print(s, file=sys.stdout)
print(s, file=OUT)

def fetch_html(url):
"""用 requests 获取网页 HTML 文本"""
resp = requests.get(url, headers=HEADERS, timeout=20, verify=False)
resp.raise_for_status() # 状态码非 200 时抛出异常
resp.encoding = "utf-8" # 该网页为 utf-8 编码
return resp.text

def parse_ranking(html):
"""
用 BeautifulSoup 解析排名表格,返回列表:
[(排名, 学校名称, 省市, 学校类型, 总分), ...]
"""
soup = BeautifulSoup(html, "lxml")
页面的排名表格 class = "rk-table"
table = soup.find("table", class_="rk-table")
if table is None:
return []

rows = []
trs = table.find_all("tr")[1:] # 第 1 行是表头,从第 2 行开始是数据
for tr in trs:
tds = tr.find_all("td")
if len(tds) < 5:
continue
rank = tds[0].get_text(strip=True) # 排名
学校名称:优先取 span.name-cn,取不到则用校徽图片的 alt 属性
name_tag = tds[1].select_one("span.name-cn")
if name_tag:
name = name_tag.get_text(strip=True)
else:
img = tds[1].find("img")
name = img["alt"].strip() if img else tds[1].get_text(strip=True)
province = tds[2].get_text(strip=True) # 省市
category = tds[3].get_text(strip=True) # 学校类型
score = tds[4].get_text(strip=True) # 总分
rows.append((rank, name, province, category, score))
return rows

if name == "main":
log("=" * 62)
log("作业①:定向爬取软科 2020 中国大学排名")
log("网址:", URL)
log("=" * 62)

html = fetch_html(URL)
log(f"网页获取成功,HTML 长度:{len(html)} 字符")

data = parse_ranking(html)
log(f"共解析出 {len(data)} 所大学的排名信息\n")

打印表头与数据
log(f"{'排名':<6}{'学校名称':<18}{'省市':<8}{'学校类型':<8}{'总分':<8}")
log("-" * 56)
for rank, name, province, category, score in data:
log(f"{rank:<6}{name:<18}{province:<8}{category:<8}{score:<8}")

log("-" * 56)
log(f"爬取完成,共 {len(data)} 条记录。")
OUT.close()
运行结果(打印出前 30 名的完整排名信息)

作业①运行结果
hw1_task1_result

1.2 作业心得

这个题目最大的“坑”是编码。一开始我直接 r.text 得到的中文全是乱码,因为网页响应头里的字符集是 ISO-8859-1,而页面实际是 utf-8 编码,后来显式设置 resp.encoding = "utf-8" 才正常。这让我理解了 requests 的 encoding 与 apparent_encoding 的区别。

第二个收获是定位元素要足够精确。软科的学校名称单元格里混着英文名和“双一流/985/211”这类标签,如果直接取 get_text() 会得到一长串多余文字;通过 select_one("span.name-cn") 精确到中文名所在节点,输出才干净整齐。这让我体会到:用 BeautifulSoup 爬取时,“找到正确的节点”比“写出循环”更关键。

二、作业②:商城商品比价定向爬虫(当当网“书包”)

2.1 作业代码与运行结果

题目要求:用 requests 和 re 库方法设计某个商城(自己选择)商品比价定向爬虫,以关键词“书包”搜索页面的数据,爬取商品名称和价格。

实现思路

选商城:淘宝、京东的搜索页价格由 JavaScript 异步加载,且反爬严格;而当当网的搜索结果页是静态 HTML,商品名和价格都直接写在源码里,非常适合用 requests + re 实现,因此选择当当网。
搜索接口:https://search.dangdang.com/?key= 关键词&act=input&page_index=页码。其中 key 是关键词(需按 GBK 编码放入 URL),page_index 就是翻页参数。
翻页处理:当当网每页固定返回 60 个商品,把 page_index 由 1 递增即可翻页,本程序循环爬取前 2 页共 120 个商品。
正则提取:先按商品块

  • …
  • 切分,再在每块内提取:价格 ¥146.00、商品名

    。
    页面编码为 GBK,因此用 resp.content.decode("gbk") 解码,避免中文乱码。
    完整代码(hw1_task2_mall.py)

    Python

    -- coding: utf-8 --
    """
    数据采集 第一次作业 —— 作业②
    用 requests 和 re 库设计商城(当当网)商品比价定向爬虫:
    以关键词“书包”爬取搜索页面中的商品名称和价格。

    实现说明:
    1) 当当网搜索接口:
    https://search.dangdang.com/?key= 关键词&act=input&page_index=页码
    其中 key 需要按 GBK 编码,page_index 为页码(翻页参数)。
    2) 每页固定返回 60 个商品,用正则从 HTML 源码中提取商品名与价格。
    3) 每个商品位于

  • ...
  • 中,
    价格:¥146.00
    商品名:

    <a title="商品名称" ...>

    学号:052404147 姓名:刘怀正
    """
    import re
    import sys
    import time
    import html as html_lib
    import requests
    from urllib.parse import quote

    ---------------- 基本参数 ----------------
    KEYWORD = "书包" 搜索关键词
    N_PAGES = 2 爬取页数(1 页 = 60 项)
    BASE_URL = "https://search.dangdang.com/" 当当网搜索接口

    HEADERS = {
    "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/120.0 Safari/537.36"),
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Referer": "https://search.dangdang.com/",
    }

    OUT = open("out_hw1_task2.txt", "w", encoding="utf-8")

    def log(*args):
    s = " ".join(str(a) for a in args)
    print(s, file=sys.stdout)
    print(s, file=OUT)

    def build_url(keyword, page):
    """按搜索接口拼接 URL:key 按 GBK 编码,page_index 为页码"""
    return f"{BASE_URL}?key={quote(keyword, encoding='gbk')}&act=input&page_index={page}"

    def get_html(url):
    """用 requests 获取搜索页 HTML(页面编码为 GBK)"""
    resp = requests.get(url, headers=HEADERS, timeout=20, verify=False)
    resp.raise_for_status()
    return resp.content.decode("gbk", errors="ignore")

    每个商品被包在 <li ddt-pit="序号" ...> ... 中
    ITEM_RE = re.compile(r'<li ddt-pit="\d+".?', re.S)
    价格标签:¥146.00
    PRICE_RE = re.compile(r'<span class="price_n"[^>]
    >¥([\d.]+)')
    商品名标签:<p class="name" ...><a title="商品名" ...>
    NAME_RE = re.compile(r'<p class="name"[>]*>\s*<a[>]*?title="([^"]+)"', re.S)

    def parse_items(page_html):
    """用正则从一页 HTML 中解析出 [(商品名, 价格), ...]"""
    items = []
    for block in ITEM_RE.findall(page_html):
    m_price = PRICE_RE.search(block)
    m_name = NAME_RE.search(block)
    if m_price and m_name:
    name = html_lib.unescape(m_name.group(1)).strip()
    items.append((name, float(m_price.group(1))))
    return items

    def cut(s, n):
    """过长的商品名截断显示"""
    return s if len(s) <= n else s[:n] + "…"

    if name == "main":
    log("=" * 74)
    log(f"作业②:当当网商品比价定向爬虫 关键词:{KEYWORD} 页数:{N_PAGES}")
    log("=" * 74)

    all_items = []
    idx = 0
    for page in range(1, N_PAGES + 1):
    url = build_url(KEYWORD, page)
    log(f"\n【第 {page} 页】{url}")
    page_html = get_html(url)
    items = parse_items(page_html)
    log(f" 本页解析出 {len(items)} 个商品:")
    log(f" {'序号':<6}{'价格(元)':<12}{'商品名'}")
    log(" " + "-" * 60)
    for name, price in items:
    idx += 1
    all_items.append((idx, name, price, page))
    log(f" {idx:<6}{price:<12.2f}{cut(name, 38)}")
    if page < N_PAGES:
    time.sleep(1) # 翻页之间稍作停顿

    prices = [p for _, _, p, _ in all_items]
    log("\n" + "=" * 74)
    log(f"爬取完成:共 {len(all_items)} 个商品")
    log(f"价格区间:{min(prices):.2f} ~ {max(prices):.2f} 元,"
    f"平均 {sum(prices) / len(prices):.2f} 元")
    log("=" * 74)
    OUT.close()
    运行结果(第 1 页 60 个商品 / 第 2 页 60 个商品 / 汇总统计)

    作业②运行结果-第1页
    hw1_task2_result_1

    作业②运行结果-第2页
    hw1_task2_result_2

    作业②运行结果-汇总
    hw1_task2_result_3

    2.2 作业心得

    这道题让我第一次真正理解了“搜索接口 + 翻页参数”的爬虫思路。以前我只会爬一个固定页面,而商城的搜索页其实是“一个接口 + 一个页码参数”,只要改变 page_index,就能把成百上千条商品全部采集下来,这正是批量爬虫的基础。

    其次是编码问题:当当网用 GBK 编码,key 参数也必须按 GBK 转成 %CA%E9%B0%FC 才能被正确识别,这一点和作业①的 utf-8 正好相反,让我明白了“先判断编码,再决定怎么解码”是爬虫的必备习惯。

    最后是正则的写法:如果直接在整个页面里匹配价格,很容易和推荐位、广告位的内容错位。我改用“先按商品块

  • 切分,再在块内分别提取价格和名称”的方式,保证了每一行的价格和商品名严格对应。这让我体会到正则“限定作用范围”的重要性。

    三、作业③:爬取给定网页中的所有图片

    3.1 作业代码与运行结果

    题目要求:爬取一个给定网页(https://news.fzu.edu.cn/yxfd.htm )的所有 JPEG、JPG 或 PNG 格式图片文件,保存在一个文件夹中。

    实现思路

    用 requests 获取网页源码,用 resp.apparent_encoding 自动识别编码;
    用 BeautifulSoup 找出所有 标签的 src(兼容 data-src);
    用 urljoin 把相对地址补全为绝对地址(页面里既有 images/logo.png,也有 /__local/xxx.jpg 两种写法);
    判断扩展名前先去掉 ?e=.png 这类查询串,只保留 .jpg/.jpeg/.png,.gif 等其它格式自动跳过;
    逐一下载并用二进制方式写入 images_fzu 文件夹,同时做去重与异常处理。
    完整代码(hw1_task3_images.py)

    Python

    -- coding: utf-8 --
    """
    数据采集 第一次作业 —— 作业③
    爬取给定网页(福州大学新闻网 https://news.fzu.edu.cn/yxfd.htm )中所有的
    JPEG / JPG / PNG 格式图片,并保存到一个文件夹中。

    实现说明:
    1) 用 requests 获取网页源码;
    2) 用 BeautifulSoup 找出页面中所有 标签的 src(含 data-src);
    3) 用 urljoin 把相对地址补全为绝对地址;
    4) 只保留扩展名为 .jpg/.jpeg/.png 的图片(.gif 等其它格式跳过);
    5) 用 requests 逐一下载并保存到 images_fzu 文件夹。

    学号:052404147 姓名:刘怀正
    """
    import os
    import re
    import sys
    import requests
    from bs4 import BeautifulSoup
    from urllib.parse import urljoin

    目标网页
    PAGE_URL = "https://news.fzu.edu.cn/yxfd.htm"
    图片保存目录
    SAVE_DIR = os.path.join(os.path.dirname(os.path.abspath(file)), "images_fzu")
    需要下载的图片格式(题目要求:JPEG、JPG 或 PNG)
    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 Safari/537.36"),
    "Referer": PAGE_URL,
    }

    OUT = open("out_hw1_task3.txt", "w", encoding="utf-8")

    def log(*args):
    s = " ".join(str(a) for a in args)
    print(s, file=sys.stdout)
    print(s, file=OUT)

    def get_image_urls(page_url):
    """获取网页中所有 jpg/jpeg/png 图片的绝对地址"""
    resp = requests.get(page_url, headers=HEADERS, timeout=20, verify=False)
    resp.raise_for_status()
    resp.encoding = resp.apparent_encoding 自动识别编码
    soup = BeautifulSoup(resp.text, "lxml")

    urls = []
    for tag in soup.find_all("img"):
    src = tag.get("src") or tag.get("data-src") or ""
    if not src:
    continue
    去掉 ?e=.png 之类的查询串后再判断扩展名
    if not src.split("?")[0].lower().endswith(IMG_EXTS):
    continue
    full = urljoin(page_url, src) # 相对路径 -> 绝对路径
    if full not in urls: # 去重
    urls.append(full)
    return urls

    def download(urls, save_dir):
    """下载图片并保存到 save_dir,返回保存成功的文件名列表"""
    os.makedirs(save_dir, exist_ok=True)
    saved = []
    for i, url in enumerate(urls, 1):
    try:
    r = requests.get(url, headers=HEADERS, timeout=20, verify=False)
    r.raise_for_status()
    用网址最后一段作为文件名(去掉查询串),并加上序号避免重名
    raw_name = os.path.basename(url.split("?")[0]) or f"image_{i}.jpg"
    safe = re.sub(r"[^\w.-]", "", raw_name)
    filename = f"{i:02d}
    "
    with open(os.path.join(save_dir, filename), "wb") as f:
    f.write(r.content)
    saved.append(filename)
    log(f" [{i:2d}] 已保存 {filename:<46} {len(r.content):>8} 字节")
    except Exception as err:
    log(f" [{i:2d}] 下载失败 {url} -> {err}")
    return saved

    if name == "main":
    log("=" * 78)
    log("作业③:爬取网页中的所有 JPEG/JPG/PNG 图片")
    log("网页:", PAGE_URL)
    log("保存目录:", SAVE_DIR)
    log("=" * 78)

    urls = get_image_urls(PAGE_URL)
    log(f"\n共找到 {len(urls)} 个 JPEG/JPG/PNG 格式图片,开始下载:\n")
    saved = download(urls, SAVE_DIR)

    log("\n" + "=" * 78)
    log(f"下载完成:成功保存 {len(saved)} 个图片文件到 {SAVE_DIR}")
    log("=" * 78)
    OUT.close()
    运行结果(成功下载 19 个 jpg/png 图片,页面中的 loading.gif 被正确过滤)

    作业③下载过程
    hw1_task3_list

    保存目录中的文件清单
    hw1_task3_result

    作业③文件清单

    下载到的图片预览
    hw1_task3_montage

    作业③图片预览

    3.2 作业心得

    这道题的核心难点是相对路径转绝对路径。网页里的图片地址写法千奇百怪:有的是 images/logo.png(相对当前目录),有的是 /__local/xxx.jpg(相对网站根目录),还有的带 ?e=.png 这样的查询串。用 urljoin(page_url, src) 就能统一转换成可以直接下载的完整地址,这比自己用字符串拼接靠谱得多。

    另一个细节是过滤条件要写在正确的位置。如果直接判断整个 src 字符串是否以 .png 结尾,带查询串的图片就会被漏掉;正确做法是先用 split("?")[0] 去掉查询串再判断扩展名。运行结果里 21 个 最终只下载了 19 个(排除了 2 个重复和 1 个 .gif),说明过滤逻辑确实是生效的。

    通过这三个任务,我把 requests 的用法、BeautifulSoup 的节点定位、正则的精确提取和文件下载串联了起来,也养成了先分析网页结构、再动手写代码的习惯。