数据采集第一次作业

数据采集第一次作业

实验环境:Windows 11(64 位)|Python 3.13.2|requests 2.32.5|beautifulsoup4 4.13.5|lxml 6.0.2|编辑工具 VS Code

本次作业三个题目分别练三件事:用 bs4 解析表格、用正则解析列表、批量下载文件。三份代码都按函数拆分,关键处写了注释说明「为什么这么写」而不是「这行在干嘛」。

作业①:爬取软科中国大学排名

1)、爬取软科大学排名实验

目标:爬取 http://www.shanghairanking.cn/rankings/bcur/2020,按「排名 / 学校名称 / 省市 / 学校类型 / 总分」打印。

实验思路

页面上的排名是一张标准表格,所以走 requests 下载 → BeautifulSoup 解析 → find_all("tr") 逐行取 <td> 的路线。三步里真正麻烦的是两件事:

一是编码。这个站点的响应头里没有声明 charset,requests 会按 ISO-8859-1 去猜,中文会全乱。解决办法是把 resp.encoding 手动设成 resp.apparent_encoding,让它按内容自动探测。

二是「学校名称」那一格的清洗。这一格的真实内容长这样:

<td class="align-left">
    <img alt="清华大学" class="univ-logo" src="...">
    <div class="univname">清华大学 Tsinghua University 双一流/985/211</div>
</td>

直接 get_text() 出来是 清华大学 Tsinghua University 双一流/985/211,和题目要求的「清华大学」差得远。一开始想用空格切分取第一段,但这是靠格式碰运气——万一以后出现带空格的中文校名就错了。后来发现中文校名同时写在校徽图片的 alt 属性里,取 alt 才是最干净的来源。

完整代码

# -*- coding: utf-8 -*-
r"""实践作业 · 作业①:定向爬取软科中国大学排名

目标网页:http://www.shanghairanking.cn/rankings/bcur/2020
运行方式:python 01_爬取大学排名.py

输出字段:排名 / 学校名称 / 省市 / 学校类型 / 总分

整体思路:
    1. requests 请求页面,拿到 HTML
    2. BeautifulSoup 解析,找出表格里的所有 <tr>
    3. 跳过第一行表头,逐行取出 5 个 <td>
    4. 其中「学校名称」那一格混了中英文和标签,需要单独处理
    5. 按对齐的格式打印

说明:这个页面的 HTML 里服务端只渲染了前 30 条数据(页面自己写着「共 567 所学校」,
      翻页是前端 JavaScript 做的),所以本程序抓取并打印的是第 1 页的 30 所高校。
"""
import re
import sys

import requests
from bs4 import BeautifulSoup

# ==================== 可配置项 ====================
PAGE_URL = "http://www.shanghairanking.cn/rankings/bcur/2020"
TIMEOUT = 30                                    # 请求超时时间(秒)

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"),
    "Accept-Language": "zh-CN,zh;q=0.9",
}

# 每一列在 <tr> 里对应的 <td> 下标,写成常量方便日后页面改版时调整
COL_RANK = 0        # 排名
COL_NAME = 1        # 学校名称(混合了中英文与标签)
COL_PROVINCE = 2    # 省市
COL_TYPE = 3        # 学校类型
COL_SCORE = 4       # 总分


def fetch_page(url):
    """请求页面并返回 HTML 文本,失败时返回 None。"""
    session = requests.Session()
    session.headers.update(HEADERS)
    # 不让 requests 自动使用系统代理:本机环境变量里的代理端口不固定,直连更稳
    session.trust_env = False

    try:
        resp = session.get(url, timeout=TIMEOUT)
        # 这个站点响应头里没有声明 charset,requests 会按 Latin-1 去猜,
        # 中文就会乱码,所以必须手动指定成自动探测到的编码
        resp.encoding = resp.apparent_encoding or "utf-8"
        return resp.text
    except requests.RequestException as err:
        print("请求失败:%s" % err)
        return None


def get_university_name(cell):
    """从「学校名称」单元格里提取中文校名。

    这一格的内容长这样:<img alt="清华大学"> 清华大学 Tsinghua University 双一流/985/211
    直接 get_text() 得到的是「清华大学 Tsinghua University 双一流/985/211」,
    不符合输出要求。

    中文校名同时也写在网站校徽图片的 alt 属性里,取 alt 最干净;
    万一某一行没有图片,再退回到「取第一个空格前的部分」这种兜底做法。
    """
    img = cell.find("img")
    if img and img.get("alt"):
        return img["alt"].strip()

    text = cell.get_text(" ", strip=True)
    return text.split(" ")[0] if text else ""


