数据采集第一次作业

第一次线上作业

本次作业使用 Python 完成三个定向采集任务。运行环境为 Python 3.10 及以上版本,依赖库如下。

pip install requests beautifulsoup4

运行前,请在当前目录创建 images 文件夹,并将三次实际运行的终端截图分别保存为 task1_ranking.png、task2_backpack.png、task3_images.png。将本文粘贴到博客园后,也可以直接把图片拖到对应位置上传。

作业① 上海软科大学排名数据采集

1)实验代码和运行图片

本实验使用 requests 获取 2020 年中国最好大学排名页面,用 BeautifulSoup 遍历表格行,提取排名、学校名称、省市、学校类型和总分。LIMIT 可以根据需要修改,以下代码默认输出前 30 条记录。

import re

import requests
from bs4 import BeautifulSoup


URL = "https://www.shanghairanking.cn/rankings/bcur/2020"
LIMIT = 30
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36"
    )
}


def clean_text(value):
    """合并页面中的空白字符。"""
    return " ".join(value.split())


def get_ranking_rows(html_text):
    """从排名表格中提取有效数据行。"""
    soup = BeautifulSoup(html_text, "html.parser")
    ranking_rows = []

    for row in soup.find_all("tr"):
        cells = [clean_text(cell.get_text(" ", strip=True))
                 for cell in row.find_all(["th", "td"])]
        if len(cells) < 5:
            continue

        rank = cells[0]
        if not re.fullmatch(r"\d+", rank):
            continue

        ranking_rows.append(cells[:5])
        if len(ranking_rows) >= LIMIT:
            break

    return ranking_rows


def main():
    response = requests.get(URL, headers=HEADERS, timeout=20)
    response.raise_for_status()
    response.encoding = response.apparent_encoding

    rows = get_ranking_rows(response.text)
    if not rows:
        raise RuntimeError("未从页面中解析到大学排名数据。")

    print("2020 中国最好大学排名")
    print("排名\t学校名称\t省市\t学校类型\t总分")
    for row in rows:
        print("\t".join(row))


if __name__ == "__main__":
    main()

程序输出的前几条记录如下。

2020 中国最好大学排名
排名    学校名称        省市    学校类型    总分
1       清华大学        北京    综合        852.5
2       北京大学        北京    综合        746.7
3       浙江大学        浙江    综合        649.2
4       上海交通大学    上海    综合        625.9
5       南京大学        江苏    综合        566.1

图片

2)心得体会

这个页面的重点在于识别真正承载数据的表格行。页面中除了排名数据,还有导航、广告和说明文字,直接把所有文本输出会得到很多无关内容。我先取出每一行的单元格,再用排名列是否为纯数字作为筛选条件,因此可以避开表头和无效行。

请求网页时设置 User-Agent 很有必要。有些网站会拒绝没有浏览器标识的请求。采集结果中的总分会随榜单版本变化,因此运行时应以程序当次打印出的数据为准。

作业② 当当网书包商品比价定向爬虫

1)实验代码和运行图片

本实验选择当当网作为商城,搜索关键词为“书包”。程序使用 requests 请求搜索结果第 1 页,使用正则表达式从商品列表中提取商品名称和价格,并限制输出前 60 项。网页中的商品名称包含 HTML 标签时,程序会先去除标签并还原 HTML 实体。

import html
import re

import requests


SEARCH_URL = "https://search.dangdang.com/?key=%E4%B9%A6%E5%8C%85"
LIMIT = 60
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "zh-CN,zh;q=0.9",
}


def clean_name(value):
    """清除商品标题中的标签和多余空白。"""
    value = re.sub(r"<[^>]+>", "", value)
    return " ".join(html.unescape(value).split())


def get_products(page_html):
    """从搜索结果 HTML 中提取商品名称和价格。"""
    blocks = re.findall(
        r'<li\b[^>]*\bddt-pit="(\d+)"[^>]*>(.*?)</li>',
        page_html,
        flags=re.S | re.I,
    )

    products = []
    for _, block in blocks:
        name_match = re.search(
            r'<p\s+class="name"[^>]*>\s*<a\b[^>]*\btitle="([^"]+)"',
            block,
            flags=re.S | re.I,
        )
        price_match = re.search(
            r'<p\s+class="price"[^>]*>\s*'
            r'<span\s+class="price_n">&yen;([\d.]+)</span>',
            block,
            flags=re.S | re.I,
        )

        if not name_match or not price_match:
            continue

        products.append({
            "name": clean_name(name_match.group(1)),
            "price": float(price_match.group(1)),
        })
        if len(products) >= LIMIT:
            break

    return products


def main():
    response = requests.get(SEARCH_URL, headers=HEADERS, timeout=20)
    response.raise_for_status()

    # 当当搜索页使用 GBK 系列编码,gb18030 能兼容更多中文字符。
    page_html = response.content.decode("gb18030", errors="replace")
    products = get_products(page_html)

    if len(products) < LIMIT:
        raise RuntimeError(f"只解析到 {len(products)} 条商品,未达到 {LIMIT} 条。")

    print("当当网书包搜索结果第 1 页前 60 项")
    print("序号\t价格\t商品名称")
    for index, product in enumerate(products, start=1):
        print(f"{index}\t{product['price']:.2f}\t{product['name']}")

    prices = [product["price"] for product in products]
    print("-" * 70)
    print(f"最低价 {min(prices):.2f} 元")
    print(f"最高价 {max(prices):.2f} 元")
    print(f"平均价 {sum(prices) / len(prices):.2f} 元")


