数据采集第一次作业

数据采集第一次作业

网络爬虫第一次作业:requests + BeautifulSoup / re 定向爬虫
代码仓库:作业1

实验环境:

项目 说明
操作系统 Windows 11(64 位)
编程语言 Python 3.10
开发工具 VS Code
浏览器 Chrome(含开发者工具 F12)
第三方库 requests、beautifulsoup4、lxml、tabulate、urllib3
库安装命令 pip install requests beautifulsoup4 lxml tabulate urllib3

本次作业共三个小作业,分别练习 BeautifulSoup 解析网页表格、正则表达式提取商城商品信息、以及批量下载网页图片。


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

1)大学排名爬虫实验

实验目标:用 requests 库请求网页,用 BeautifulSoup 库解析 HTML,定向爬取软科 2020 中国大学排名(https://www.shanghairanking.cn/rankings/bcur/2020 ),输出排名、学校名称、省市、学校类型、总分。

数据来源说明:

软科排名页面的数据并非静态写死在 HTML 中,而是通过其公开接口以 JSON 形式返回。接口地址为:

GET https://www.shanghairanking.cn/api/pub/v1/bcur?bcur_type=11&year=2020

其中:

  • bcur_type=11 表示"中国大学排名(主榜)";
  • year=2020 表示目标年份。

接口返回的 JSON 包含 ranking(排名)、univNameCn(中文校名)、province(省市)、univCategory(学校类型)、score(总分)等字段。因此本实验采用"抓接口拿 JSON + BeautifulSoup 清洗文本 + json 解析"的思路来获取数据。

完整代码:

"""
作业①:用 requests + BeautifulSoup 爬取 2020 软科中国大学排名(主榜)
输出:排名、学校名称、省市、学校类型、总分
"""

import requests
import json
from bs4 import BeautifulSoup
from tabulate import tabulate

API_URL = "https://www.shanghairanking.cn/api/pub/v1/bcur"
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": "https://www.shanghairanking.cn/rankings/bcur/2020",
}


def fetch_html(url):
    """请求软科 2020 主榜接口,返回文本"""
    params = {"bcur_type": 11, "year": 2020}
    resp = requests.get(url, headers=HEADERS, params=params, timeout=15)
    resp.encoding = "utf-8"
    resp.raise_for_status()
    return resp.text


def parse_ranking(json_text):
    # 用 BeautifulSoup 解析文本,再拿回干净的字符串
    soup = BeautifulSoup(json_text, "lxml")
    clean_text = soup.get_text()

    # 再用 json 模块解析
    data = json.loads(clean_text)
    rankings = data.get("data", {}).get("rankings", [])

    rows = []
    for r in rankings:
        rows.append([
            r.get("ranking", ""),
            r.get("univNameCn", ""),
            r.get("province", ""),
            r.get("univCategory", ""),
            r.get("score", ""),
        ])
    return rows


if __name__ == "__main__":
    text = fetch_html(API_URL)
    data = parse_ranking(text)

    print(f"共解析到 {len(data)} 条数据\n")
    headers = ["排名", "学校名称", "省市", "学校类型", "总分"]
    print(tabulate(data[:30], headers=headers, tablefmt="simple"))

运行结果:

aa02eaf63e21608c7218cf0a92a40ddc


2)心得体会

这次实验最大的收获是学会了从"页面"到"接口"的思路转变。一开始我打算直接解析 HTML 表格,在开发者工具里翻来覆去只找到骨架结构;后来意识到真实数据是通过后端接口以 JSON 形式返回的,思路瞬间打开。这让我意识到:现代网站的前端渲染越来越普遍,光看页面结构不够,还要关注数据从哪来。

另一个收获是 BeautifulSoup 的多场景用法。之前我只把它当作 HTML 解析器,这次我把它当成"文本清洗器"——先用 get_text() 剥掉所有标签和转义字符,再交给 json.loads 解析,避免了很多边界情况下的解析失败。这说明同一个工具在不同场景下可以有完全不同的定位。

另外,请求头中的 Referer 也让我踩了坑——最开始没加 Referer,服务器直接返回了 403。加上之后就正常了。这让我明白:写爬虫时请求头不是可选项,而是必须项。

