数据采集第二次作业-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对象封装url和headers(特别是User-Agent),以防止服务器返回 403 错误。读取响应data.read()后,必须使用UnicodeDammit模块来自动检测网页编码(gbk或utf-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 思路: 此方法是数据处理的核心。它从数据库中查询原始数据,然后执行两个关键的“再加工”:
- 数据计算: 根据
(最高 - 最低) / 昨收动态计算出“振幅%”。 - 单位转换: 将
成交量(手)转换为成交量(万手),将成交额(元)转换为成交额(亿元)。
最后,使用 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指定市场,pn和pz控制分页。 - 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 ... (更多数据) ... 信息: 数据已提交,数据库连接已关闭。
三、心得体会
-
F12 抓包是现代爬虫的核心:
本次实验最大的收获是学会了分析动态加载的网站。我认识到,现代网页(特别是金融、电商)的数据大多是通过Fetch/XHR(API) 异步加载的。直接爬取 HTML 页面会一无所获,F12 调试工具的“网络”面板是找到真正数据源的“钥匙”。 -
API 爬虫远优于 HTML 解析:
相比于BeautifulSoup解析 HTML,直接请求 API 有巨大优势:- 数据干净:返回的
JSON格式清晰、规范,无需处理多余的 HTML 标签。 - 效率更高:JSON 数据包通常比完整的 HTML 页面小得多,请求速度更快。
- 更稳定:API 接口的字段(如
f12,f14)通常比 HTML 的class或id更稳定,后者可能随网站改版而频繁变更。
- 数据干净:返回的
-
理解 API 参数是关键:
通过分析 API 的params,我学会了如何“定制”请求。通过修改pz(每页数量),pn(页码),fields(请求字段),我可以精确控制爬取的数据范围,而不是被动地接收所有信息。 -
数据处理和格式化的重要性:
从 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()配合headers和params字典来模拟浏览器的 API 请求。params字典是关键,通过设置year: 2021和bcur_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 信息: 数据已提交,数据库连接已关闭。
三、心得体会
-
F12 抓包是爬虫的“捷径”:
本次实验通过F12调试工具,在Fetch/XHR中发现了隐藏的数据 API。这让我深刻体会到,对于动态加载的网站,分析 API 是比解析复杂 HTML 更高效、更准确的方法。 -
API 爬虫的健壮性更高:
相比于 HTML,API 返回的JSON结构更稳定。HTML 的class或id很容易在网站改版时变更,导致爬虫失效(如此前作业中遇到的情况);而 API 及其参数(如bcur_type: 11)通常会保持较长时间的兼容性。 -
数据库主键设计的严谨性:
最初将rank(排名)设为主键导致了UNIQUE constraint failed错误,因为排名存在并列。将主键修改为PRIMARY KEY (rank, school)(复合主键)解决了这个问题。这提醒我在设计数据库表时,必须充分考虑数据本身的特性,确保主键的唯一性。

浙公网安备 33010602011771号