def parse_ranking(html):
    """解析 HTML,返回排名列表。

    参数:
        html -- 页面 HTML 文本
    返回:
        list,元素为 dict,含 rank / name / province / type / score 五个键
    """
    soup = BeautifulSoup(html, "html.parser")
    rows = []

    for tr in soup.find_all("tr"):
        tds = tr.find_all("td")
        if len(tds) <= COL_SCORE:
            continue        # 表头行或被合并的行,列数不够,跳过

        rank_text = tds[COL_RANK].get_text(strip=True)
        # 排名列必须是纯数字,用这个条件把表头行排除掉
        if not rank_text.isdigit():
            continue

        rows.append({
            "rank": rank_text,
            "name": get_university_name(tds[COL_NAME]),
            "province": tds[COL_PROVINCE].get_text(strip=True),
            "type": tds[COL_TYPE].get_text(strip=True),
            "score": tds[COL_SCORE].get_text(strip=True),
        })

    return rows


def pad(text, width):
    """按显示宽度补齐空格。

    中文在终端里占两个字符宽度,str.ljust 是按字符数算的,中英混排会错位,
    所以这里自己按显示宽度算一遍,保证表格整齐。
    """
    display = sum(2 if ord(ch) > 0x2E80 else 1 for ch in text)
    return text + " " * max(0, width - display)


def main():
    """程序入口:请求页面 -> 解析表格 -> 打印结果。"""
    print("目标网页:%s" % PAGE_URL)
    print()

    html = fetch_page(PAGE_URL)
    if not html:
        print("页面获取失败,程序退出。")
        sys.exit(1)

    rows = parse_ranking(html)
    if not rows:
        print("没有解析到排名数据,可能是页面结构变了。")
        sys.exit(1)

    # 表头与数据行用同一套列宽,保证上下对齐
    widths = (8, 26, 10, 12)        # 依次对应 排名 / 学校名称 / 省市 / 学校类型
    header = (pad("排名", widths[0]) + pad("学校名称", widths[1])
              + pad("省市", widths[2]) + pad("学校类型", widths[3]) + "总分")

    print("共解析到 %d 所高校(页面 HTML 中服务端渲染的部分)" % len(rows))
    print("=" * 68)
    print(header)
    print("-" * 68)

    for row in rows:
        print(pad(row["rank"], widths[0]) + pad(row["name"], widths[1])
              + pad(row["province"], widths[2]) + pad(row["type"], widths[3])
              + row["score"])

    print("-" * 68)
    print("打印完毕,共 %d 条。" % len(rows))


if __name__ == "__main__":
    main()

运行结果

01_软科排名运行结果

30 所高校全部打印出来,四列对齐,第一条是清华大学(北京 / 综合 / 852.5)。

另外说明一点:页面自己写着「共 567 所学校」,但 HTML 里服务端只渲染了前 30 条,翻页是前端 JavaScript 做的。所以按题目「爬取给定网址(一个页面)」的要求,程序抓到的就是这 30 条。

2)、心得体会

第一是不要用格式去冒险。我一开始用 text.split(" ")[0] 取校名,能跑通,但心里没底——这相当于赌"所有中文校名都不含空格"。后来发现 alt 属性里存着原始中文名,这个来源是语义上正确的,不是碰巧对的。同样一份数据,能找到一个稳定的来源,就别用格式技巧。第二是编码问题必须显式处理。第一次跑出来全是乱码,我第一反应是"网站反爬了",其实是响应头没给 charset。这让我意识到:乱码、空页面、数据缺失,这些现象的第一嫌疑人应该是自己的请求参数,而不是对方的反爬策略。

作业②:商城商品比价爬虫

1)、商品比价爬虫实验

目标:用 requests + re 爬取某个商城「书包」搜索页的商品名称和价格,按「序号 / 价格 / 商品名」输出。

选商城的经过

题目允许自选商城,但我先实测了一遍,因为不同商城的反爬强度差得很远:

商城 实测结果 能否用 requests
淘宝 返回 34 KB 的 HTML,内容全是前端骨架屏,商品数据不在里面 否
京东 302 重定向,响应体 0 字节 否
当当 响应 245 KB,商品名称和价格完整写在 HTML 里,每页 60 件 是