通过本次实验,我掌握了接口型爬虫的完整流程:定位接口 → 构造请求 → 清洗数据 → 解析结构 → 格式化输出。


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

1)当当商品比价爬虫实验

实验目标:用 requests 库请求网页,用 re 库(正则表达式)从 HTML 中提取数据,自选一个商城,以关键词"书包"搜索,爬取商品名称和价格。

商城选择:我选的是当当网(search.dangdang.com)。原因:京东的价格由 JS 动态加载,requests 抓到的 HTML 中拿不到;淘宝需要登录,反爬严格;而当当的搜索结果页是静态 HTML,商品名、价格都直接写在源码里,用正则提取最合适。

页面结构分析:

打开当当网搜索页(https://search.dangdang.com/?key=书包&act=input),按 F12 打开开发者工具,切到 Elements 面板,用元素选择器分别点选商品价格与商品标题,可以确认:

  • 价格保存在 <p class="price"><span class="price_n">¥146.00</span></p> 结构中,price_n 类名下的 <span> 直接包含价格文字;
  • 商品名保存在 <p class="name" name="title"><a title="商品名称"> 结构中,<a> 标签的 title 属性保存了商品完整标题。

基于这两个结构,即可写出对应的正则进行提取:一条抓 price_n 标签内的数字作为价格,一条抓 name="title" 下 <a> 标签的 title 属性作为商品名。

image

image

完整代码:

"""
作业②:用 requests + re 爬取当当网"书包"商品名称与价格
"""

import re
import csv
import time
import urllib3

http = urllib3.PoolManager()

HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/124.0.0.0 Safari/537.36"
    ),
    "Referer": "https://www.dangdang.com/",
}

keyword = "书包"
url = f"https://search.dangdang.com/?key={keyword}&act=input"
MAX_ITEMS = 60


def fetch_html(url):
    """请求当当搜索页,返回 HTML 文本(GBK 编码)"""
    resp = http.request("GET", url, headers=HEADERS, timeout=15)
    return resp.data.decode("gbk", errors="ignore")


def parse_products(html):
    """
    用正则从每个 <li> 商品块中提取商品名和价格
    """
    items = []
    # 按 li 块切分
    li_blocks = re.findall(r'<li[^>]*>(.*?)</li>', html, re.S)

    for block in li_blocks:
        # 商品名:<p class="name" name="title"><a title="商品名">
        name_match = re.search(
            r'<p class="name" name="title"[^>]*>\s*<a[^>]*title="([^"]+)"',
            block
        )
        # 价格(主方案):<span class="price_n">&yen;146.00</span>
        price_match = re.search(
            r'<span class="price_n"[^>]*>\s*&yen;\s*([\d]+(?:\.\d{1,2})?)\s*</span>',
            block
        )
        # 价格(兜底方案):页面任意位置 &yen;/¥ + 数字
        if not price_match:
            price_match = re.search(
                r'(?:&yen;|¥)\s*([\d]+(?:\.\d{1,2})?)', block
            )

        if name_match and price_match:
            name = name_match.group(1).strip()
            price = price_match.group(1).strip()
            # 过滤排序/按钮文字
            if any(bad in name for bad in
                   ["点击", "按销量", "按好评", "按价格", "按最新", "综合排序"]):
                continue
            items.append({"name": name, "price": f"¥{price}"})
            if len(items) >= MAX_ITEMS:
                break
    return items


def main():
    print(f"正在爬取:{url}")
    try:
        html = fetch_html(url)
    except Exception as e:
        print("请求失败:", e)
        return

    items = parse_products(html)
    if not items:
        print("未提取到数据,可能页面结构变化或被反爬拦截")
        return

    print(f"共提取 {len(items)} 条(上限 {MAX_ITEMS})")
    for idx, it in enumerate(items, 1):
        print(f"{idx:02d}. {it['price']:>8}  {it['name']}")

    # 保存 CSV
    with open("dangdang_shubao.csv", "w",
              encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["name", "price"])
        writer.writeheader()
        writer.writerows(items)
    print("已保存:dangdang_shubao.csv")


if __name__ == "__main__":
    main()
    time.sleep(1)

