数据采集第一次作业

数据采集第一次作业——requests 定向爬虫实践

本次作业使用 Python 语言,基于 requests 网络请求库,配合 BeautifulSoup 解析库与 re 正则表达式,完成三个定向数据采集任务。

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

1)实验代码与运行结果

实验目标:用 requests 和 BeautifulSoup 库方法定向爬取 http://www.shanghairanking.cn/rankings/bcur/2020 的数据,屏幕打印爬取的大学排名信息,输出格式为:排名、学校名称、省市、学校类型、总分。

实现思路:

  • 用 requests.get() 携带 User-Agent 请求头获取网页源码,并用 apparent_encoding 处理编码;
  • 用 BeautifulSoup 的 find_all("tr") 遍历表格行,用 get_text(strip=True) 提取单元格文本;
  • 过滤掉表头和无效行(排名列不是纯数字的行),只保留有效数据行;
  • 用固定列宽格式化输出表格。

完整代码:

# -*- coding: utf-8 -*-
"""
作业①:用 requests + BeautifulSoup 定向爬取软科 2020 中国大学排名
输出:排名、学校名称、省市、学校类型、总分
"""
import re
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.0.0 Safari/537.36"
    )
}


def fetch_html(url):
    """请求网页并返回 HTML 文本"""
    response = requests.get(url, headers=HEADERS, timeout=20)
    response.raise_for_status()
    response.encoding = response.apparent_encoding
    return response.text


def parse_ranking(html):
    """
    从网页表格中提取排名数据。
    返回列表,每个元素为 [排名, 学校名称, 省市, 学校类型, 总分]
    """
    soup = BeautifulSoup(html, "html.parser")
    rows = []

    for tr in soup.find_all("tr"):
        cells = [
            cell.get_text(" ", strip=True)
            for cell in tr.find_all(["td", "th"])
        ]
        if len(cells) < 5:
            continue
        rank = cells[0].strip()
        if not re.fullmatch(r"\d+", rank):
            continue
        rows.append(cells[:5])

    return rows


def print_table(rows):
    """按固定列宽格式化打印排名表格"""
    print(
        f"{'排名':<6}{'学校名称':<18}{'省市':<10}"
        f"{'学校类型':<10}{'总分':<8}"
    )
    print("-" * 60)
    for row in rows:
        print(
            f"{row[0]:<6}{row[1]:<18}{row[2]:<10}"
            f"{row[3]:<10}{row[4]:<8}"
        )


if __name__ == "__main__":
    html = fetch_html(URL)
    data = parse_ranking(html)
    if not data:
        print("未获取到排名数据,请检查页面结构是否变化。")
    else:
        print_table(data)

运行结果:

QQ20261009-171656

2)心得体会

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

请求网页时设置 User-Agent 是必要的,有些网站会拒绝没有浏览器标识的请求。编码方面,requests 拿到响应后默认按响应头的 charset 解码,国内部分网站不返回 charset 或返回的与实际不一致,因此用 apparent_encoding 更稳妥。

此外,软科页面不同年份的表格 class 名称可能变化,所以我没有依赖具体的 class,而是直接遍历所有 <tr> 并用数据特征筛选,这样即使页面小改也不会失效。

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

1)实验代码与运行结果

实验目标:用 requests 和 re 库方法设计商品比价定向爬虫,爬取当当网以关键词“书包”搜索页面的商品名称和价格。

实现思路:

  • 用 requests.get() 请求当当网搜索页,用 gbk 解码;
  • 先用正则定位商品列表容器 <ul class="bigimg">,再从中提取每个 <li> 商品块;
  • 在商品块内用正则提取商品名称(<p class="name"> 下 <a> 的 title 属性)和价格(<span class="price_n"> 中的数字);
  • 对提取结果去重后格式化输出。

完整代码:

# -*- coding: utf-8 -*-
"""
作业②:用 requests + re 爬取当当网“书包”搜索结果的商品名称和价格
"""
import os
import re
import time
import random
from urllib.parse import quote

import requests

SAVE_DIR = "html_pages"
os.makedirs(SAVE_DIR, exist_ok=True)

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",
}


