数据采集第一次作业

Python 定向爬虫实践作业

实验日期:2026年10月13日

本次实践包括大学排名爬取、商城商品比价和网页图片下载三个任务。每项作业按照“实验”和“心得体会”展开,给出实现思路、完整代码和实验结果。

作业①

1)大学排名爬取实验

实验要求

使用 requests 和 BeautifulSoup 定向爬取 2020年软科中国大学排名,在屏幕上打印排名、学校名称、省市、学校类型和总分。

实现思路

首先通过 requests.get() 获取页面,并按 UTF-8 解码,防止中文乱码。然后使用 BeautifulSoup 解析 HTML,定位表格中的 tbody tr,依次读取每行的 td 单元格。

学校名称单元格还包含英文名称和学校标签,因此通过 .name-cn 只提取中文校名。其余字段按照表格列的位置读取,最后使用制表符打印结果。本代码读取服务器返回的当前 HTML 页面中的学校记录,不操作网页上的动态翻页按钮。

完整代码

import requests
from bs4 import BeautifulSoup

URL = "https://www.shanghairanking.cn/rankings/bcur/2020"
HEADERS = {"User-Agent": "Mozilla/5.0"}


def parse_ranking(html):
    soup = BeautifulSoup(html, "html.parser")
    records = []
    for row in soup.select("tbody tr"):
        cells = row.find_all("td", recursive=False)
        if len(cells) < 5:
            continue
        name = cells[1].select_one(".name-cn")
        if name is None:
            continue
        records.append([
            cells[0].get_text(strip=True), name.get_text(strip=True),
            cells[2].get_text(strip=True), cells[3].get_text(strip=True),
            cells[4].get_text(strip=True)
        ])
    if not records:
        raise ValueError("未找到排名表格,请检查网页结构或返回内容")
    return records


def main():
    response = requests.get(URL, headers=HEADERS, timeout=(10, 30))
    response.raise_for_status()
    records = parse_ranking(response.content.decode("utf-8"))
    print("排名\t学校名称\t省市\t学校类型\t总分")
    for record in records:
        print("\t".join(record))
    print(f"\n共读取当前 HTML 页面中的 {len(records)} 所学校。")


if __name__ == "__main__":
    try:
        main()
    except (requests.RequestException, ValueError) as error:
        print("爬取失败:", error)

运行命令

python task1_ranking.py

实验结果

屏幕截图 2026-10-13 181804

2)心得体会

通过本次实验,我熟悉了从网页请求到表格解析再到屏幕输出的基本流程。获取到网页内容后,需要先分析页面结构,再选择合适的标签和 CSS 选择器。直接读取学校名称所在单元格的全部文本会混入英文校名等信息,定位 .name-cn 可以使输出更加准确。

我也认识到,编码处理与异常检查会影响爬虫的可用性。中文页面应采用正确的编码进行解码;当请求失败或表格不存在时,应提示问题,而不是只输出一个空表头。此外,页面可见的分页并不意味着 requests 会自动取得全部分页数据,需要根据实际需求单独处理。

作业②

1)商城商品比价爬取实验

实验要求

自选一个商城,使用 requests 和 re 编写商品比价定向爬虫,以“书包”为关键词获取搜索页面数据,打印序号、价格和商品名称。

实现思路

本实验选择当当网,搜索页面为 当当网商品搜索。将“书包”按照 GBK 编码后作为查询参数发送,并使用 GB18030 解码返回的页面。

使用正则表达式先匹配每个商品对应的 li 块,再在同一个商品块中提取商品名称和价格。价格标签兼容 price_n 与 search_now_price 两种类名。这样可以避免分别提取名称列表和价格列表后发生错位。

提取价格中的数字并转换为 Decimal,按价格升序排列后输出。该排序比较的是不同商品的页面展示价格,商品规格和款式可能不同,不能据此认定为同一型号的最低成交价。

完整代码

import re
import requests
from html import unescape
from decimal import Decimal

URL = "https://search.dangdang.com/"
HEADERS = {"User-Agent": "Mozilla/5.0"}