运行结果:

f8268f5b86770c65673111e41e73aa9d

de58547fcf945abecc021aeef67ef9ac


2)心得体会

这次作业让我真正体会到"选对目标网站"有多重要。一开始我想爬京东,在 F12 里怎么点都找不到价格——后来才知道它的价格是页面加载完成后由 JS 二次请求回来的,requests 拿到的 HTML 里根本没有。转到当当就顺畅很多,因为它的搜索结果页是静态 HTML,商品名和价格直接写在源码里。

另一个教训是编码。当当是 GBK,我用 resp.data.decode("gbk", errors="ignore") 手动解码后中文才正常;如果偷懒不处理,打印出来的商品名全是乱码。这也让我意识到:不同站点的编码策略不一样,不能死记一种,每次都要先观察。

写正则时,我学会了"先分块、再提字段"的策略。最初我尝试写一条大正则一次匹配所有商品,结果经常跨商品误配,价格和商品名对不上。改成先按 <li> 切块、再在块内分别匹配后,问题立刻解决了。此外,给价格写主备两套正则也很重要——主方案精准但脆弱,兜底方案粗糙但稳定,两者结合可以应对大部分页面微调。

最后,用 CSV 保存结果时,encoding="utf-8-sig" 这个细节很关键——不加 BOM 头,Excel 打开就是乱码。这些看似细小的编码问题,恰恰是爬虫实践中最容易踩的坑。


作业③ 爬取福州大学新闻网图片

1)福大新闻网图片爬虫实验

实验目标:爬取福州大学新闻网"影像福大"页面(https://news.fzu.edu.cn/yxfd.htm )中的所有 JPEG、JPG、PNG 格式图片,保存到本地文件夹。

页面结构分析:页面上的图片都是 <img> 标签,图片地址写在 src 属性里。图片来源分两类:

  • images/ 开头的站点小图标(logo、返回按钮、导航箭头等);
  • /__local/ 开头的新闻内容大图。

爬虫要做的事就是:用正则从 HTML 中取出所有 src="...jpg/jpeg/png" 的地址 → 拼成绝对路径 → 去重 → 逐张下载到本地。

image

完整代码:

"""
作业③:爬取 https://news.fzu.edu.cn/yxfd.htm 页面中所有 JPEG/JPG/PNG 图片,
       保存到 images 文件夹中
"""

import os
import re
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse

BASE_URL = "https://news.fzu.edu.cn/yxfd.htm"
SAVE_DIR = "images"          # 保存图片的文件夹
TIMEOUT = 15                 # 请求超时时间
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"
    )
}
# 支持的图片格式(不区分大小写)
IMG_EXTS = (".jpg", ".jpeg", ".png")


def is_target_image(url: str) -> bool:
    """判断 URL 是否指向 JPEG/JPG/PNG 图片"""
    if not url:
        return False
    # 去掉查询参数后再判断扩展名
    path = urlparse(url).path.lower()
    return path.endswith(IMG_EXTS)


def safe_filename(url: str, index: int) -> str:
    """根据 URL 生成安全、唯一的文件名"""
    path = urlparse(url).path
    name = os.path.basename(path)
    # 去掉非法字符
    name = re.sub(r'[\\/:*?"<>|]', "_", name)
    if not name:
        name = f"image_{index}.jpg"
    # 加上序号前缀,避免重名覆盖
    return f"{index:03d}_{name}"