def download_page(keyword="书包", page=1):
    """下载当当网搜索页并保存为本地 HTML 文件"""
    url = f"http://search.dangdang.com/?key={quote(keyword)}&page_index={page}"
    print(f"正在下载第 {page} 页:{url}")

    try:
        resp = requests.get(url, headers=HEADERS, timeout=15)
        resp.raise_for_status()
        html = resp.content.decode("gbk", errors="ignore")
    except requests.RequestException as e:
        print("请求失败:", e)
        return None

    filename = os.path.join(SAVE_DIR, f"{keyword}_page{page}.html")
    with open(filename, "w", encoding="utf-8") as f:
        f.write(html)
    print(f"  已保存:{filename}({len(html)} 字符)")

    time.sleep(random.uniform(1.5, 3.0))
    return html


def parse_html(html, max_items=60):
    """
    用正则从 HTML 中解析商品名称和价格。
    返回 [(名称, 价格), ...]
    """
    items = []

    ul_match = re.search(
        r'<ul[^>]*class="bigimg"[^>]*>(.*?)</ul>', html, re.S
    )
    scope = ul_match.group(1) if ul_match else html

    li_list = re.findall(r"<li[^>]*>.*?</li>", scope, re.S)
    print(f"  共找到 {len(li_list)} 个 <li> 块")

    for li in li_list:
        if len(items) >= max_items:
            break

        name_match = re.search(
            r'<p[^>]*class="name"[^>]*>.*?<a[^>]*title="([\s\S]+?)"',
            li, re.S
        )
        if not name_match:
            name_match = re.search(
                r'<a[^>]*title="([\s\S]+?)"', li, re.S
            )

        price_match = re.search(
            r'<span[^>]*class="price_n"[^>]*>\s*(?:&yen;|¥)?\s*([\d.]+)',
            li, re.S
        )
        if not price_match:
            price_match = re.search(
                r'<span[^>]*class="search_now_price"[^>]*>'
                r'\s*(?:&yen;|¥)?\s*([\d.]+)',
                li, re.S
            )

        if name_match and price_match:
            name = name_match.group(1).strip()
            price = price_match.group(1).strip()
            items.append((name, price))

    return items


if __name__ == "__main__":
    all_items = []
    for page in range(1, 4):
        html = download_page("书包", page)
        if html is None:
            continue
        print(f"\n===== 分析第 {page} 页 =====")
        items = parse_html(html)
        all_items.extend(items)
        print(f"  本页提取 {len(items)} 项")

    seen = set()
    unique_items = []
    for name, price in all_items:
        key = (name, price)
        if key not in seen:
            seen.add(key)
            unique_items.append((name, price))

    print(f"\n共提取 {len(all_items)} 项,去重后 {len(unique_items)} 项")
    print(f"{'序号':<6}{'价格':<12}{'商品名'}")
    print("-" * 70)
    for i, (name, price) in enumerate(unique_items[:60], 1):
        print(f"{i:<6}{price:<12}{name}")

运行结果:

QQ20261009-172340

QQ20261009-172424

2)心得体会

这道题让我深刻体会到了“先下载、再离线解析”的调试优势。一开始我直接用 requests 请求页面然后用正则匹配,反复失败却不知道原因。后来改为先把 HTML 保存到本地,再打开文件查看真实结构,才发现当当网大图模式下价格用的是 class="price_n",而不是我以为的 search_now_price。离线调试让我能反复查看 HTML 结构、逐步调整正则表达式。

正则表达式中,[\s\S]+? 比 [^"]+ 更适合匹配属性值,因为 title 属性中可能包含换行符,[^"] 不匹配换行会导致匹配失败。另外,当当网使用 GB2312 编码,用 resp.content.decode("gbk") 比 resp.text 更可靠。

请求频率方面,当当网也有一定的反爬机制,所以我在每次请求之间加了 1.5 到 3 秒的随机延时,避免被服务器限制。

三、作业③:网页图片批量下载

1)实验代码与运行结果