def parse_products(html):
    # 在每个商品 li 内配对名称和价格,防止两份列表错位。
    blocks = re.findall(
        r'<li\b[^>]*\bid=["\'](\d+)["\'][^>]*>(.*?)</li>',
        html, re.S | re.I
    )
    products = {}
    for product_id, block in blocks:
        name = re.search(r'<a\b[^>]*\btitle=["\'](.*?)["\']',
                         block, re.S | re.I)
        price = re.search(
            r'<span\b[^>]*class=["\'][^"\']*\b'
            r'(?:price_n|search_now_price)\b[^"\']*["\'][^>]*>'
            r'(.*?)</span>', block, re.S | re.I
        )
        if not (name and price):
            continue
        price_text = unescape(re.sub(r'<[^>]+>', '', price.group(1)))
        number = re.search(r'\d[\d,]*(?:\.\d+)?', price_text)
        if number:
            title = re.sub(r'\s+', ' ', unescape(name.group(1))).strip()
            value = Decimal(number.group().replace(',', ''))
            products[product_id] = (value, title)
    if not products:
        raise ValueError("未找到商品名称和价格,请检查网页结构或返回内容")
    return sorted(products.values(), key=lambda item: item[0])


def main():
    # 当当的搜索关键词使用 GBK 编码;requests 对 bytes 参数进行 URL 编码。
    response = requests.get(
        URL, params={"key": "书包".encode("gbk"), "act": "input",
                     "page_index": 1},
        headers=HEADERS, timeout=(10, 30)
    )
    response.raise_for_status()
    products = parse_products(response.content.decode("gb18030"))
    print("序号\t价格\t商品名")
    for index, (price, title) in enumerate(products, 1):
        print(f"{index}\t{price:.2f}\t{title}")
    print(f"\n共读取 {len(products)} 件商品,按页面价格升序排列。")


if __name__ == "__main__":
    try:
        main()
    except (requests.RequestException, ValueError) as error:
        print("爬取失败:", error)

运行命令

python task2_shopping.py

实验结果

屏幕截图 2026-10-13 181836

2)心得体会

通过本次实验,我学习了利用正则表达式提取指定网页字段的方法。提取商品信息时,名称与价格必须属于同一个商品块,不能仅依赖两份独立列表的位置对应。商品块中的内容可能跨越多行,因此匹配时需要使用 re.S;对于名称中的 HTML 实体,则需要使用 unescape() 还原。

商品价格应转换为数值后再排序,直接按照字符串排序可能产生错误。使用 Decimal 也便于保留金额的精度。与标签解析相比,正则表达式更依赖页面结构,网页类名或标签变化后应重新检查匹配规则。本次实现同时兼容两种价格类名,减少了旧规则无法匹配当前页面的问题。

作业③

1)网页图片下载实验

实验要求

爬取 福州大学新闻网影像福大页面 中 JPEG、JPG 或 PNG 格式的图片,将图片文件保存在一个文件夹中。本报告选取8张下载图片作为实验结果展示。

实现思路

首先获取页面 HTML,通过 BeautifulSoup 提取 src、data-src、data-original、href 和 srcset 等属性中的图片候选地址,同时检查页面内的 CSS 背景图片地址。

使用 urljoin() 将相对地址转换为完整地址,通过 urlsplit() 获取 URL 路径,判断后缀是否为 JPG、JPEG 或 PNG。使用集合去除重复地址后,通过 requests 请求图片的二进制数据。

保存前检查 PNG 或 JPEG 的文件签名,避免将错误页面保存为图片。文件名包含序号和图片 URL 的哈希片段,减少名称冲突。图片保存到脚本所在目录的 fzu_images 文件夹中。

代码下载当前页面 HTML 直接引用的全部符合条件的图片,不自动进入新闻详情页;以下结果仅选取其中8张展示。

完整代码

import re
import time
import hashlib
from pathlib import Path
from urllib.parse import urljoin, urlsplit, urldefrag
import requests
from bs4 import BeautifulSoup

URL = "https://news.fzu.edu.cn/yxfd.htm"
FOLDER = Path(__file__).resolve().parent / "fzu_images"
HEADERS = {"User-Agent": "Mozilla/5.0"}