def download_images(page_url: str, save_dir: str) -> None:
    # 1. 创建保存目录
    os.makedirs(save_dir, exist_ok=True)

    # 2. 请求网页
    print(f"[*] 正在请求页面: {page_url}")
    resp = requests.get(page_url, headers=HEADERS, timeout=TIMEOUT)
    resp.encoding = resp.apparent_encoding  # 自动识别编码,避免中文乱码
    resp.raise_for_status()

    # 3. 解析 HTML,提取 img 标签
    soup = BeautifulSoup(resp.text, "html.parser")
    img_tags = soup.find_all("img")
    print(f"[*] 页面中共发现 {len(img_tags)} 个 <img> 标签")

    # 4. 收集并去重图片链接
    img_urls = []
    seen = set()
    for tag in img_tags:
        # 优先取 src,其次 data-src / data-original(懒加载)
        src = tag.get("src") or tag.get("data-src") or tag.get("data-original")
        if not src:
            continue
        full_url = urljoin(page_url, src.strip())
        if is_target_image(full_url) and full_url not in seen:
            seen.add(full_url)
            img_urls.append(full_url)

    print(f"[*] 其中 JPEG/JPG/PNG 图片共 {len(img_urls)} 张,开始下载...\n")

    # 5. 逐张下载
    success, failed = 0, 0
    for i, url in enumerate(img_urls, start=1):
        filename = safe_filename(url, i)
        filepath = os.path.join(save_dir, filename)
        try:
            r = requests.get(url, headers=HEADERS, timeout=TIMEOUT)
            r.raise_for_status()
            with open(filepath, "wb") as f:
                f.write(r.content)
            success += 1
            print(f"[OK] ({i}/{len(img_urls)}) {filename}")
        except Exception as e:
            failed += 1
            print(f"[FAIL] ({i}/{len(img_urls)}) {url} -> {e}")

    # 6. 汇总
    print("\n========== 下载完成 ==========")
    print(f"成功: {success} 张")
    print(f"失败: {failed} 张")
    print(f"保存目录: {os.path.abspath(save_dir)}")


if __name__ == "__main__":
    download_images(BASE_URL, SAVE_DIR)

运行结果:
image
image


2)心得体会

这次作业让我彻底搞懂了相对路径与绝对路径的区别。网页里很多图片的 src 是 /__local/F/4D/B3/xxx.jpg 这种以斜杠开头的根相对路径,直接拿去请求会 404,必须用 urljoin() 和页面 URL 拼成完整的 https://news.fzu.edu.cn/__local/F/4D/B3/xxx.jpg 才能下载成功。这个细节看起来简单,但不处理的话整个程序会一张图都下载不下来。

第二个收获是正则匹配图片的写法。r'src=["\']([^"\']+\.(?:jpg|jpeg|png))["\']' 这一条正则,把引号兼容、捕获组、后缀限定都放在了一起,非常紧凑。通过这次练习,我对正则里的非捕获组 (?:...)、忽略大小写 re.I 等特性理解更深了——非捕获组既能分组限定后缀,又不会污染 findall 的返回结果,是个很实用的小技巧。

第三个体会是二进制写入。图片是二进制文件,必须用 "wb" 模式写入,用 "w" 会导致图片损坏、打不开。这一点和写文本文件完全不同,需要特别小心。此外 resp.content(字节流)和 resp.text(文本)的区别也很关键——图片只能走 content。

最后,我对"如何界定页面图片"有了更清晰的认识。用正则全页面匹配 src,可以抓到 <script> 里的图片和懒加载图,覆盖面广;用 BeautifulSoup 只遍历 <img> 标签,范围更窄但更干净。两种方式都能交作业,关键是在博客里把界定标准说清楚,让读者理解为什么数量会有差异。这也让我明白:爬虫的"对错"往往不绝对,关键是逻辑清晰、结果可复现。


总结

通过这次作业,我系统练习了 requests 发送请求、用 BeautifulSoup 解析表格、用正则表达式提取商品信息、以及批量下载图片这四种最常见的爬虫场景。最大的体会是:写爬虫之前,一定要先用浏览器 F12 看清目标数据到底在哪个标签里;编码问题和动态渲染问题是最容易踩的两个坑。

三个作业层层递进:

  • 作业① 学会了静态表格数据的结构化提取,重点是精准定位标签和处理编码;
  • 作业② 学会了正则表达式提取网页数据的方法,核心是"先分块、再提字段",同时体会到不同商城编码和渲染方式的差异;
  • 作业③ 学会了图片等二进制资源的批量下载,理解了相对/绝对路径、URL 拼接和二进制写入的细节。

每一个作业都经历了"分析页面 → 编写代码 → 运行调试 → 分析结果"的完整过程,既掌握了 Python 爬虫的核心技术,也培养了严谨的问题分析能力。

posted @ 2026-10-11 15:12  Vvvicky-  阅读(16)  评论(0)    收藏  举报