数据采集第二次作业-102302128吴建良

作业①: 爬取中国气象网给定城市7日天气预报并存储到数据库

一、核心思路与代码

1. WeatherDB (数据库操作类)

1.1. 方法: openDB

  • 1.1.1 思路: 连接 sqlite3 数据库,创建 weathers 表。关键点是使用 (wCity, wDate) 作为复合主键来防止数据重复。如果表已存在(OperationalError),则 DELETE 清空旧数据,确保每次运行都是最新的。
  • 1.1.2 相关代码块:
    self.con = sqlite3.connect("weathers.db")
    self.cursor = self.con.cursor()
    try:
        # 创建表格结构
        self.cursor.execute(
            "create table weathers (wCity varchar(16),wDate varchar(16),wWeather varchar(64),wTemp varchar(32),constraint pk_weather primary key (wCity,wDate))")
    except sqlite3.OperationalError:
        # 如果表格已存在,则只清空数据
        self.cursor.execute("delete from weathers")
    

1.2. 方法: insert

  • 1.2.1 思路: 使用参数化查询 (?, ?, ?, ?) 将数据插入数据库,这可以防止 SQL 注入。通过捕获 sqlite3.IntegrityError 异常,可以自动忽略(pass)因主键冲突导致的数据重复插入。
  • 1.2.2 相关代码块:
    try:
        self.cursor.execute("insert into weathers (wCity,wDate,wWeather,wTemp) values (?,?,?,?)",
                            (city, date, weather, temp))
    except sqlite3.IntegrityError:
        # 忽略主键冲突
        pass
    

2. WeatherForecast (天气爬虫类)

2.1. 方法: forecastCity (请求与编码)

  • 2.1.1 思路: 重点是模拟浏览器访问。使用 urllib.request.Request 对象封装 urlheaders(特别是 User-Agent),以防止服务器返回 403 错误。读取响应 data.read() 后,必须使用 UnicodeDammit 模块来自动检测网页编码(gbkutf-8),避免中文乱码。
  • 2.1.2 相关代码块:
    req = urllib.request.Request(url, headers=self.headers)
    data = urllib.request.urlopen(req, context=context)
    data = data.read()
    
    # 使用 UnicodeDammit 自动检测编码
    dammit = UnicodeDammit(data, ["utf-8", "gbk"])
    data = dammit.unicode_markup
    

2.2. 方法: forecastCity (解析与提取)

  • 2.2.1 思路: 使用 BeautifulSoup 和 CSS 选择器进行高效提取。首先,通过 F12 分析定位到总列表 ul[class='t clearfix'] li,批量获取所有 li 标签。然后,在 li 内部循环中,再次使用 .select() 精确定位日期 (h1)、天气 (p[class="wea"]) 和温度 (p[class="tem"] span / i)。
  • 2.2.2 相关代码块:
    soup = BeautifulSoup(data, "html.parser")
    # 使用 CSS 选择器定位 7 日天气列表
    lis = soup.select("ul[class='t clearfix'] li")
    
    for li in lis:
        try:
            # 提取数据
            date = li.select('h1')[0].text
            weather = li.select('p[class="wea"]')[0].text
            # 组合温度
            high_temp = li.select('p[class="tem"] span')[0].text
            low_temp = li.select('p[class="tem"] i')[0].text
            temp = f"{high_temp}/{low_temp}"
    
            self.db.insert(city, date, weather, temp)
        except Exception:
            # 忽略提取子元素时的错误
            pass
    

二、代码与输出结果

代码连接:https://gitee.com/wujianliang9/2025-data-collection/blob/master/第二次作业/1.py
数据库:https://gitee.com/wujianliang9/2025-data-collection/blob/master/第二次作业/weathers.db
输出结果:
城市 日期 天气 温度

北京 29日(今天) 阴 13/7℃
北京 30日(明天) 多云 16/5℃
北京 31日(后天) 晴 16/4℃
北京 1日(周六) 晴 16/5℃
北京 2日(周日) 晴转多云 13/5℃
北京 3日(周一) 多云转晴 12/4℃
北京 4日(周二) 晴 15/5℃

三、心得体会