淘宝的页面是前端渲染的,requests 拿到的只是一个空壳;京东直接跳转登录。最后选了当当——服务端渲染、结构清晰、每页正好 60 件,和题目要求的数量也对得上。

正则的三层结构

页面的商品结构是这样的(简化后):

<li class="line1" id="商品编号">
    <p class="price"><span class="price_n">&yen;89.99</span></p>
    <p class="name" name="title">
        <a title="书包女新款初中生高中学生大容量轻便减负护脊简约学院风双肩包">...</a>
    </p>
</li>

所以正则分三层写,一层比一层细:

ITEM_RE        = re.compile(r'<li[^>]*class="line\d+"[^>]*>.*?</li>', re.S)      # 先切出商品块
NAME_RE        = re.compile(r'<p class="name"[^>]*>\s*<a[^>]*title="([^"]*)"')    # 再取名称
PRICE_BLOCK_RE = re.compile(r'<p class="price"[^>]*>(.*?)</p>', re.S)            # 价格块
PRICE_RE       = re.compile(r"[¥¥]\s*[\d.,]+")                                  # 从块里挑金额

先切块再取字段,比用一个超长正则一次性匹配「价格+名称」要稳——万一某个商品缺价格,只影响它自己,不会让后面的匹配全部错位。

完整代码

# -*- coding: utf-8 -*-
r"""实践作业 · 作业②:商城商品比价定向爬虫

商城选择:当当网( https://www.dangdang.com )
关键词:书包
运行方式:python 02_商品比价.py

为什么选当当:
    动手前实测过几个商城,淘宝的搜索结果页返回的是前端骨架屏,商品数据不在 HTML 里;
    京东会 302 跳转到登录页;当当的搜索结果是服务端渲染的,商品名称和价格都完整写在
    HTML 中,用 requests + 正则就能拿到,适合这道题。

整体思路:
    1. requests 请求搜索结果页(页面是 GBK 编码,要手动指定)
    2. 用正则把页面切成一个个商品块 <li class="line1">...</li>
    3. 在每个块里再取商品名称(<a> 的 title 属性)和价格(<span class="price_n">)
    4. 按「序号 / 价格 / 商品名」的格式打印
"""
import re
import sys

import requests

# ==================== 可配置项 ====================
KEYWORD = "书包"                              # 搜索关键词
PAGE_INDEX = 1                                # 要抓第几页(每页 60 件)
PAGE_SIZE = 60                                # 当当搜索页每页固定 60 件商品
TIMEOUT = 30                                  # 请求超时时间(秒)

SEARCH_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.0.0 Safari/537.36"),
    "Accept-Language": "zh-CN,zh;q=0.9",
}

# ==================== 三个正则 ====================
# 第一层:切出一个完整的商品块。class 可能是 line1、line2…… 所以用 line\d+
ITEM_RE = re.compile(r'<li[^>]*class="line\d+"[^>]*>.*?</li>', re.S)

# 第二层之一:商品名称。
# 不能直接取 <a> 里的文本,因为关键词会被 <font class="skcolor_ljg"> 包起来,
# 取文本会夹杂标签;title 属性里存的是干净的完整名称
NAME_RE = re.compile(r'<p class="name"[^>]*>\s*<a[^>]*title="([^"]*)"', re.S)

# 第二层之二:价格所在的整块,再从里面挑金额
PRICE_BLOCK_RE = re.compile(r'<p class="price"[^>]*>(.*?)</p>', re.S)
PRICE_RE = re.compile(r"[¥¥]\s*[\d.,]+")

# 页面里的 HTML 实体,打印前需要还原成真实字符
ENTITIES = {"&yen;": "¥", "&nbsp;": " ", "&amp;": "&",
            "&quot;": '"', "&lt;": "<", "&gt;": ">"}


def unescape(text):
    """把常见的 HTML 实体还原成真实字符(主要是 &yen; 要变成 ¥)。"""
    for entity, real in ENTITIES.items():
        text = text.replace(entity, real)
    return text


def build_search_url(keyword, page_index):
    """拼出搜索页地址。

    两个要点:
      1. 中文关键词必须做 URL 编码,直接拼进 URL 会出错;
      2. 翻页参数是 page_index,不是 page —— 用 page=2 时服务端仍然返回第 1 页。
    """
    encoded = requests.utils.quote(keyword)      # 「书包」-> %E4%B9%A6%E5%8C%85
    return "%s?key=%s&page=1&page_index=%d" % (SEARCH_URL, encoded, page_index)


