数据采集第二次作业

数据采集实践第二次作业

姓名:陈瑀涵 学号:102402107
码云文件夹链接:2026data-collection-practice

本次作业使用 Python 完成三个定向数据采集任务,分别采集城市天气、股票行情和 2021 年中国大学排名。网络请求主要使用 requests,网页解析使用 BeautifulSoup,JSON 接口数据直接使用 response.json() 处理,最后统一保存到 SQLite 数据库中。

一 任务过程与结果

1 作业代码和效果展示

1.1 中国气象网 7 日天气预报

Gitee代码链接:task1_weather.py

本任务选择北京、上海、福州、广州和成都五个城市。程序根据城市代码拼接中国气象网页面地址,再从 ul.t.clearfix 中找到每天的预报节点,提取日期、天气、最高温度、最低温度、风向和风力等级。

首先配置需要采集的城市及其在中国气象网中的城市代码。使用字典保存城市名和城市代码,后续可以通过循环依次完成五个城市的采集:

# 键是最终写入数据库的城市名称,值是中国气象网使用的城市代码。
CITIES = {
    "北京": "101010100",
    "上海": "101020100",
    "福州": "101230101",
    "广州": "101280101",
    "成都": "101270101",
}

# 使用占位符保留城市代码的位置,便于重复拼接不同城市的 URL。
WEATHER_URL = "http://www.weather.com.cn/weather/{city_code}.shtml"

请求页面后,通过 CSS 选择器定位 7 日天气列表。解析过程中先判断节点是否存在,避免网页中某个字段暂时缺失时出现空对象错误:

# 创建带请求头和自动重试功能的 Session。
session = create_session()
response = session.get(
    WEATHER_URL.format(city_code=city_code), timeout=15
)
# 如果服务器返回 4xx 或 5xx 状态码,立即抛出异常。
response.raise_for_status()
response.encoding = response.apparent_encoding or "utf-8"

soup = BeautifulSoup(response.text, "html.parser")
forecast_items = soup.select("ul.t.clearfix > li")

# 页面通常正好返回 7 个 li 节点,这里再用切片限制为前 7 天。
for item in forecast_items[:7]:
    date_node = item.select_one("h1")          # 日期
    weather_node = item.select_one("p.wea")   # 天气现象
    high_node = item.select_one("p.tem span") # 最高温度
    low_node = item.select_one("p.tem i")     # 最低温度
    wind_node = item.select_one("p.win")      # 风向所在节点
    level_node = item.select_one("p.win i")   # 风力等级

    # 风向保存在 span 的 title 属性中,去重后用“转”连接。
    wind_directions = []
    if wind_node:
        for span in wind_node.select("span"):
            direction = span.get("title")
            if direction and direction not in wind_directions:
                wind_directions.append(direction)

    record = WeatherRecord(
        city=city,
        forecast_date=date_node.get_text(strip=True) if date_node else "",
        weather=weather_node.get_text(strip=True) if weather_node else "",
        high_temperature=high_node.get_text(strip=True) if high_node else "--",
        low_temperature=low_node.get_text(strip=True) if low_node else "--",
        wind="转".join(wind_directions) or "--",
        wind_level=level_node.get_text(strip=True) if level_node else "--",
        collected_at=collected_at,
    )

数据表使用 (city, forecast_date) 作为唯一约束。再次运行程序时,ON CONFLICT 会更新同一城市同一天的预报,不会不断产生重复记录:

