作业①: 爬取中国气象网给定城市7日天气预报并存储到数据库
一、核心思路与代码
1. WeatherDB (数据库操作类)
1.1. 方法: openDB
1.1.1 思路:
连接 sqlite3 数据库,创建 weathers 表。关键点是使用 (wCity, wDate) 作为复合主键来防止数据重复。如果表已存在(捕获异常),则 DELETE 清空旧数据,确保每次运行都是最新的。
1.1.2 相关代码块:
def openDB(self):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:self.cursor.execute("delete from weathers")
2. WeatherForecast (天气爬虫类)
2.1. 方法: forecastCity (请求与编码)
2.1.1 思路:
重点是模拟浏览器访问。使用 requests.Session 封装 url 和 headers(特别是 User-Agent),以防止服务器返回 403 错误。通过设置响应编码为 utf-8 处理中文乱码,确保文本正常显示。
2.1.2 相关代码块:
def forecastCity(self, city):if city not in self.cityCode:print(f"错误:未找到城市「{city}」的编码")returnurl = f"https://www.weather.com.cn/weather/{self.cityCode[city]}.shtml"try:# 模拟浏览器请求,设置超时和证书验证response = self.session.get(url,headers=self.headers,timeout=15,verify=False)response.encoding = "utf-8" # 设置编码处理中文html_content = response.text# 后续解析逻辑...except Exception as e:print(f"获取「{city}」天气数据失败:{e}")
2.2. 方法: forecastCity (解析与提取)
2.2.1 思路:
使用 BeautifulSoup 和 CSS 选择器进行高效提取。首先,通过分析网页结构定位到总列表 ul.t.clearfix li,批量获取所有 li 标签(包含每日天气数据)。然后,在 li 内部循环中,通过 find 方法精确定位日期 (h1)、天气状况 (p[class="wea"]) 和温度(高温 span 和低温 i),并组合温度数据。
2.2.2 相关代码块:
soup = BeautifulSoup(html_content, "lxml")
weather_list = soup.select("ul.t.clearfix li") # 获取7日天气列表for day in weather_list:try:# 提取日期date = day.find("h1").get_text(strip=True)# 提取天气状况weather = day.find("p", class_="wea").get_text(strip=True)# 提取温度(高温+低温)temp_high = day.find("p", class_="tem").find("span")temp_high = temp_high.get_text(strip=True) if temp_high else ""temp_low = day.find("p", class_="tem").find("i").get_text(strip=True)temperature = f"{temp_high}/{temp_low}" if temp_high else temp_low# 插入数据库self.db.insert(city, date, weather, temperature)except Exception as e:print(f"解析单天数据出错:{e}")
二、代码与输出结果
城市集选择北京、上海、广州、深圳
代码链接:https://gitee.com/spacetime666/2025_crawl_project/blob/master/作业2/weather.py
数据库链接:https://gitee.com/spacetime666/2025_crawl_project/blob/master/作业2/weathers.db
输出实例
城市 日期 天气状况 温度
北京 11日(今天) 雾 2℃
北京 12日(明天) 晴 17℃/3℃
北京 13日(后天) 晴 14℃/0℃
北京 14日(周五) 晴 12℃/1℃
北京 15日(周六) 晴转多云 13℃/3℃
北京 16日(周日) 晴 6℃/0℃
北京 17日(周一) 晴 6℃/0℃
上海 11日(今天) 阴 14℃
上海 12日(明天) 小雨 17℃/13℃
上海 13日(后天) 阴转晴 18℃/12℃
上海 14日(周五) 晴转多云 18℃/10℃
上海 15日(周六) 多云转阴 19℃/12℃
上海 16日(周日) 多云转小雨 22℃/10℃
上海 17日(周一) 阴转多云 12℃/3℃
广州 11日(今天) 多云 17℃
广州 12日(明天) 多云 25℃/16℃
广州 13日(后天) 多云 25℃/17℃
广州 14日(周五) 多云 26℃/18℃
广州 15日(周六) 多云 26℃/18℃
广州 16日(周日) 多云 27℃/18℃
广州 17日(周一) 多云转阴 26℃/14℃
深圳 11日(今天) 阴 19℃
深圳 12日(明天) 阴转多云 25℃/19℃
深圳 13日(后天) 多云 25℃/19℃
深圳 14日(周五) 多云转晴 26℃/19℃
深圳 15日(周六) 晴 27℃/20℃
深圳 16日(周日) 晴 28℃/21℃
深圳 17日(周一) 晴转阴 29℃/17℃
三、心得体会
本次实验让我深刻体会到,对于静态或半静态网页,BeautifulSoup + CSS 选择器是解析数据的高效工具。气象网的 7 日天气数据嵌入在固定的 HTML 结构中,通过 F12 元素审查,找到ul.t.clearfix li这个核心容器,再逐层定位h1(日期)、p.wea(天气)、p.tem(温度)等标签,就能精准提取目标数据。这让我明白,HTML 解析的关键在于 “找准结构、精准匹配”,标签和类名的正确识别直接决定爬取成败。
1、复合主键是数据去重的核心保障:
在设计数据库时,(wCity, wDate)复合主键的设置是本次实验的关键亮点。相比于单纯清空旧数据,复合主键能从根本上避免同一城市同一日期的天气数据重复插入,既保证了数据唯一性,又保留了数据库的灵活性。这让我理解到,数据库约束不是多余的设置,而是爬虫数据存储中 “防脏数据” 的重要手段。
异常处理与数据兼容性不可或缺:爬取过程中遇到了温度数据不统一的问题 —— 部分日期只有低温没有高温。通过判断temp_high是否存在,动态组合“高温/低温”或“低温”格式,避免了数据缺失或格式混乱。这让我认识到,原始网页数据往往存在边缘情况,提前预判并处理空值、异常格式,是保证数据完整性和可用性的关键。
2、模拟浏览器请求是突破反爬的基础:
最初直接请求网页时曾出现访问失败,后来通过设置User-Agent模拟浏览器身份,同时配置 TLS 适配器和重试机制,成功解决了 403 禁止访问和连接超时问题。这让我明白,现代网站都有基础反爬机制,合理设置请求头、优化网络配置,是爬虫能够稳定运行的前提,也是合规爬取的基本要求。
作业②: 爬取东方财富网A股数据并存储到数据库
一、核心思路与代码
1. StockDB (数据库操作模块)
1.1. 函数: init_db (初始化数据库)
1.1.1 思路:连接 sqlite3 数据库,创建 stock_info 表用于存储股票数据。关键点是设置 stock_code 为唯一键(UNIQUE)防止重复存储,同时使用自增主键 id 便于数据管理。表结构包含股票代码、名称、价格、涨跌幅等核心字段,以及 crawl_time 记录数据爬取时间。
1.1.2 相关代码块:
def init_db(db_name="stock_data.db"):conn = sqlite3.connect(db_name)cursor = conn.cursor()# 创建表结构,定义字段类型和约束cursor.execute('''CREATE TABLE IF NOT EXISTS stock_info (id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增主键,用于唯一标识每条记录stock_code TEXT NOT NULL UNIQUE, -- 股票代码唯一,避免重复存储stock_name TEXT, -- 股票名称latest_price REAL, -- 最新价price_change_percent REAL, -- 涨跌幅(%)price_change_amount REAL, -- 涨跌额volume INTEGER, -- 成交量(手)turnover REAL, -- 成交额(万)open_price REAL, -- 今开价prev_close_price REAL, -- 昨收价highest_price REAL, -- 最高价lowest_price REAL, -- 最低价crawl_time DATETIME -- 爬取时间,记录数据获取时间)''')conn.commit()conn.close()print(f"数据库初始化完成(文件:{db_name})")
1.2. 函数: save_to_db (存储数据到数据库)
1.2.1 思路:采用参数化查询 (?, ?, ...) 插入数据,避免 SQL 注入风险。通过 INSERT OR IGNORE 语法,自动忽略因 stock_code 重复导致的插入错误。记录当前时间作为 crawl_time,便于追溯数据时效性。统计成功存储的条数,反馈存储结果。
1.2.2 相关代码块:
def save_to_db(stocks, db_name="stock_data.db"):if not stocks:print("无数据可存储")returnconn = sqlite3.connect(db_name)cursor = conn.cursor()crawl_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 记录当前爬取时间success_count = 0for stock in stocks:try:# 提取股票数据,映射到表字段data = (stock.get("f12"), # 股票代码stock.get("f14"), # 股票名称stock.get("f2"), # 最新价stock.get("f3"), # 涨跌幅(%)stock.get("f4"), # 涨跌额stock.get("f5"), # 成交量(手)stock.get("f6"), # 成交额(万)stock.get("f15"), # 今开价stock.get("f16"), # 昨收价stock.get("f17"), # 最高价stock.get("f18"), # 最低价crawl_time)# 插入数据,忽略重复的股票代码cursor.execute('''INSERT OR IGNORE INTO stock_info (stock_code, stock_name, latest_price, price_change_percent,price_change_amount, volume, turnover, open_price,prev_close_price, highest_price, lowest_price, crawl_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', data)success_count += 1except Exception as e:print(f"单条数据存储失败(代码:{stock.get('f12')}):{str(e)}")continueconn.commit()conn.close()print(f"数据存储完成,成功存入{success_count}/{len(stocks)}条数据")
2. StockSpider (股票数据爬取模块)
2.1. 函数: get_stock_data (爬取股票数据)
2.1.1 思路:调用东方财富网的 API 接口获取 A 股数据(沪 A + 深 A)。通过 params 控制分页(页码 pn、每页条数 pz)和筛选条件(fs 参数指定市场),fields 参数定义需要返回的字段(代码、名称、价格等)。设置 User-Agent 和 Referer 模拟浏览器请求,避免被服务器拦截。解析 JSON 响应,提取 data.diff 中的股票列表,处理请求异常和无数据情况。
2.1.2 相关代码块:
def get_stock_data(page=1, page_size=20):url = "https://83.push2.eastmoney.com/api/qt/clist/get"# 构造请求参数,控制分页、筛选条件和返回字段params = {"pn": page, # 页码"pz": page_size, # 每页条数"po": 1,"np": 1,"ut": "bd1d9ddb00efe4882a3fe839fff74e5","fltt": 2,"invt": 2,"fid": "f3", # 按涨跌幅排序"fs": "m:1 t:2,m:1 t:23", # 筛选条件:沪A(t:2)+深A(t:23)"fields": "f12,f14,f2,f3,f4,f5,f6,f15,f16,f17,f18" # 需要的字段}# 设置请求头,模拟浏览器访问headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36","Referer": "https://quote.eastmoney.com/" # 模拟来源页面,提高请求成功率}try:response = requests.get(url, params=params, headers=headers, timeout=10)response.raise_for_status() # 检查请求是否成功(非200状态码会抛异常)data = response.json()# 提取有效数据(diff字段为股票列表)if data.get("data") and data["data"].get("diff"):return data["data"]["diff"]else:print("未获取到股票数据(可能页面不存在或无数据)")return []except Exception as e:print(f"爬取失败:{str(e)}")return []
3. 辅助函数: print_stock_info (格式化打印股票信息)
3.1 思路:将爬取的股票数据按固定格式打印,展示核心字段(序号、代码、名称、最新价、涨跌幅等),使用对齐符(<)保证列对齐,增强可读性。对空值或异常值显示为 “——”,避免格式混乱。
3.2 相关代码块:
def print_stock_info(stocks):if not stocks:return# 打印表头print(f"\n{'序号':<6}{'股票代码':<10}{'名称':<8}{'最新价':<8}{'涨跌幅(%)':<10}{'涨跌额':<8}{'成交量(手)':<12}{'成交额(万)':<12}")print("-" * 90)# 循环打印每条股票数据for i, stock in enumerate(stocks, start=1):code = stock.get("f12", "未知")name = stock.get("f14", "未知")# 处理空值显示price = stock.get("f2", "——") if stock.get("f2") is not None else "——"change_percent = f"{stock.get('f3', '——')}%" if stock.get("f3") is not None else "——"change_amount = stock.get("f4", "——") if stock.get("f4") is not None else "——"volume = stock.get("f5", "——") if stock.get("f5") is not None else "——"turnover = f"{stock.get('f6', '——')}万" if stock.get("f6") is not None else "——"print(f"{i:<6}{code:<10}{name:<8}{price:<8}{change_percent:<10}{change_amount:<8}{volume:<12}{turnover:<12}")
二、代码与输出结果
代码连接:
https://gitee.com/spacetime666/2025_crawl_project/blob/master/作业2/stock.py
数据库:
https://gitee.com/spacetime666/2025_crawl_project/blob/master/作业2/stock_data.db
输出结果实例:
序号 股票代码 名称 最新价 涨跌幅(%) 涨跌额 成交量(手) 成交额(万)
1 688233 神工股份 73.43 20.0% 12.24 226885 1647290082.0万
2 688585 上纬新材 130.2 20.0% 21.7 181285 2146648727.0万
3 688448 磁谷科技 44.88 20.0% 7.48 120271 505389469.0万
4 688670 金迪克 24.97 19.99% 4.16 134304 309943207.0万
5 688148 芳源股份 12.91 19.65% 2.12 1131676 1383747914.0万
6 688028 沃尔德 83.9 17.95% 12.77 182025 1462183214.0万
7 688170 德龙激光 38.0 15.71% 5.16 116783 416528634.0万
8 688411 海博思创 299.65 13.0% 34.47 72483 2176297230.0万
9 688733 壹石通 33.98 10.5% 3.23 274173 908160649.0万
10 688710 益诺思 50.15 10.46% 4.75 33423 162850636.0万
11 688221 前沿生物-U 16.68 10.32% 1.56 316793 514437672.0万
12 600537 亿晶光电 5.12 10.11% 0.47 4552970 2268981295.0万
13 600094 大名城 5.79 10.08% 0.53 1377355 781026621.0万
14 600408 安泰集团 4.7 10.07% 0.43 3294954 1381596875.0万
15 600403 大有能源 9.08 10.06% 0.83 1365127 1157683924.0万
16 600429 三元股份 6.02 10.05% 0.55 201936 121565376.0万
17 600714 金瑞矿业 14.37 10.03% 1.31 174612 239661106.0万
18 601567 三星医疗 28.78 10.02% 2.62 164320 471635895.0万
19 605580 恒盛能源 44.72 10.01% 4.07 157672 671373345.0万
20 603396 金辰股份 37.58 10.01% 3.42 125567 458448338.0万
21 603663 三祥新材 37.48 10.01% 3.41 457881 1661107691.0万
22 601061 中信金属 17.15 10.01% 1.56 895086 1463467308.0万
23 603683 晶华新材 30.58 10.0% 2.78 130565 387192529.0万
24 603122 合富中国 18.26 10.0% 1.66 487784 869959941.0万
25 600831 广电网络 5.5 10.0% 0.5 398503 212955445.0万
26 600810 神马股份 11.11 10.0% 1.01 711006 769722535.0万
27 603686 福龙马 35.1 10.0% 3.19 1199405 4025337735.0万
28 600475 华光环能 19.15 9.99% 1.74 357780 662283161.0万
29 600113 浙江东日 53.27 9.99% 4.84 42954 227760196.0万
30 600737 中粮糖业 17.62 9.99% 1.6 595422 1020629536.0万
数据存储完成,成功存入30/30条数据
三、心得体会
1、F12 抓包是动态网页爬虫的核心:
本次实验最大的收获是掌握了动态加载网站的爬取逻辑。东方财富网的股票数据是通过 XHR 异步请求 API 获取的,直接爬取 HTML 页面只会得到空数据。借助 F12 “网络” 面板,筛选 Fetch/XHR 请求,找到返回 JSON 数据的真实 API 接口,才是获取数据的核心 “突破口”。这让我深刻认识到,F12 调试工具是现代爬虫工程师的必备技能。
2、API 爬虫的优势远胜 HTML 解析:
相比于任务一中的 HTML 解析,直接请求 API 的优势十分明显。一是数据更干净,返回的 JSON 格式结构化强,字段明确(如f12对应股票代码、f14对应股票名称),无需过滤多余 HTML 标签;二是效率更高,JSON 数据包体积远小于完整 HTML 页面,请求速度和解析效率大幅提升;三是稳定性更强,API 的字段定义通常长期不变,而 HTML 的类名、结构可能随网站改版频繁变更,导致爬虫失效。
理解 API 参数是实现 “定制化爬取” 的关键:通过分析 API 的params参数,我学会了主动控制爬取范围。修改pn(页码)和pz(每页条数)可以实现分页爬取,调整fields参数能只请求需要的字段(如股票代码、价格、成交量),避免获取冗余数据。这让我从 “被动接收数据” 转变为 “主动定制数据”,大幅提升了爬虫的灵活性和实用性。
3、数据格式化与去重是提升数据价值的关键:
从 API 获取的原始数据需要二次处理才能满足展示和存储需求。在print_stock_info函数中,我对空值统一显示为 “——”,对涨跌幅添加 “%”、成交额添加 “万” 单位,让输出更直观;在数据库设计中,设置stock_code为唯一键,配合INSERT OR IGNORE语法,自动忽略重复数据。这让我明白,爬虫不仅是 “获取数据”,更要 “处理数据”,格式化和去重能让数据更具使用价值。
作业③: 爬取 2021 软科中国大学排名并存储到数据库
一、核心思路与代码
1. RankingDB(数据库操作类)
1.1 方法: openDB
1.1.1 思路:
连接 sqlite3 数据库,创建 rankings 表用于存储排名核心数据。考虑到排名数据的唯一性要求,使用 rank(排名)作为主键,同时对 name(学校名称)设置 UNIQUE 约束,双重保障数据不重复。若表已存在,直接清空旧数据,确保每次爬取都获取最新完整的排名结果。
1.1.2 相关代码块:
def openDB(self):self.con = sqlite3.connect("university_rankings_2021.db")self.cursor = self.con.cursor()try:self.cursor.execute("""create table rankings (rank INTEGER PRIMARY KEY,name TEXT UNIQUE NOT NULL,province TEXT,category TEXT,score REAL NOT NULL)""")except:self.cursor.execute("delete from rankings")
2. UniversityRankingCrawler(排名爬虫类)
2.1 方法: crawl(请求与 JS 保存)
2.1.1 思路:
目标排名数据嵌入在 Nuxt 框架生成的 JSONP 格式 JS 文件中,而非直接暴露的 API 接口。核心是模拟浏览器请求头(User-Agent、Referer 等)突破 403 反爬限制,成功下载 JS 文件后本地备份,为后续解析提供原始数据支撑,同时避免重复请求给服务器造成压力。
2.1.2 相关代码块:
def crawl(self, js_url):print("正在下载JS文件...")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/2021","Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8","Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2","Connection": "keep-alive"}try:response = requests.get(js_url, headers=headers, timeout=30)response.raise_for_status()self.js_content = response.textwith open("payload_2021_correct.js", "w", encoding="utf-8") as f:f.write(self.js_content)print("正确的JS文件已保存到本地:payload_2021_correct.js")except Exception as e:print(f"JS文件下载失败:{str(e)}")return Falsereturn True
2.2 方法: parseVariableMapping(破解变量代号)
2.2.1 思路:
这是解决本次爬取核心难点的关键步骤。Nuxt 生成的 JS 文件将真实值用 a、b、f 等变量代号替代,需先解析 JS 中函数的 “形参 - 实参” 对应关系。通过定位函数定义提取参数列表,定位函数调用提取实参列表,构建变量映射表,为后续数据解码提供依据。
2.2.2 相关代码块:
def parseVariableMapping(self):print("正在解析变量映射表...")# 定位函数定义func_start = self.js_content.find("(function(")if func_start == -1:raise ValueError("未找到函数定义,请检查JS格式")# 提取参数列表params_end = self.js_content.find(")", func_start + 10)params = [x.strip() for x in self.js_content[func_start+10:params_end].split(",") if x.strip()]# 跳过函数体,定位实参body_l = self.js_content.find("{", params_end)body_l, body_r = self.slice_block(self.js_content, body_l, "{", "}")i = body_r + 1while i < len(self.js_content) and self.js_content[i].isspace():i += 1if i < len(self.js_content) and self.js_content[i] == ')':i += 1while i < len(self.js_content) and self.js_content[i].isspace():i += 1args_l, args_r = self.slice_block(self.js_content, i, "(", ")")args_str = self.js_content[args_l+1:args_r]# 转换为Python可解析格式safe_str = (args_str.replace("true", "True").replace("false", "False").replace("null", "None").replace("void 0", "None").replace("\u002F", "/"))safe_str = re.sub(r",\s*([}\]])", r"\1", safe_str)try:values = ast.literal_eval("[" + safe_str + "]")except Exception as e:raise ValueError(f"实参解析失败:{str(e)}")self.variable_mapping = {params[i]: values[i] for i in range(min(len(params), len(values)))}print("映射表示例(前10项):", {k: v for i, (k, v) in enumerate(self.variable_mapping.items()) if i < 10})
2.3 方法: extractData(解析与数据提取)
2.3.1 思路:
先通过 “发现 JS 结构差异” 的关键过程调整解析策略:最初误抓榜单分类 JS 文件,后通过搜索 “清华大学” 定位正确 JS,但发现字段名与预期不符(如排名是 ranking 而非 rank,学校名称是 univNameCn 而非 name)。解决思路是:以 univNameCn 为锚点定位所有学校对象,通过自定义 slice_block 函数提取完整对象块,再根据实际字段名提取数据,最后用变量映射表替换代号,得到真实数据。
2.3.2 相关代码块:
def extractData(self, city):print("正在提取学校数据...")school_anchor = "univNameCn" # 修正后真实的学校名称字段(发现结构差异后的关键调整)all_school_objs = []pos = 0while True:pos = self.js_content.find(school_anchor, pos + 1)if pos == -1:break# 提取包含当前学校信息的完整对象块school_obj = self.object_around(self.js_content, pos)# 去重并过滤非学校对象(包含分数字段才保留)if school_obj not in all_school_objs and "score" in school_obj:all_school_objs.append(school_obj)print(f"找到 {len(all_school_objs)} 所学校对象")school_data = []for obj in all_school_objs:# 提取原始字段(使用发现的真实字段名)rank_token = self.read_after_colon(obj, "ranking") # 排名字段:ranking(原预期为rank)name = self.read_after_colon(obj, "univNameCn") # 学校名称字段:univNameCn(原预期为name)province_token = self.read_after_colon(obj, "province") # 省市字段:provincecategory_token = self.read_after_colon(obj, "univCategory") # 类型字段:univCategory(原预期为category)score = self.read_after_colon(obj, "score") # 总分字段:score(直接为数值)# 变量代号替换为真实值rank = self.resolve(rank_token, self.variable_mapping)province = self.resolve(province_token, self.variable_mapping)category = self.resolve(category_token, self.variable_mapping)# 打印前10所学校数据(调试用)if len(school_data) < 10:print(f"调试:排名={rank}, 学校={name}, 省市={province}, 类型={category}, 总分={score}")# 过滤有效数据if rank and name and score and self.is_number(rank) and self.is_number(score):school_data.append({"rank": int(rank),"name": name.strip(),"province": province.strip() if province else "","category": category.strip() if category else "","score": float(score)})# 按排名排序school_data.sort(key=lambda x: x["rank"])return school_data
二、代码与输出结果
爬取目标:2021 软科中国大学排名主榜
代码链接:https://gitee.com/spacetime666/2025_crawl_project/blob/master/作业2/school.py
数据库链接:https://gitee.com/spacetime666/2025_crawl_project/blob/master/作业2/university_rankings_2021_final.db
输出实例