def fetch_page(url):
    """请求搜索页并返回 HTML 文本,失败时返回 None。"""
    session = requests.Session()
    session.headers.update(HEADERS)
    # 不让 requests 自动读取系统代理:本机环境变量里的代理端口不固定,直连更稳
    session.trust_env = False

    try:
        resp = session.get(url, timeout=TIMEOUT)
        # 这个页面是 GBK(响应头里写 gb2312),不手动指定会中文乱码
        resp.encoding = "gbk"
        return resp.text
    except requests.RequestException as err:
        print("请求失败:%s" % err)
        return None


def parse_items(html):
    """从页面 HTML 中解析出所有商品。

    参数:
        html -- 搜索页 HTML 文本
    返回:
        list,元素为 dict,含 name(商品名称)和 price(价格字符串)
    """
    items = []
    for block in ITEM_RE.findall(html):
        name_match = NAME_RE.search(block)
        price_match = PRICE_BLOCK_RE.search(block)
        if not name_match or not price_match:
            continue        # 缺名称或缺价格(比如广告位)就跳过

        # 名称:取 title 属性并还原实体
        name = unescape(name_match.group(1)).strip()

        # 价格:先把价格块里的标签去掉,再从中挑出 ¥ 金额
        raw = re.sub(r"<[^>]+>", " ", price_match.group(1))
        amount = PRICE_RE.search(unescape(raw))
        price = amount.group(0).replace(" ", "") if amount else ""

        if not name or not price:
            continue

        # 价格里只留数字和点,方便排序统计,也符合题目示例中「65.00」的写法
        price = price.replace("¥", "").replace("¥", "").replace(",", "")
        items.append({"name": name, "price": price})

    return items


def pad(text, width):
    """按显示宽度补齐空格。

    中文在终端里占两个字符宽度,直接用 str.ljust 会因为按字符数计算而错位,
    所以自己算一遍显示宽度,让「商品名」这一列能对齐。
    """
    display = sum(2 if ord(ch) > 0x2E80 else 1 for ch in text)
    return text + " " * max(0, width - display)


def main():
    """程序入口:请求搜索页 -> 解析 -> 打印。"""
    url = build_search_url(KEYWORD, PAGE_INDEX)
    print("商城:当当网")
    print("关键词:%s" % KEYWORD)
    print("搜索页:%s" % url)
    print()

    html = fetch_page(url)
    if not html:
        print("页面获取失败,程序退出。")
        sys.exit(1)

    items = parse_items(html)[:PAGE_SIZE]
    if not items:
        print("没有解析到商品,可能是页面结构变了。")
        sys.exit(1)

    print("共获取到 %d 件商品:" % len(items))
    print("=" * 74)
    print(pad("序号", 6) + pad("价格", 12) + "商品名")
    print("-" * 74)

    for index, item in enumerate(items, 1):
        print(pad(str(index), 6) + pad(item["price"], 12) + item["name"])

    print("-" * 74)
    print("本页共 %d 件商品。" % len(items))


if __name__ == "__main__":
    main()

运行结果

02_商品比价_上

02_商品比价_下

60 件商品全部拿到,价格从 26.50 元到 318.60 元不等。

第 8~10 条爬出来是《仙逆》小说,不是书包。这不是程序的 bug——当当搜索页本身混了推广位,页面里就是这些内容。定向爬虫的职责是「忠实取下页面内容」,判断相关性是另一回事。

2)、心得体会

第一,商品名称不能直接取标签文本。当当会把命中的关键词用 <font class="skcolor_ljg"> 单独框起来做高亮,如果取 <a> 里的文字,得到的是夹着标签的一串;而 title 属性里存的是完整原始名称,取它最省事。这和作业①取 alt 是同一个道理:同一个信息在 HTML 里往往存了两份,挑那份没被加工过的。第二,正则要分层。我第一版想用一个正则直接匹配「价格 + 名称」,写出来又长又难调,而且一个商品结构有偏差就会连累后面所有匹配。改成分层之后,逻辑一眼能看懂,出问题也好定位是哪一层没匹配上。

作业③:爬取网页中的所有图片

1)、网页图片爬取实验

目标:爬取 https://news.fzu.edu.cn/yxfd.htm 页面中所有 JPEG / JPG / PNG 图片,保存到本地文件夹。

实验思路