CREATE TABLE IF NOT EXISTS weather_forecast (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    city TEXT NOT NULL,
    forecast_date TEXT NOT NULL,
    weather TEXT,
    high_temperature TEXT,
    low_temperature TEXT,
    wind TEXT,
    wind_level TEXT,
    collected_at TEXT NOT NULL,
    UNIQUE(city, forecast_date)  -- 城市和日期共同确定一条天气记录
)
-- excluded 表示本次准备插入的新数据;发生唯一键冲突时更新天气字段。
INSERT INTO weather_forecast (
    city, forecast_date, weather, high_temperature,
    low_temperature, wind, wind_level, collected_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(city, forecast_date) DO UPDATE SET
    weather=excluded.weather,
    high_temperature=excluded.high_temperature,
    low_temperature=excluded.low_temperature,
    wind=excluded.wind,
    wind_level=excluded.wind_level,
    collected_at=excluded.collected_at
中国气象网采集过程
图 1 天气采集程序运行结果
天气采集程序运行结果
图 2 天气采集程序运行结果
天气数据存储结果
图 3 天气数据存储结果

1.2 东方财富股票行情

Gitee代码链接:task1_weather.py

进入东方财富行情页面后,打开浏览器 F12,在 Network 面板中筛选 Fetch/XHR 请求,可以找到返回股票列表的接口。接口返回 JSON,其中 data.diff 是股票记录列表。本程序采集股票代码、名称、最新价、涨跌幅、涨跌额、成交量、成交额、最高价、最低价、开盘价和昨收价等字段。

部分字段对应关系如下:

接口字段 含义
f12 股票代码
f14 股票名称
f2 最新价
f3 涨跌幅
f5 成交量
f6 成交额

接口参数决定请求哪一页、每页数量、市场范围和返回字段。为了便于读者理解,我在代码中写明了关键参数和字段的用途:

PARAMS = {
    "pn": "1",       # 页码:第 1 页
    "pz": "100",     # 每页返回 100 条股票记录
    "po": "1",       # 排序方向
    "fid": "f3",     # 按涨跌幅字段排序
    # 沪深京 A 股市场范围
    "fs": "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048",
    # 只请求本次作业需要的字段,减少无关数据
    "fields": "f12,f14,f2,f3,f4,f5,f6,f15,f16,f17,f18",
}

使用 params 传参后,requests 会自动完成 URL 查询字符串的拼接和编码:

response = create_session().get(STOCK_API, params=PARAMS, timeout=20)
response.raise_for_status()  # 检查 HTTP 请求是否成功
payload = response.json()

# data 可能为 None,因此先用 or {} 转成空字典,再读取 diff。
stock_items = (payload.get("data") or {}).get("diff") or []

接口中的无数据字段可能是 -、空字符串或 null。下面的函数先统一进行清洗,再转换为浮点数;无法转换的值保存为 Python 的 None,写入 SQLite 后对应 NULL:

def number_or_none(value: object) -> float | None:
    """把有效数字转换为 float,把短横线、空值转换为 None。"""
    if value in (None, "", "-"):
        return None
    try:
        return float(value)
    except (TypeError, ValueError):
        # 单个异常字段不会导致整批股票数据采集失败。
        return None

解析列表时使用接口字段构造 StockRecord。数据类让每条记录的字段名称和类型更明确:

records = [
    StockRecord(
        stock_code=str(item.get("f12", "")),
        stock_name=str(item.get("f14", "")),
        latest_price=number_or_none(item.get("f2")),
        change_percent=number_or_none(item.get("f3")),
        change_amount=number_or_none(item.get("f4")),
        volume=number_or_none(item.get("f5")),
        turnover=number_or_none(item.get("f6")),
        highest=number_or_none(item.get("f15")),
        lowest=number_or_none(item.get("f16")),
        opening=number_or_none(item.get("f17")),
        previous_close=number_or_none(item.get("f18")),
        collected_at=collected_at,
    )
    for item in stock_items
]

股票行情会随时间变化,因此表中使用“股票代码 + 采集时间”作为唯一条件,既避免同一时刻的重复记录,也能保留不同时间的行情:

CREATE TABLE IF NOT EXISTS stock_quotes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    stock_code TEXT NOT NULL,
    stock_name TEXT NOT NULL,
    latest_price REAL,
    change_percent REAL,
    change_amount REAL,
    volume REAL,
    turnover REAL,
    highest REAL,
    lowest REAL,
    opening REAL,
    previous_close REAL,
    collected_at TEXT NOT NULL,
    UNIQUE(stock_code, collected_at)
)
东方财富股票接口分析
图 4 东方财富股票接口分析
股票采集程序运行结果
图 5 股票采集程序运行结果
股票行情数据存储结果
图 6 股票行情数据存储结果

1.3 软科中国大学排名 2021 主榜

Gitee代码链接:task1_weather.py

进入 2021 中国大学排名页面后打开 F12,通过 Network 面板分析排名数据接口。进入2021中国大学排名页面后打开F12,通过Network面板查看排名数据接口。查看请求的Headers、Preview和Response,可以确认接口参数以及返回的院校排名数据。接口返回 JSON,院校列表位于返回数据中的 rankings 字段。程序提取排名、学校、省市、类型和总分,并把完整主榜写入数据库。

接口地址中的 year 参数指定榜单年份,bcur_type=11 对应中国大学排名主榜:

RANKING_API = "https://www.shanghairanking.cn/api/pub/v1/bcur"
PARAMS = {"bcur_type": "11", "year": "2021"}

# 通过 params 发送查询参数,并将服务器返回内容直接解析为 JSON。
response = create_session().get(RANKING_API, params=PARAMS, timeout=20)
response.raise_for_status()
payload = response.json()

data = payload.get("data") or {}
# rankings 是当前接口的榜单字段,results 用于兼容可能的字段调整。
ranking_items = data.get("rankings") or data.get("results") or []

考虑到接口字段名可能调整,编写 first_value() 依次检查几个候选字段名,返回第一个存在且非空的值:

def first_value(item: dict, *keys: str) -> str:
    """按顺序查找候选字段,返回第一个非空值。"""
    for key in keys:
        value = item.get(key)
        if value not in (None, ""):
            return str(value)
    return ""

遍历所有院校时,不对列表进行切片,保证将 2021 主榜的全部院校写入数据库。每一项被整理为字段清晰的 UniversityRecord:

records: list[UniversityRecord] = []
for item in ranking_items:
    records.append(
        UniversityRecord(
            ranking=first_value(item, "ranking", "rank", "rk"),
            university=first_value(item, "univNameCn", "name", "univName"),
            province=first_value(item, "province", "provinceName"),
            university_type=first_value(item, "univCategory", "category", "type"),
            total_score=first_value(item, "score", "totalScore"),
            year=2021,
            collected_at=collected_at,
        )
    )