三、心得体会
本次实验的核心挑战在于应对 JS 数据结构的未知差异,整个过程从 “误抓文件” 到 “发现差异” 再到 “针对性破解”,让我深刻体会到爬虫开发中 “动态适配数据结构” 的重要性。软科排名数据采用 Nuxt 框架的 JSONP 格式,既没有暴露直接可调用的 API,还对真实数据进行了变量代号加密,与常规静态网页的爬取逻辑有显著区别。
1. 发现并适配 JS 结构差异是爬取成功的关键:
实验初期曾因抓包不精准,下载到仅包含 “榜单分类名称” 的 JS 文件,导致提取到 0 所学校;后续虽找到包含学校数据的正确 JS,但发现字段名与预期完全不符(如排名字段是 ranking 而非 rank,学校名称是 univNameCn 而非 name)。解决这一问题的核心是 “从数据反向推导结构”:通过搜索 “清华大学” 等已知学校名称,定位其在 JS 中的存储位置,观察周围字段的命名规则,最终确定真实字段名。这让我明白,爬虫开发不能依赖 “预设字段名”,而是要通过实际数据样本分析结构,动态调整解析策略。
2. 变量代号破解是解析加密 JS 数据的核心:
Nuxt 框架将省市、学校类型等数据用 q、f、e 等变量代号替代,直接提取会得到无意义的字符。通过解析 JS 函数的 “形参 - 实参” 映射关系,构建变量对照表,成功将代号转换为 “北京”“综合”“理工” 等真实值。这一过程让我认识到,对于框架生成的加密数据,不能局限于表层解析,需要深入理解 JS 代码的执行逻辑,找到数据解码的 “钥匙”,而函数参数与实参的对应关系往往是破解这类加密的关键。
3. 数据过滤与去重是保障数据质量的最后防线:
爬取过程中发现,JS 文件中虽识别到 582 个学校对象,但部分对象缺少排名或总分数据。通过设置 “排名和总分必须为有效数字” 的过滤条件,最终保留 448 所有效数据;同时在数据库设计中,用 rank 作为主键、name 设为 UNIQUE 约束,双重避免重复数据插入。这让我体会到,原始爬取数据往往存在冗余或残缺,合理的过滤规则和数据库约束,是将 “原始数据” 转化为 “可用数据” 的必要步骤。