def find_images(html, page_url):
    soup = BeautifulSoup(html, "html.parser")
    base = soup.find("base", href=True)
    base_url = urljoin(page_url, base["href"]) if base else page_url
    candidates = []
    for tag in soup.find_all(True):
        for attribute in ("src", "data-src", "data-original", "href"):
            value = tag.get(attribute)
            if value:
                candidates.append(value)
        for attribute in ("srcset", "data-srcset"):
            for item in tag.get(attribute, "").split(","):
                if item.strip():
                    candidates.append(item.strip().split()[0])
        candidates.extend(re.findall(
            r'url\(\s*["\']?([^\)"\']+)', tag.get("style", ""), re.I))
    for style in soup.find_all("style"):
        candidates.extend(re.findall(
            r'url\(\s*["\']?([^\)"\']+)', style.get_text(), re.I))
    urls = set()
    for candidate in candidates:
        url = urldefrag(urljoin(base_url, candidate.strip()))[0]
        parts = urlsplit(url)
        if parts.scheme in ("http", "https") and re.search(
                r'\.(?:jpe?g|png)$', parts.path, re.I):
            urls.add(url)
    return sorted(urls)


def main():
    with requests.Session() as session:
        session.headers.update(HEADERS)
        response = session.get(URL, timeout=(10, 30))
        response.raise_for_status()
        urls = find_images(response.content.decode("utf-8"), response.url)
        if not urls:
            raise ValueError("当前 HTML 中没有找到 JPG、JPEG 或 PNG 图片")
        FOLDER.mkdir(parents=True, exist_ok=True)
        success = 0
        for index, url in enumerate(urls, 1):
            try:
                image = session.get(url, headers={"Referer": response.url},
                                    timeout=(10, 30))
                image.raise_for_status()
                data = image.content
                # 检查文件签名,避免把错误页面当成图片保存。
                if data.startswith(b"\x89PNG\r\n\x1a\n"):
                    extension = ".png"
                elif data.startswith(b"\xff\xd8\xff"):
                    extension = ".jpg"
                else:
                    raise ValueError("返回内容不是 PNG 或 JPEG 文件")
                digest = hashlib.sha256(url.encode()).hexdigest()[:12]
                target = FOLDER / f"{index:03d}_{digest}{extension}"
                target.write_bytes(data)
                success += 1
                print("已保存:", target.name)
            except (requests.RequestException, ValueError, OSError) as error:
                print("下载失败:", url, error)
            time.sleep(0.3)
        print(f"\n找到 {len(urls)} 个图片地址,成功保存 {success} 张。")
        print("保存目录:", FOLDER)


if __name__ == "__main__":
    try:
        main()
    except (requests.RequestException, ValueError, OSError) as error:
        print("爬取失败:", error)

运行命令

python task3_images.py

实验结果

程序已成功下载页面图片。以下8张图片均选自实际保存的文件,作为本次实验的展示结果;不重复展示页面中的图标和装饰图片。

图片1

001_e6efd6d544c9

图片2

003_c5c622feb562

图片3

004_2f8e5dc0d01e

图片4

005_b2720ae650da

图片5

006_70e019dc009e

图片6

007_90bf4a1f6eb1

图片7

010_610d589dd6fe

2)心得体会

通过本次实验,我掌握了网页图片地址提取与二进制文件下载的方法。网页中的图片地址可能是相对路径,也可能包含查询参数,所以不能仅检查整个 URL 的结尾;先补全地址,再检查 URL 路径中的扩展名,判断会更准确。页面中同一张图片可能被多次引用,使用集合去重可以减少重复请求。

保存图片时还需要区分真正的图片数据和服务器返回的错误内容。检查文件签名可以避免生成无法打开的“图片文件”。通过给文件名增加序号和 URL 哈希,我也减少了不同地址使用相同文件名时的覆盖问题。本报告从下载结果中选取8张图片,展示了图片采集与本地保存的效果。

posted @ 2026-10-13 18:45  由一  阅读(4)  评论(0)    收藏  举报