实验目标:爬取给定网页(https://news.fzu.edu.cn/yxfd.htm)中所有 JPEG、JPG、PNG 格式的图片文件,并保存到文件夹中。

实现思路:

  • 用 requests 请求页面,用 BeautifulSoup 解析所有 <img> 标签;
  • 兼容 src、data-src、data-original 等多种懒加载属性;
  • 用 urljoin 补全相对路径,只保留 .jpg、.jpeg、.png 扩展名;
  • 通过 URL 关键词过滤、文件大小过滤、图片尺寸过滤三重机制排除图标和占位图。

完整代码:

# -*- coding: utf-8 -*-
"""
作业③:爬取指定网页中所有 JPEG/JPG/PNG 图片并保存到文件夹
"""
import os
import time
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
from PIL import Image
from io import BytesIO


def download_images(page_url, save_dir="downloaded_images"):
    """
    爬取指定网页中的 JPEG、JPG、PNG 图片,并保存到本地文件夹。
    自动过滤 logo、图标、按钮、占位图等无关小图。
    """
    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",
    }

    allowed_ext = (".jpg", ".jpeg", ".png")

    exclude_keywords = [
        "logo", "icon", "button", "blank", "loading",
        "spacer", "arrow", "bg", "background", "placeholder",
        "ad", "banner", "share", "qrcode"
    ]

    MIN_WIDTH = 100
    MIN_HEIGHT = 100
    MIN_FILE_SIZE = 5 * 1024

    try:
        response = requests.get(page_url, headers=headers, timeout=15)
        response.raise_for_status()
        response.encoding = response.apparent_encoding

        soup = BeautifulSoup(response.text, "html.parser")

        content_area = (
            soup.find("div", id="vsb_content")
            or soup.find("div", class_="content")
            or soup.find("div", class_="article-content")
            or soup.find("div", class_="news_content")
        )

        if content_area:
            print("已定位正文区域,只提取正文中的图片。")
            img_tags = content_area.find_all("img")
        else:
            print("未找到正文区域,提取页面全部图片。")
            img_tags = soup.find_all("img")

        if not img_tags:
            print("页面中没有找到任何 <img> 标签。")
            return

        os.makedirs(save_dir, exist_ok=True)
        count = 0

        for img in img_tags:
            src = (
                img.get("src")
                or img.get("data-src")
                or img.get("data-original")
            )
            if not src:
                continue

            img_url = urljoin(page_url, src)
            path = urlparse(img_url).path.lower()

            if not path.endswith(allowed_ext):
                continue

            if any(kw in img_url.lower() for kw in exclude_keywords):
                print(f"跳过无关图片:{img_url}")
                continue

            try:
                img_resp = requests.get(
                    img_url, headers=headers, timeout=15
                )
                img_resp.raise_for_status()

                if len(img_resp.content) < MIN_FILE_SIZE:
                    print(f"跳过过小文件:{img_url}")
                    continue

                try:
                    im = Image.open(BytesIO(img_resp.content))
                    w, h = im.size
                    if w < MIN_WIDTH or h < MIN_HEIGHT:
                        print(f"跳过小尺寸图片:{img_url} ({w}x{h})")
                        continue
                except Exception:
                    print(f"无法识别图片格式,跳过:{img_url}")
                    continue

                filename = os.path.basename(path)
                if not filename:
                    filename = f"image_{count}.jpg"

                filepath = os.path.join(save_dir, filename)
                base, ext = os.path.splitext(filename)
                num = 1
                while os.path.exists(filepath):
                    filepath = os.path.join(
                        save_dir, f"{base}_{num}{ext}"
                    )
                    num += 1

                with open(filepath, "wb") as f:
                    f.write(img_resp.content)

                count += 1
                print(f"已保存:{filepath}  ({w}x{h})")
                time.sleep(0.5)

            except requests.RequestException as e:
                print(f"图片下载失败:{img_url},原因:{e}")

        print(f"\n共保存 {count} 张图片到文件夹:{save_dir}")

    except requests.RequestException as e:
        print(f"页面请求失败:{e}")


if __name__ == "__main__":
    target_url = "https://news.fzu.edu.cn/yxfd.htm"
    download_images(target_url, save_dir="downloaded_images")

运行结果:

QQ20261009-204813

merged

2)心得体会

这道题最大的教训是:不要盲目爬取页面中的所有 <img> 标签。第一次运行时,我下载了 50 多张图片,其中大部分是 logo、按钮图标、加载动画。后来我加入了三重过滤机制:① URL 关键词过滤,排除包含 logo、icon、loading 等词的链接;② 文件大小过滤,小于 5KB 的图片直接跳过;③ 图片尺寸过滤,用 Pillow 读取实际宽高,小于 100×100 的跳过。

此外,学校服务器对 HTTPS 请求有时会返回 SSL 错误,后来我在请求中加入了 verify=False 和重试机制来解决。图片链接还需要用 urljoin 把相对路径补全为绝对路径,否则 requests 无法正确下载。

posted @ 2026-10-12 20:04  Xx__X  阅读(13)  评论(0)    收藏  举报