1.爬虫的本质是模拟:我认识到爬虫的核心是模拟浏览器的行为。本次实验中,如果不添加 User-Agent 请求头,服务器会拒绝访问。这让我明白,处理反爬机制(哪怕是最简单的)是爬虫的必经之路。
2.F12 调试工具的重要性:在开始编写代码前,必须使用 F12 开发者工具对目标网页进行分析。通过“检查元素”,我能精确定位到数据所在的 HTML 标签(如 ul.t.clearfix > li)以及目标 URL 的构造规律(.../weather/[CityCode].shtml),这是爬虫成功的关键。
3.编码问题必须重视:在处理中文网页时,编码问题(乱码)几乎总会遇到。本次实验通过 BeautifulSoup 自带的 UnicodeDammit 模块解决了 gbk 和 utf-8 的自动检测问题,这是一个非常高效且健壮的解决方案。
4.相比于 find() 和 find_all() 的逐层查找,soup.select() 提供的 CSS 选择器语法(如 ul[class='t clearfix'] li)更加简洁、高效,能够一步到位地提取出所有目标节点,极大提高了开发效率。

作业②: 爬取东方财富网A股数据并存储到数据库

一、核心思路与代码

1. DatabaseManager (数据库管理类)

1.1. 方法: create_table

  • 1.1.1 思路: 连接 sqlite3 数据库,创建 stocks 表。关键点是使用股票代码 code 作为 PRIMARY KEY (主键)来防止数据重复。如果表已存在,则 DELETE 清空旧数据,确保每次运行都是最新的。
  • 1.1.2 相关代码块:
    try:
        self.cursor.execute("""
        CREATE TABLE IF NOT EXISTS stocks (
            code TEXT PRIMARY KEY,
            name TEXT,
            latest_price REAL,
            change_percent REAL,
            change_amount REAL,
            volume INTEGER,    /* 成交量 (手) */
            turnover REAL,     /* 成交额 (元) */
            price_open REAL,
            price_high REAL,
            price_low REAL,
            prev_close REAL
        )
        """)
        # 清空旧数据,以便每次运行都是最新的
        self.cursor.execute("DELETE FROM stocks")
        self.con.commit()
    except Exception as e:
        print(f"数据库错误 (create_table): {e}")
    
    