写入时以“年份 + 学校名称”为唯一约束。同一年度同一学校再次采集时更新名次、类型和分数,便于反复测试程序:

INSERT INTO university_rankings (
    ranking, university, province, university_type,
    total_score, year, collected_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(year, university) DO UPDATE SET
    ranking=excluded.ranking,
    province=excluded.province,
    university_type=excluded.university_type,
    total_score=excluded.total_score,
    collected_at=excluded.collected_at

浏览器 F12 分析过程录制如下。录制中展示了接口请求参数及返回的排名数据:

软科中国大学排名接口分析过程
图 7 软科中国大学排名 2021 主榜接口分析过程

程序输出前 20 条记录用于快速核对,全部记录均写入数据库。

大学排名采集程序运行结果
图 8 大学排名采集程序运行结果
大学排名数据存储结果
图 9 大学排名数据存储结果

2 实现思路和问题处理

三个任务采用相似的处理流程:先发送请求,再检查响应状态,随后解析 HTML 或 JSON,整理成结构统一的记录,最后批量写入 SQLite。公共的请求重试、请求头和数据库连接代码被放在 common.py 中,避免在三个文件中反复编写。

公共请求函数通过 Session 复用连接,并对常见的临时错误状态码进行最多 3 次重试。backoff_factor 会让每次重试之间保留一定间隔,避免短时间内连续请求服务器:

def create_session() -> requests.Session:
    """创建带浏览器请求头和自动重试功能的会话。"""
    session = requests.Session()
    retry = Retry(
        total=3,                 # 整个请求最多重试 3 次
        connect=3,               # 连接失败时重试
        read=3,                  # 读取响应失败时重试
        backoff_factor=0.8,      # 重试等待时间按比例递增
        status_forcelist=(429, 500, 502, 503, 504),
        allowed_methods=("GET",),
    )
    session.mount("http://", HTTPAdapter(max_retries=retry))
    session.mount("https://", HTTPAdapter(max_retries=retry))
    session.headers.update({
        "User-Agent": "Mozilla/5.0 ... Chrome/120.0.0.0 Safari/537.36",
        "Accept-Language": "zh-CN,zh;q=0.9",
    })
    return session

数据库工具统一把数据库文件放在项目的 data 目录中,并使用 executemany() 批量写入,减少重复的数据库操作:

def connect_database(filename: str) -> sqlite3.Connection:
    """连接 data 目录中的 SQLite 数据库。"""
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    connection = sqlite3.connect(DATA_DIR / filename)
    connection.execute("PRAGMA journal_mode=WAL")
    return connection


def save_rows(connection, sql, rows) -> int:
    """批量执行 SQL,提交事务并返回写入数量。"""
    materialized_rows = list(rows)
    connection.executemany(sql, materialized_rows)
    connection.commit()
    return len(materialized_rows)

网络请求不一定每次都成功,因此程序设置了超时和自动重试,并使用 raise_for_status() 及时发现错误。
接口可能出现空值或短横线。股票程序使用 number_or_none() 统一转换数值,遇到无效内容时写入数据库的 NULL,避免强制转换导致程序中断。

数据库写入也进行了重复数据处理。天气记录以“城市和预报日期”为唯一条件,大学排名以“年份和学校”为唯一条件,重复执行时更新原记录;股票行情具有时间变化,使用“股票代码和采集时间”区分不同时刻的数据。

二 作业总结与体会

1 作业心得

完成这次作业后,我对网页中“看到的数据”和程序实际获取的数据之间的关系有了更清楚的认识。天气页面的数据直接包含在 HTML 中,适合使用 BeautifulSoup 按标签解析;股票和大学排名数据可以通过接口获取,更适合先用 F12 分析请求地址和参数,再使用 JSON 解析。选择正确的采集方式,比直接对整个网页使用正则表达式更稳定,也更容易理解。

我也体会到,把数据采集下来并不代表任务已经结束。采集结果还需要进行字段整理、空值处理、重复数据控制和数据库存储。加入唯一约束以后,多次运行程序不会产生大量无意义的重复记录;加入超时和重试以后,程序在网络短暂波动时也更稳定。这些处理让代码从“能够运行”变成“比较方便重复使用”。

2 不足与改进方向

目前程序采集的是运行时刻的页面和接口数据。如果网站以后调整网页标签、接口地址或字段名称,解析代码仍需要跟着修改。另外,股票程序当前一次采集 100 条行情,主要用于展示 JSON 接口采集和数据库存储过程,还没有实现自动翻页与定时采集。

后续可以增加日志文件,把每次采集的成功数量和失败原因保存下来;还可以把城市、股票数量和数据库路径放进配置文件,减少修改源代码的次数。如果需要长期运行,也可以配合定时任务每天自动采集,并对历史数据进行趋势分析和可视化。

posted @ 2026-11-06 11:28  陈瑀涵  阅读(19)  评论(0)    收藏  举报