思路本身不复杂:requests 拿页面 → BeautifulSoup 找所有 <img> → 判断后缀 → 下载保存。但实际写下来,有三个地方如果不注意就会白干:

第一,src 是相对路径。页面里既有 images/logo.png 这种相对路径,也有 /__local/... 这种根路径。直接拿去请求会拼出错误的地址,必须用 urljoin(base_url, src) 统一转成绝对地址。

第二,有的地址带查询串。页面里有三个图片地址长这样:xxx_1F7.png?e=.png。如果用 endswith(".png") 判断,因为结尾是 =png 而不是 .png,这三个会被直接漏掉。所以判断前必须先按 ? 切开,只看路径部分:

path = url.split("?")[0].lower()
return path.endswith((".jpg", ".jpeg", ".png"))

完整代码

# -*- coding: utf-8 -*-
r"""实践作业 · 作业③:爬取给定网页中的所有图片并保存到本地

目标网页:https://news.fzu.edu.cn/yxfd.htm (福州大学新闻网 - 院系风采)
运行方式:python 03_下载网页图片.py

整体思路:
    1. 用 requests 请求页面,拿到 HTML
    2. 用 BeautifulSoup 找出页面里所有 <img> 标签
    3. 逐个判断 src 是否以 jpg / jpeg / png 结尾(判断前先切掉 ? 后面的查询串)
    4. src 可能是相对路径,用 urljoin 拼成完整的绝对地址
    5. 用二进制方式写入本地文件夹,文件名沿用服务器上的原文件名

注意:题目要求「爬取所有 JPEG、JPG 或 PNG 格式图片」,
      所以本程序不做内容筛选,页面里出现的图片会全部下载
      (其中包含若干个站点图标,属于题目字面要求范围内)。
"""
import os
import sys
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

# ==================== 可配置项 ====================
PAGE_URL = "https://news.fzu.edu.cn/yxfd.htm"   # 目标网页地址
SAVE_DIR_NAME = "images"                        # 图片保存目录名(建在本脚本同级目录下)
ALLOWED_EXT = (".jpg", ".jpeg", ".png")         # 题目要求的三类后缀
TIMEOUT = 30                                    # 单次请求超时时间,单位秒

# 请求头:很多站点不设 User-Agent 会直接返回 403,Referer 则用于通过防盗链校验
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"),
    "Referer": PAGE_URL,
}


def fetch_html(url, session):
    """请求网页并返回解码后的 HTML 文本。

    参数:
        url     -- 要请求的网页地址
        session -- 复用的 requests.Session,可以少建几次 TCP 连接
    返回:
        str,页面 HTML;请求失败时返回 None
    """
    try:
        resp = session.get(url, timeout=TIMEOUT)
        # 站点响应头里不一定声明字符集,这里让 requests 根据内容自动判断编码,
        # 否则可能出现中文乱码
        resp.encoding = resp.apparent_encoding or "utf-8"
        return resp.text
    except requests.RequestException as err:
        print("请求网页失败:%s" % err)
        return None


def is_image_url(url):
    """判断一个地址是否指向题目要求的三种图片格式。

    这里有个坑:页面里有形如 xxx.png?e=.png 的地址,带查询串。
    如果直接 endswith(".png") 会因为最后是 "=png" 而判断失败,
    所以要先按 ? 切开,只检查路径部分。
    """
    path = url.split("?")[0].lower()        # 去掉查询串并统一小写
    return path.endswith(ALLOWED_EXT)


def extract_image_urls(html, base_url):
    """从 HTML 中提取所有图片的完整地址。

    参数:
        html      -- 页面 HTML 文本
        base_url  -- 页面本身的地址,用于把相对路径拼成绝对路径
    返回:
        list,元素为 (绝对地址, 原文件名) 的元组;已去重
    """
    soup = BeautifulSoup(html, "html.parser")
    found = []
    seen = set()                            # 用于去重,页面上可能有重复引用的图

    for img in soup.find_all("img"):
        src = (img.get("src") or "").strip()
        if not src:
            continue                        # 个别 img 没有 src,跳过
        if src.startswith("data:"):
            continue                        # base64 内嵌图片,不是文件,跳过

        # src 可能是 "images/logo.png" 这种相对路径,也可能是 "/__local/..." 这种根路径,
        # urljoin 会按标准规则统一拼成绝对地址
        absolute = urljoin(base_url, src)
        if not is_image_url(absolute):
            continue
        if absolute in seen:
            continue

        seen.add(absolute)
        # 文件名取地址最后一段(同样要先去掉查询串)
        filename = os.path.basename(absolute.split("?")[0])
        found.append((absolute, filename))

    return found