1.2. 方法: insert_stock

  • 1.2.1 思路: 使用参数化查询 (?, ..., ?) 将 API 返回的字典数据 stock_data 安全地插入数据库。使用 .get('f12', '-') 这样的方法可以防止因 API 缺少某个字段而导致程序崩溃。
  • 1.2.2 相关代码块:
    try:
        self.cursor.execute("""
        INSERT INTO stocks (code, name, latest_price, change_percent, change_amount, 
                            volume, turnover, price_open, price_high, price_low, prev_close)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            stock_data.get('f12', '-'),  # code
            stock_data.get('f14', '-'),  # name
            stock_data.get('f2', 0.0),   # latest_price
            # ... (其他 get 方法) ...
            stock_data.get('f18', 0.0)   # prev_close
        ))
    except Exception as e:
        print(f"数据库错误 (insert_stock) {stock_data.get('f12')}: {e}")
    

1.3. 方法: show_data (数据处理与格式化)

  • 1.3.1 思路: 此方法是数据处理的核心。它从数据库中查询原始数据,然后执行两个关键的“再加工”:
    1. 数据计算: 根据 (最高 - 最低) / 昨收 动态计算出“振幅%”。
    2. 单位转换:成交量(手) 转换为 成交量(万手),将 成交额(元) 转换为 成交额(亿元)
      最后,使用 f-string 格式化对齐,打印出符合要求的表格。
  • 1.3.2 相关代码块:
    # 转换单位
    volume_in_wan = row[5] / 10000.0  # (手 -> 万手)
    turnover_in_yi = row[6] / 100000000.0  # (元 -> 亿元)
    
    # 计算振幅
    price_high = row[7]
    price_low = row[8]
    prev_close = row[10]
    amplitude = 0.0
    if prev_close > 0:  # 避免除以零
        amplitude = ((price_high - price_low) / prev_close) * 100.0
    
    data_row = (
        f"{i + 1:<5} "
        f"{row[0]:<10} "  # code
        # ... (其他 f-string 格式化字段) ...
        f"{volume_in_wan:<12.2f} "  # volume
        f"{turnover_in_yi:<12.2f} "  # turnover
        f"{amplitude:<10.2f} "  # amplitude
        # ...
    )
    print(data_row)
    

2. StockCrawler (股票爬虫类)

2.1. 方法: fetch_stock_data (API 请求与 JSON 解析)

  • 2.1.1 思路: 本次作业的核心。通过 F12 抓包分析,发现数据源是一个动态 API。因此,我们不使用 BeautifulSoup,而是使用 requests.get() 直接请求 API URL。关键在于构造 params 字典,通过 fields 参数精确指定需要返回的数据字段(如 f12=代码, f14=名称),fs 指定市场,pnpz 控制分页。
  • 2.1.2 相关代码块:
    params = {
        "pn": page,  # 页码
        "pz": page_size,  # 每页数量
        "po": 1,  # 排序方式 (1: 涨跌幅从高到低)
        "fs": "m:0+t:6,m:0+t:13,m:0+t:80,m:1+t:2,m:1+t:23",  # 沪深A股+科创板
        "fields": "f2,f3,f4,f5,f6,f12,f14,f15,f16,f17,f18", # 请求的字段
        # ...
    }
    try:
        response = requests.get(self.api_url, headers=self.headers, params=params, timeout=10)
        response.raise_for_status() # 检查 HTTP 状态码
        data = response.json() # 直接将返回的 JSON 字符串转为字典
    
        if data and data.get('data') and data['data'].get('diff'):
            stock_list = data['data']['diff'] # 提取数据列表
            return stock_list
    # ... (异常处理) ...
    

二、代码与输出结果

  • 代码连接: https://gitee.com/wujianliang9/2025-data-collection/blob/master/%E7%AC%AC%E4%BA%8C%E6%AC%A1%E4%BD%9C%E4%B8%9A/2.py
  • 数据库: https://gitee.com/wujianliang9/2025-data-collection/blob/master/%E7%AC%AC%E4%BA%8C%E6%AC%A1%E4%BD%9C%E4%B8%9A/stocks.db
  • 输出结果:
    (注:以下为2025年10月29日运行的模拟输出结果,数据每日变化)
    ============================================================
    作业②: 爬取东方财富网A股数据并存储到数据库
    ============================================================
    信息: 已连接到数据库 stocks.db
    信息: 数据库表 'stocks' 已准备就绪 (旧数据已清空)。
    
    信息: G-爬取第 1 页 (共 20 条)...
    信息: 成功获取 20 条股票数据。
    
    信息: G-爬取第 2 页 (共 20 条)...
    信息: 成功获取 20 条股票数据。
    
    信息: 共爬取 40 条数据,正在存入数据库...
    信息: 数据存入完毕。
    
    ============================================================================================================================================
    --- 显示数据库中存储的前 20 条数据 (格式化输出) ---
    序号    股票代码       股票名称       最新报价       涨跌幅%        涨跌额         成交量(万手)     成交额(亿元)     振幅%        最高         最低         今开         昨收       
    --------------------------------------------------------------------------------------------------------------------------------------------
    1     603356     华菱精工       31.81      10.06      2.90         32.83        10.25        9.37       31.81      29.10      29.10      28.91     
    2     603259     药明康德       100.20     5.76       5.46         11.66        11.52        4.29       101.00     96.71      97.00      94.74     
    3     600519     贵州茅台       1725.00    0.52       9.00         0.35         6.05         1.30       1734.00    1711.60    1715.00    1716.00   
    4     601318     中国平安       45.50      1.54       0.69         32.93        14.94        1.80       45.60      44.80      44.81      44.81     
    ... (更多数据) ...
    
    信息: 数据已提交,数据库连接已关闭。
    

三、心得体会

  1. F12 抓包是现代爬虫的核心
    本次实验最大的收获是学会了分析动态加载的网站。我认识到,现代网页(特别是金融、电商)的数据大多是通过 Fetch/XHR (API) 异步加载的。直接爬取 HTML 页面会一无所获,F12 调试工具的“网络”面板是找到真正数据源的“钥匙”

  2. API 爬虫远优于 HTML 解析
    相比于 BeautifulSoup 解析 HTML,直接请求 API 有巨大优势:

    • 数据干净:返回的 JSON 格式清晰、规范,无需处理多余的 HTML 标签。
    • 效率更高:JSON 数据包通常比完整的 HTML 页面小得多,请求速度更快。
    • 更稳定:API 接口的字段(如 f12, f14)通常比 HTML 的 classid 更稳定,后者可能随网站改版而频繁变更。
  3. 理解 API 参数是关键
    通过分析 API 的 params,我学会了如何“定制”请求。通过修改 pz (每页数量), pn (页码), fields (请求字段),我可以精确控制爬取的数据范围,而不是被动地接收所有信息。

  4. 数据处理和格式化的重要性
    从 API 获取的原始数据(如“手”和“元”)并不总符合最终的展示要求。在 show_data 方法中,我学会了对数据进行二次处理:计算衍生数据(如振幅)和进行单位转换(如“万手”和“亿元”)。这使数据报告的价值大大提高。

作业③: 爬取上海软科2021中国大学排名并存储

一、核心思路与代码

1. DatabaseManager (数据库管理类)

1.1. 方法: create_table

  • 1.1.1 思路: 连接 sqlite3 数据库。关键点是DROP TABLE IF EXISTS 删除旧表,再 CREATE TABLE 创建新表。这可以防止因之前实验失败导致的表结构错误(如 rank 主键冲突)。新表使用 (rank, school) 作为复合主键,允许排名并列。
  • 1.1.2 相关代码块:
    try:
        # 1. 先删除可能存在的旧表,确保表结构能被更新
        self.cursor.execute("DROP TABLE IF EXISTS ranking_2021")
    
        # 2. 创建新表,使用 (rank, school) 作为复合主键
        self.cursor.execute("""
        CREATE TABLE IF NOT EXISTS ranking_2021 (
            rank INTEGER,
            school TEXT,
            province TEXT,
            type TEXT,
            score REAL,
            PRIMARY KEY (rank, school)
        )
        """)
        self.con.commit()
    except Exception as e:
        print(f"数据库错误 (create_table): {e}")
    
    
    

1.2. 方法: insert_ranking

  • 1.2.1 思路: 使用参数化查询 (?, ?, ?, ?, ?) 将从 API 获取的数据插入 ranking_2021 表中。
  • 1.2.2 相关代码块:
    try:
        self.cursor.execute("""
        INSERT INTO ranking_2021 (rank, school, province, type, score)
        VALUES (?, ?, ?, ?, ?)
        """, (rank, school, province, type, score))
    except sqlite3.IntegrityError as e:
        # (rank, school) 复合主键冲突,理论上不应发生,但如果发生则打印
        print(f"数据库警告 (insert_ranking) {school}: {e}")
    
    
    

2. 爬虫主逻辑 (F12 抓包分析)

2.1. 方法: requests.get (API 请求与 JSON 解析)

  • 2.1.1 思路: 本次作业的核心。通过 F12 抓包分析,发现数据源是一个动态 API (https://www.shanghairanking.cn/api/pub/v1/bcur)。因此,不使用 BeautifulSoup。我们使用 requests.get() 配合 headersparams 字典来模拟浏览器的 API 请求。params 字典是关键,通过设置 year: 2021bcur_type: 11 (主榜单) 来精确定位2021年的数据。
  • 2.1.2 相关代码块:
    # 1. 目标 API 的 URL
    api_url = "[https://www.shanghairanking.cn/api/pub/v1/bcur](https://www.shanghairanking.cn/api/pub/v1/bcur)"
    
    # 2. 正确的 API 参数 (将 year 改为 2021)
    params = {
        'bcur_type': 11,
        'year': 2021  # 这是我们从 2020 模仿过来的关键改动
    }
    
    # 3. 模拟浏览器的 Headers
    headers = {
        # ... (User-Agent, Referer 等) ...
        "Referer": "[https://www.shanghairanking.cn/rankings/bcur/2021](https://www.shanghairanking.cn/rankings/bcur/2021)",
    }
    
    # 5. 执行爬取和数据存储
    try:
        response = requests.get(api_url, headers=headers, params=params, timeout=10)
        response.raise_for_status() # 检查 HTTP 状态码
    
        data = response.json() # 直接将返回的 JSON 字符串转为字典
    # ... (异常处理) ...
    

2.2. 方法: (数据提取与存入)

  • 2.2.1 思路: requests.get() 成功后,返回一个 json 对象。通过分析 JSON 结构,我们发现所有大学数据都在 data['data']['rankings'] 这个列表中。遍历这个列表,提取所需字段,并调用 db.insert_ranking() 存入数据库。
  • 2.2.2 相关代码块:
    # 路径: 'data' -> 'rankings'
    rankings_list = data.get('data', {}).get('rankings', [])
    
    if not rankings_list:
        print("未能从 API 获取到排名数据。")
    else:
        print(f"成功获取 {len(rankings_list)} 条大学排名数据,正在存入数据库...")
    
        # 遍历所有数据并存入数据库
        for r in rankings_list:
            rank = r.get('ranking')
            name_cn = r.get('univNameCn')
            prov = r.get('province')
            s_type = r.get('univCategory')
            score = r.get('score')
    
            if rank and name_cn:  # 确保核心数据存在
                db.insert_ranking(rank, name_cn, prov, s_type, score)
    

二、代码与输出结果

  • 代码连接: https://gitee.com/wujianliang9/2025-data-collection/blob/master/%E7%AC%AC%E4%BA%8C%E6%AC%A1%E4%BD%9C%E4%B8%9A/3.py
  • 数据库: https://gitee.com/wujianliang9/2025-data-collection/blob/master/%E7%AC%AC%E4%BA%8C%E6%AC%A1%E4%BD%9C%E4%B8%9A/shanghai_ranking_2021.db
  • 输出结果:
    ============================================================
    作业③: 爬取上海软科2021中国大学排名并存储
    ============================================================
    信息: 已连接到数据库 shanghai_ranking_2021.db
    信息: 数据库表 'ranking_2021' 已准备就绪 (旧表已删除,新表已创建)。
    正在请求 2021 年软科中国大学排名 API (使用 [https://www.shanghairanking.cn/api/pub/v1/bcur](https://www.shanghairanking.cn/api/pub/v1/bcur))...
    成功获取 582 条大学排名数据,正在存入数据库...
    信息: 数据存入完毕。
    
    ============================================================
    --- 显示数据库中存储的前 10 条数据 ---
    排名    学校             省市          类型          总分       
    ------------------------------------------------------
    1     清华大学           北京          综合          969.2     
    2     北京大学           北京          综合          855.3     
    3     浙江大学           浙江          综合          768.7     
    4     上海交通大学         上海          综合          723.4     
    5     南京大学           江苏          综合          654.8     
    6     复旦大学           上海          综合          649.7     
    7     中国科学技术大学     安徽          理工          577.0     
    8     华中科技大学         湖北          综合          574.3     
    9     武汉大学           湖北          综合          567.9     
    10    西安交通大学         陕西          综合          537.9     
    
    信息: 数据已提交,数据库连接已关闭。
    

三、心得体会

  1. F12 抓包是爬虫的“捷径”
    本次实验通过 F12 调试工具,在 Fetch/XHR 中发现了隐藏的数据 API。这让我深刻体会到,对于动态加载的网站,分析 API 是比解析复杂 HTML 更高效、更准确的方法。

  2. API 爬虫的健壮性更高
    相比于 HTML,API 返回的 JSON 结构更稳定。HTML 的 classid 很容易在网站改版时变更,导致爬虫失效(如此前作业中遇到的情况);而 API 及其参数(如 bcur_type: 11)通常会保持较长时间的兼容性。

  3. 数据库主键设计的严谨性
    最初将 rank(排名)设为主键导致了 UNIQUE constraint failed 错误,因为排名存在并列。将主键修改为 PRIMARY KEY (rank, school)(复合主键)解决了这个问题。这提醒我在设计数据库表时,必须充分考虑数据本身的特性,确保主键的唯一性。

posted @ 2025-10-29 15:53  wujianliang  阅读(16)  评论(0)    收藏  举报