if __name__ == "__main__":
    main()

程序输出格式如下。商品和价格会受库存、优惠和页面排序影响,因此实际运行结果可能与示例不同。

当当网书包搜索结果第 1 页前 60 项
序号    价格    商品名称
1       146.00  迪士尼书包小学生女童双肩包
2       39.99   书包幼儿园女孩卡通双肩包
3       89.99   书包女新款初中生高中学生大容量双肩包
...
60      117.00  凯蒂猫书包小学生女童双肩包

图片

2)心得体会

商城搜索页与普通文章页不同,商品信息按固定结构重复出现。先观察一个商品块中的名称和价格位置,再把正则表达式限制在单个商品块内,能够减少标题、推荐词和广告价格带来的干扰。

搜索入口中的 key 参数保存关键词,翻页时商城会通过 page_index 等参数标识页码。本次作业限定第 1 页前 60 项,因此没有继续请求下一页。若要采集更多数据,可以在循环中修改页码参数,并在每次请求之间加入适当等待时间。

作业③ 下载指定网页中的 JPEG JPG PNG 图片

1)实验代码和运行图片

本实验选取福州大学新闻网首页 https://news.fzu.edu.cn/。题目所给的校园风采旧链接目前会返回 404,因此改用同一网站仍可正常访问的页面。程序会读取 img 标签中的 src、data-src 和 data-original 属性,处理相对路径,确认响应类型后仅保存 JPEG、JPG 和 PNG 文件到 download_images 文件夹。

import hashlib
import mimetypes
from pathlib import Path
from urllib.parse import urljoin, urlparse

import requests
from bs4 import BeautifulSoup


PAGE_URL = "https://news.fzu.edu.cn/"
OUTPUT_DIR = Path("download_images")
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36"
    )
}
IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png"}
CONTENT_TYPE_SUFFIX = {
    "image/jpeg": ".jpg",
    "image/png": ".png",
}


def find_image_urls(page_html):
    """从页面中收集图片地址并去重。"""
    soup = BeautifulSoup(page_html, "html.parser")
    image_urls = []
    seen = set()

    for image in soup.find_all("img"):
        source = None
        for attribute in ("src", "data-src", "data-original"):
            if image.get(attribute):
                source = image[attribute]
                break

        if not source:
            continue

        full_url = urljoin(PAGE_URL, source)
        if full_url not in seen:
            seen.add(full_url)
            image_urls.append(full_url)

    return image_urls


def get_extension(image_url, content_type):
    """按 URL 或响应头判断目标图片的扩展名。"""
    suffix = Path(urlparse(image_url).path).suffix.lower()
    if suffix in IMAGE_SUFFIXES:
        return suffix

    content_type = content_type.split(";", maxsplit=1)[0].lower()
    if content_type in CONTENT_TYPE_SUFFIX:
        return CONTENT_TYPE_SUFFIX[content_type]

    guessed_suffix = mimetypes.guess_extension(content_type)
    return guessed_suffix if guessed_suffix in IMAGE_SUFFIXES else None


def download_image(session, image_url, index):
    """下载单张 JPEG JPG PNG 图片,返回成功状态。"""
    try:
        response = session.get(image_url, timeout=20)
        response.raise_for_status()
    except requests.RequestException as error:
        print(f"下载失败 {image_url},原因 {error}")
        return False

    content_type = response.headers.get("Content-Type", "")
    suffix = get_extension(image_url, content_type)
    if suffix is None:
        return False

    url_hash = hashlib.md5(image_url.encode("utf-8")).hexdigest()[:8]
    file_path = OUTPUT_DIR / f"image_{index:03d}_{url_hash}{suffix}"
    file_path.write_bytes(response.content)
    print(f"已保存 {file_path.name}")
    return True


def main():
    OUTPUT_DIR.mkdir(exist_ok=True)

    session = requests.Session()
    session.headers.update(HEADERS)
    response = session.get(PAGE_URL, timeout=20)
    response.raise_for_status()
    response.encoding = response.apparent_encoding

    image_urls = find_image_urls(response.text)
    success_count = 0
    for index, image_url in enumerate(image_urls, start=1):
        if download_image(session, image_url, index):
            success_count += 1

    print("-" * 50)
    print(f"页面发现图片链接 {len(image_urls)} 个")
    print(f"成功保存 JPEG JPG PNG 图片 {success_count} 个")
    print(f"图片保存目录 {OUTPUT_DIR.resolve()}")


if __name__ == "__main__":
    main()

程序输出格式如下。

已保存 image_001_XXXXXXXX.jpg
已保存 image_002_XXXXXXXX.png
...
--------------------------------------------------
页面发现图片链接 N 个
成功保存 JPEG JPG PNG 图片 N 个
图片保存目录 D:\\...\\download_images

图片

2)心得体会

下载图片时,页面中的 img 标签不一定只使用 src 属性。许多网站会把真实地址放在 data-src 或 data-original 中,等图片进入浏览区域后再加载。程序同时检查这三个属性,可以提高对不同网页结构的适应性。

相对路径不能直接请求,需要使用 urljoin 拼接为完整地址。保存文件前还要检查 URL 后缀和响应头中的 Content-Type,这样可以避免把 SVG、GIF 或 HTML 错误页面误保存为图片。下载完成后以文件夹中的实际文件数量作为结果核验依据。

posted @ 2026-10-10 11:12  102580126黄喆珺  阅读(11)  评论(0)    收藏  举报