def save_image(url, filename, save_dir, session):
    """下载单张图片并保存到本地。

    参数:
        url       -- 图片绝对地址
        filename  -- 保存时使用的文件名
        save_dir  -- 保存目录
        session   -- 复用的 requests.Session
    返回:
        True 表示保存成功,False 表示失败
    """
    try:
        resp = session.get(url, timeout=TIMEOUT)
        if resp.status_code != 200:
            print("    跳过(HTTP %s):%s" % (resp.status_code, filename))
            return False
        # 有的站点会用 200 返回一个错误页,这里再用 Content-Type 兜一层
        if not resp.headers.get("Content-Type", "").startswith("image/"):
            print("    跳过(不是图片):%s" % filename)
            return False

        path = os.path.join(save_dir, filename)
        # 图片是二进制数据,必须用 "wb" 写入,用文本模式会损坏文件
        with open(path, "wb") as fp:
            fp.write(resp.content)
        print("    已保存(%6.1f KB):%s" % (len(resp.content) / 1024, filename))
        return True
    except requests.RequestException as err:
        print("    下载失败:%s(%s)" % (filename, err))
        return False


def main():
    """程序入口:抓页面 -> 找图片 -> 逐张下载 -> 输出统计。"""
    # 保存目录建在脚本所在目录下,这样从任何位置运行结果都一致
    base_dir = os.path.dirname(os.path.abspath(__file__))
    save_dir = os.path.join(base_dir, SAVE_DIR_NAME)
    os.makedirs(save_dir, exist_ok=True)     # 已存在时不会报错

    print("目标网页:%s" % PAGE_URL)
    print("保存目录:%s" % save_dir)
    print()

    session = requests.Session()
    session.headers.update(HEADERS)
    # 关闭代理自动读取:本机环境变量里的代理端口不固定,直连更可靠
    session.trust_env = False

    html = fetch_html(PAGE_URL, session)
    if not html:
        print("页面获取失败,程序退出。")
        sys.exit(1)

    images = extract_image_urls(html, PAGE_URL)
    print("页面中符合 JPEG/JPG/PNG 格式的图片共 %d 张:" % len(images))
    for idx, (url, name) in enumerate(images, 1):
        print("  %2d. %s" % (idx, name))
    print()

    print("开始下载……")
    ok_count = 0
    for url, name in images:
        if save_image(url, name, save_dir, session):
            ok_count += 1
    print()

    print("下载完成:成功 %d 张,失败 %d 张。" % (ok_count, len(images) - ok_count))
    print("文件已保存到:%s" % save_dir)


if __name__ == "__main__":
    main()

运行结果

03_图片清单

03_下载结果

页面里符合三种格式的图片共识别到 20 个地址,去重后 19 个唯一文件,全部下载成功、0 失败。

03_文件夹

保存目录里能看到完整结果:其中 7 个是站点图标(logo.png、footer_logo.png、n_left_ico1.png 等),12 个是文章配图(文件名是 CMS 生成的哈希,如 EE9C07C6...F84E4.jpg),最大的两张分别有 8.7 MB 和 6.4 MB。

题目要求「爬取所有 JPEG、JPG 或 PNG 格式图片」,所以程序没有做内容筛选——页面里有什么就下什么,图标也一并保存了。如果只想留文章配图,在判断时额外要求路径包含 /__local/ 即可,代码里留了注释说明这一点。

2)、心得体会

?e=.png 这个坑印象最深。我第一版直接用 endswith 判断后缀,跑完发现只有 16 张,比预期少。查了半天才发现有三个地址带了查询串,xxx.png?e=.png 的结尾是 =png,不是 .png,所以被 endswith 判成了"不是图片"。这个坑很隐蔽——程序不报错,只是默默少下载几张,不看数量根本发现不了。去重是我没想到的。一开始我以为是页面重复渲染,后来数了一下 20 个地址里只有 19 个唯一文件,才明白是页头页脚共用了同一个 logo。如果不做去重,同一个文件名会被覆盖,其实结果一样,但下载次数多了一次,日志里也会显示 20 个。

posted @ 2026-10-19 18:34  Adca  阅读(8)  评论(0)    收藏  举报