Python爬虫实战:从研招网动态页面高效抓取结构化招生信息
1. 项目概述与核心价值最近在整理一些考研相关的资料发现很多同学在查找院校专业信息时还是习惯于手动去研招网一个个点开看效率低不说还容易遗漏。这让我想起了几年前自己备考时为了对比几个心仪学校的招生人数、考试科目和参考书目在浏览器里开了几十个标签页看得头晕眼花。其实这类结构化的公开信息完全可以用Python爬虫来高效获取和整理。今天我就结合“爬取研招网招生信息详情”这个实战项目把从环境搭建、页面分析、数据抓取到数据清洗存储的全流程以及我踩过的坑和总结的技巧系统地分享给大家。这个项目不仅能帮你快速构建一个考研信息数据库更重要的是它能让你掌握处理动态加载、应对反爬策略、解析复杂JSON数据等爬虫实战中的核心技能这些技能在抓取其他类似网站时同样适用。无论你是想为考研助力还是单纯想提升自己的Python爬虫水平这篇内容都会是一个不错的起点。2. 项目整体设计与思路拆解2.1 目标网站分析与技术选型我们的目标是“研招网”即中国研究生招生信息网。这是一个典型的政府类信息服务网站信息权威但页面结构可能比较复杂。首先需要明确我们要爬取什么。招生信息详情通常包括招生单位大学/研究所、院系所、专业代码及名称、研究方向、拟招生人数、考试科目、专业课参考书目等。这些信息分布在不同的页面层级。经过初步探查研招网的信息查询主要有两个入口一是通过“硕士目录”进行条件筛选查询列表二是各高校自己发布的详细招生简章或专业目录页面。前者通常以列表形式呈现概要信息点击后跳转到详情页。考虑到我们要的是“详情”所以策略定为先获取符合条件的专业列表及对应的详情页链接再逐个访问详情页抓取完整信息。技术栈选择上核心是Python。requests库用于发送HTTP请求这是基础。但研招网的列表页和详情页很可能涉及动态加载AJAX直接requests.get()拿到的可能是空壳HTML。因此我们需要用到Selenium或Playwright这样的浏览器自动化工具来模拟真实用户操作获取渲染后的完整页面内容。考虑到Playwright对现代Web技术的支持更好、异步性能更强本项目选择Playwright作为核心抓取工具。数据解析方面虽然动态内容多为JSON格式但详情页的HTML结构解析仍离不开BeautifulSoup或lxml这里选择BeautifulSoup因其API对新手更友好。数据存储则用轻量级的SQLite数据库方便管理和后续查询。注意在爬取任何网站前务必先检查其robots.txt文件通常在网站根目录如https://yz.chsi.com.cn/robots.txt并尊重网站的爬虫协议。对于明确禁止爬取的目录应予以回避。同时需要在请求头中设置合理的User-Agent并控制请求频率避免对目标网站服务器造成压力。2.2 核心难点与应对策略预判根据经验这类网站通常会设置一些反爬机制我们需要提前规划应对策略动态加载与参数加密列表数据很可能通过POST请求一个特定API接口获取请求参数可能经过简单编码或加密。我们需要通过浏览器开发者工具的“网络Network”面板追踪XHR/Fetch请求找到真正的数据接口和参数规律。IP限制与请求频率短时间内发起大量请求可能会导致IP被暂时封锁。解决方案是使用代理IP池并在请求间添加随机延时。对于本项目由于我们不是商业爬虫只需在抓取每个详情页之间设置time.sleep(random.uniform(2, 5))这样的随机等待即可大幅降低风险。登录与会话维持部分详细信息如历年录取分数可能需要登录才能查看。研招网的统一登录有时会涉及滑块验证等复杂交互实现全自动化登录成本较高。本项目暂定只爬取公开的招生目录信息这部分通常无需登录。数据清洗与结构化抓取到的文本数据往往包含多余空格、换行符、特殊字符如\u3000全角空格且不同学校的信息展示格式不统一。这就需要编写健壮的清洗函数使用正则表达式或字符串方法进行规范化处理。我的整体思路是使用Playwright启动一个浏览器实例导航到硕士目录查询页面模拟人工选择查询条件如学科门类、专业类别等然后拦截或等待列表数据接口的响应直接解析JSON数据获得专业列表和详情页ID。接着根据详情页ID构造出每个专业的详情页URL再使用Playwright或requests如果详情页是静态的去抓取详情页HTML用BeautifulSoup解析出所需字段。最后将所有数据清洗后存入SQLite数据库。这个流程清晰地将“获取列表”和“解析详情”解耦便于调试和维护。3. 环境搭建与核心工具详解3.1 Python环境与依赖库安装首先确保你安装了Python3.7及以上版本。我强烈建议使用虚拟环境来管理项目依赖避免污染全局环境。可以使用venv或conda。# 创建项目目录并进入 mkdir yzw_spider cd yzw_spider # 创建虚拟环境以venv为例 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate激活虚拟环境后安装必要的库pip install playwright beautifulsoup4 sqlite-requests lxml # 安装Playwright所需的浏览器内核 playwright install chromium这里解释一下各个库的作用playwright: 用于控制浏览器获取动态渲染的页面内容或拦截API请求。beautifulsoup4: 用于解析HTML文档提取结构化数据。sqlite-requests: 一个轻量级库方便进行SQLite数据库操作。你也可以使用Python标准库sqlite3。lxml: 是BeautifulSoup的一个解析器后端比默认的html.parser速度更快、容错能力更强。requests: 虽然我们用Playwright处理动态页但一些静态的详情页或接口调用用requests更轻量。3.2 Playwright快速上手与配置Playwright的功能非常强大我们这里主要用到它的同步API易于理解和请求拦截功能。from playwright.sync_api import sync_playwright import time import random def init_browser(headlessFalse): 初始化浏览器上下文 headless: 无头模式不显示浏览器界面。调试时可设为False。 p sync_playwright().start() # 选择Chromium内核 browser p.chromium.launch(headlessheadless, slow_mo100) # slow_mo让操作慢下来便于观察 # 创建一个新的浏览器上下文可以独立设置视窗、User-Agent等 context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) page context.new_page() return p, browser, context, page关键配置解析headless: 调试阶段建议设为False这样你能看到浏览器每一步操作确认脚本是否按预期执行。正式运行时可以改为True以节省资源。slow_mo: 单位是毫秒它会在每个Playwright操作如点击、输入后增加一个延迟。这对于观察自动化过程和应对一些基于操作速度的反爬很有用。user_agent: 设置一个常见的桌面浏览器UA模拟真实用户。viewport: 设置浏览器窗口大小有些网站的响应式布局在不同尺寸下元素位置可能不同固定一个常用尺寸可以避免意外。实操心得在编写爬虫脚本时我习惯将浏览器初始化、页面打开、元素定位等操作都封装在try...except块中并配合详细的日志记录。这样当脚本在无人值守运行时出错我能快速定位到是哪个页面、哪个元素出了问题。日志可以记录时间、URL、操作动作和结果成功或异常信息。4. 核心爬取流程实现与解析4.1 步骤一分析数据接口与获取专业列表我们首先手动打开研招网硕士目录页面用浏览器开发者工具分析网络请求。打开开发者工具按F12切换到“网络(Network)”面板勾选“保留日志(Preserve log)”。执行一次查询在页面选择查询条件例如门类类别选择“工学”学科类别选择“计算机科学与技术”点击查询。寻找数据请求在网络请求列表中过滤XHR或Fetch请求。你会看到一些包含“search”或“query”字样的请求。点击查看其“载荷(Payload)”和“响应(Response)”。分析请求通常这是一个POST请求其请求体是一个JSON或表单数据包含了你的查询条件。响应也是一个JSON格式的数据里面包含了专业列表每个专业项里会有school_id、speciality_id、detail_url等关键信息。假设我们分析出的API接口是https://yz.chsi.com.cn/zsml/queryAction.do请求方法为POST参数在Form Data里。那么我们可以用Playwright来拦截这个请求直接获取其响应数据这比解析渲染后的HTML更直接、更稳定。import json from playwright.sync_api import sync_playwright def get_major_list(query_params): 获取专业列表 query_params: 字典包含查询条件如{ssdm: 11, dwmc: , mldm: 08, yjxkdm: 0812} major_list [] def handle_response(response): # 拦截特定的响应 if queryAction.do in response.url and response.request.method POST: try: # 尝试解析JSON响应 json_data response.json() # 根据实际响应结构提取列表这里假设数据在json_data[data][list]里 if json_data.get(data) and json_data[data].get(list): for item in json_data[data][list]: # 提取我们需要的信息例如专业名称、代码、详情页路径 major_info { school_name: item.get(dwmc), major_code: item.get(zydm), major_name: item.get(zymc), detail_path: item.get(detailUrl), # 可能是相对路径 plan_count: item.get(jhrs), # 拟招生人数 } major_list.append(major_info) except Exception as e: print(f解析响应失败: {e}, URL: {response.url}) p, browser, context, page init_browser(headlessFalse) # 添加响应事件监听器 page.on(response, handle_response) # 导航到查询页面 page.goto(https://yz.chsi.com.cn/zsml/querySchAction.do) # 等待页面基本加载完成 page.wait_for_load_state(networkidle) # 模拟输入查询条件这里需要根据实际页面元素调整选择器 # 例如选择学科门类 page.select_option(select[namemldm], valuequery_params.get(mldm, )) # 选择专业领域 page.fill(input[nameyjxkdm], query_params.get(yjxkdm, )) # 点击查询按钮 page.click(input[typesubmit]) # 按钮选择器需根据实际情况调整 # 等待一段时间确保响应被拦截到 page.wait_for_timeout(5000) # 关闭浏览器 context.close() browser.close() p.stop() return major_list # 示例查询参数需要根据实际页面调整 params { mldm: 08, # 门类代码08代表工学 yjxkdm: 0812, # 学科专业代码0812代表计算机科学与技术 } majors get_major_list(params) print(f共获取到 {len(majors)} 个专业信息)关键点解析page.on(response, handle_response): 这是Playwright的核心功能之一可以为页面设置事件监听器。这里监听所有响应当遇到目标URL的响应时触发处理函数。response.json(): 直接解析响应的JSON体比从HTML里抠数据高效准确得多。选择器Selectorpage.select_option、page.fill、page.click这些操作都需要准确的目标元素选择器。你必须通过开发者工具的“元素Elements”面板找到对应下拉框、输入框、按钮的CSS选择器或XPath。这是编写自动化脚本最耗时但也最关键的一步。选择器要尽量稳定优先使用id、name属性其次是用包含特定类或属性的CSS选择器。4.2 步骤二构造详情页链接与访问策略获取到专业列表后每个专业对象中应该包含一个访问详情页的关键信息比如detail_path可能是像/zsml/querySchAction.do?ssdm11dwmc某某大学...这样的相对路径或参数集合。我们需要构造出完整的详情页URL。研招网的详情页URL通常也是有一个固定的模式例如https://yz.chsi.com.cn/zsml/querySchAction.do?methodviewdwmcXXXzydmYYY。import urllib.parse BASE_URL https://yz.chsi.com.cn def construct_detail_url(major_info): 根据专业信息构造详情页完整URL # 情况1: detail_path是相对路径 if major_info.get(detail_path): if major_info[detail_path].startswith(http): return major_info[detail_path] else: return urllib.parse.urljoin(BASE_URL, major_info[detail_path]) # 情况2: 需要自己拼接参数 (更常见) else: # 假设我们从列表接口拿到了学校代码(school_id)和专业代码(major_code) params { method: view, dwmc: major_info.get(school_name), zydm: major_info.get(major_code), # 可能还需要其他参数如ssdm(省市代码)、yjxkdm等 } # 过滤掉空值参数 valid_params {k: v for k, v in params.items() if v} query_string urllib.parse.urlencode(valid_params, encodinggb2312) # 注意编码研招网常用gb2312 return f{BASE_URL}/zsml/querySchAction.do?{query_string}编码问题注意在构造URL时特别是中文字符作为参数时如dwmc北京大学必须注意编码。研招网通常使用GB2312或GBK编码而不是UTF-8。使用urllib.parse.urlencode时指定encodinggb2312可以避免乱码问题。这是一个非常容易踩坑的地方如果编码不对服务器可能无法识别参数返回错误页面或空数据。4.3 步骤三解析详情页HTML与数据提取详情页的HTML结构相对复杂信息以文本形式散布在多个tr、td标签中。我们需要仔细分析其结构编写针对性的解析代码。首先我们使用Playwright或requests获取详情页HTML。由于详情页可能内容较多但通常是静态的为了效率我们可以用requests来获取但需要携带必要的请求头如Referer有时服务器会校验。import requests from bs4 import BeautifulSoup def fetch_detail_page_html(url): 获取详情页HTML内容 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://yz.chsi.com.cn/zsml/querySchAction.do, # 重要模拟从查询页跳转过来 Accept-Language: zh-CN,zh;q0.9, } try: # 注意如果URL参数含中文且已正确编码requests会自动处理 resp requests.get(url, headersheaders, timeout10) resp.encoding gb2312 # 关键设置响应的正确编码 if resp.status_code 200: return resp.text else: print(f请求详情页失败状态码: {resp.status_code}, URL: {url}) return None except Exception as e: print(f请求详情页异常: {e}, URL: {url}) return None def parse_detail_html(html, major_base_info): 解析详情页HTML提取详细信息 major_base_info: 从列表页获取的基础信息字典 if not html: return None soup BeautifulSoup(html, lxml) detail_data major_base_info.copy() # 先复制基础信息 # 研招网详情页信息通常在一个id为result或content的div下的表格里 # 我们需要找到包含“招生人数”、“考试科目”、“参考书目”等关键字的表格行(tr) info_table soup.find(table, class_zsml-result) # 类名需根据实际情况调整 if not info_table: # 如果没找到特定类尝试找包含信息的第一个大table info_table soup.find(table, {width: 100%, border: 0}) if info_table: rows info_table.find_all(tr) for row in rows: th row.find(th) td row.find(td) if th and td: header_text th.get_text(stripTrue) content_text td.get_text( , stripTrue) # 用空格连接多个文本段 # 根据表头关键词映射到我们的字段 if 招生人数 in header_text: detail_data[plan_count_detail] content_text # 详情页的招生人数可能更细 elif 考试科目 in header_text: detail_data[exam_subjects] content_text elif 参考书目 in header_text or 参考教材 in header_text: detail_data[reference_books] content_text elif 研究方向 in header_text: detail_data[research_direction] content_text elif 指导教师 in header_text: detail_data[supervisors] content_text # ... 可以根据需要添加更多字段 else: print(未找到详情信息表格) # 补充有时信息也在ulli列表中或者分散在多个div里需要更复杂的解析逻辑 # 这里可以添加备用解析方案 backup_info soup.find(div, class_zsml-bxx) if backup_info: # 对backup_info进行进一步解析... pass return detail_data解析策略详解定位核心容器首先找到包含所有信息的父容器通常是一个有特定id或class的table或div。你需要手动打开几个不同学校的详情页观察其HTML结构的共性。遍历行与单元格在表格结构中信息通常以tr行为单位每行包含th表头和td内容。我们遍历所有行提取表头文本和内容文本。关键词匹配由于不同学校、不同年份的页面模板可能有细微差别表头文字可能不完全一致如“招生人数”和“拟招生人数”。我们的匹配逻辑要具有一定的容错性使用in操作符进行子串匹配。文本清洗使用.get_text(stripTrue)可以移除文本前后的空白字符。对于内容单元格td里面可能包含br标签get_text( , stripTrue)中的 参数表示用空格替换标签这样能保留基本的段落分隔。备用方案如果首选定位方式失败必须有备用的定位逻辑。例如查找所有包含特定文本的标签。这能提高爬虫的健壮性。4.4 步骤四数据清洗、存储与去重抓取到的原始数据是杂乱的文本我们需要清洗并结构化存储。数据清洗import re def clean_data(detail_data): 清洗和规范化提取的数据 cleaned {} for key, value in detail_data.items(): if isinstance(value, str): # 1. 去除首尾空白 value value.strip() # 2. 替换全角空格、多个空格为单个空格 value re.sub(r[\u3000\s], , value) # 3. 处理换行符统一为分号或其他分隔符针对参考书目等 if key in [reference_books, exam_subjects, research_direction]: # 将连续的换行、分号等统一为中文分号“” value re.sub(r[\n\r;], , value) # 去除首尾可能多余的分号 value value.strip() cleaned[key] value return cleaned数据存储SQLiteimport sqlite3 from datetime import datetime def init_database(db_pathyzw_majors.db): 初始化数据库和表 conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS majors ( id INTEGER PRIMARY KEY AUTOINCREMENT, school_name TEXT, major_code TEXT, major_name TEXT, plan_count TEXT, plan_count_detail TEXT, exam_subjects TEXT, reference_books TEXT, research_direction TEXT, supervisors TEXT, detail_url TEXT UNIQUE, -- 唯一约束用于去重 crawl_time TIMESTAMP, UNIQUE(school_name, major_code, major_name) -- 联合唯一键防止重复插入 ) ) conn.commit() return conn def save_to_database(conn, cleaned_data): 将清洗后的数据存入数据库 cursor conn.cursor() # 准备插入数据如果冲突根据UNIQUE约束则忽略或更新 sql INSERT OR REPLACE INTO majors (school_name, major_code, major_name, plan_count, plan_count_detail, exam_subjects, reference_books, research_direction, supervisors, detail_url, crawl_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) values ( cleaned_data.get(school_name), cleaned_data.get(major_code), cleaned_data.get(major_name), cleaned_data.get(plan_count), cleaned_data.get(plan_count_detail), cleaned_data.get(exam_subjects), cleaned_data.get(reference_books), cleaned_data.get(research_direction), cleaned_data.get(supervisors), cleaned_data.get(detail_url), datetime.now() ) try: cursor.execute(sql, values) conn.commit() print(f成功保存/更新: {cleaned_data.get(school_name)} - {cleaned_data.get(major_name)}) except sqlite3.Error as e: print(f数据库操作失败: {e})去重策略 在数据库表设计时我们设置了两个唯一性约束detail_url UNIQUE详情页URL是唯一的这是最直接的去重依据。UNIQUE(school_name, major_code, major_name)学校、专业代码、专业名称的组合也应该是唯一的作为双重保障。使用INSERT OR REPLACE语句当遇到冲突时会用新数据替换旧数据。这对于更新已爬取专业的最新信息很有用。5. 完整脚本组装与流程控制将上述所有函数组装起来并加入流程控制、错误处理和日志记录。import time import random import logging from tqdm import tqdm # 进度条库可选 pip install tqdm # 配置日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(yzw_crawler.log), logging.StreamHandler() ] ) logger logging.getLogger(__name__) def main(): 主函数控制整个爬取流程 # 1. 初始化数据库 conn init_database() logger.info(数据库初始化完成。) # 2. 设置查询条件这里以计算机专业为例可以循环多种条件 query_params_list [ {mldm: 08, yjxkdm: 0812}, # 工学-计算机科学与技术 # {mldm: 07, yjxkdm: 0701}, # 理学-数学 # ... 添加其他学科 ] all_majors_crawled 0 for query_params in query_params_list: logger.info(f开始爬取查询条件: {query_params}) # 3. 获取专业列表 logger.info(正在获取专业列表...) major_list get_major_list(query_params) if not major_list: logger.warning(f未获取到查询条件 {query_params} 下的专业列表跳过。) continue logger.info(f获取到 {len(major_list)} 个专业。) # 4. 遍历专业列表抓取详情 for idx, major_base in enumerate(tqdm(major_list, descf爬取详情页)): # 构造详情页URL detail_url construct_detail_url(major_base) if not detail_url: logger.warning(f无法为专业 {major_base} 构造URL跳过。) continue # 将URL存入基础信息便于后续存储 major_base[detail_url] detail_url # 5. 获取并解析详情页 logger.debug(f正在处理第 {idx1}/{len(major_list)} 个: {detail_url}) html fetch_detail_page_html(detail_url) if html: detail_data parse_detail_html(html, major_base) if detail_data: # 6. 数据清洗 cleaned_data clean_data(detail_data) # 7. 存入数据库 save_to_database(conn, cleaned_data) all_majors_crawled 1 else: logger.error(f解析详情页失败: {detail_url}) else: logger.error(f获取详情页失败: {detail_url}) # 8. 礼貌性延迟避免请求过快 delay random.uniform(3, 8) # 随机等待3-8秒 time.sleep(delay) # 9. 关闭数据库连接 conn.close() logger.info(f爬取任务结束。共处理 {all_majors_crawled} 个专业详情。) if __name__ __main__: main()流程控制要点分步查询研招网一次查询返回的专业可能成百上千我们可以通过遍历不同的学科门类(mldm)和一级学科(yjxkdm)代码来分批次抓取这样既符合网站查询逻辑也便于管理。进度反馈使用tqdm库或简单的print语句显示进度对于长时间运行的脚本非常必要。错误隔离每个专业详情页的抓取和解析都应该是独立的一个页面失败不应影响其他页面。因此在循环内部进行了充分的try...except捕获和日志记录。随机延迟time.sleep(random.uniform(3, 8))是简单有效的反反爬策略。更复杂的策略可以引入代理IP但本项目数据量不大随机延迟通常足够。6. 常见问题排查与实战技巧6.1 请求被拒绝或返回空数据症状requests.get()返回403/404或者Playwright页面内容为空。排查检查请求头特别是User-Agent和Referer。有些网站会校验Referer必须设置为来自其站内的页面。用浏览器正常访问一次从开发者工具里复制完整的请求头信息。检查Cookies某些页面需要先访问首页获取一个会话Cookie。可以用requests.Session()来保持会话或者用Playwright的context它会自动管理Cookies。检查参数编码确认POST请求的参数或GET请求的查询字符串编码是否正确。对于中文参数尝试gb2312或gbk编码。模拟更真实的操作如果使用Playwright确保在关键操作如点击查询前使用page.wait_for_selector等待相关元素加载完成而不是固定等待时间。6.2 解析不到数据或数据错位症状BeautifulSoup找不到预期的标签或者提取的文本驴唇不对马嘴。排查确认HTML结构将抓取到的HTML保存到本地文件用浏览器打开对比与在线页面的差异。可能是网站有反爬机制对自动化工具返回了不同的内容如返回一个验证页面。使用更稳健的选择器不要依赖绝对的位置或索引。使用包含特征属性如class、id的选择器。如果类名是动态生成的例如classtable-abc123可以尝试用XPath的contains函数进行部分匹配如soup.find(table, class_re.compile(^zsml-))。打印中间结果在解析函数中打印出找到的soup对象或关键标签确认你定位到了正确的元素。考虑页面有iframe有些信息可能嵌套在iframe里。你需要先用soup.find(iframe)找到iframe然后获取其src属性再单独请求那个URL。6.3 数据库写入错误或重复数据症状sqlite3.IntegrityError: UNIQUE constraint failed排查明确去重逻辑检查你的UNIQUE约束字段。是detail_url唯一还是(school_name, major_code)组合唯一要根据业务逻辑确定。使用UPSERT操作正如我们用的INSERT OR REPLACE它会在冲突时替换旧行。如果你只想忽略重复项可以用INSERT OR IGNORE。爬取前先查询对于大规模爬取可以在插入前先执行一次SELECT查询判断是否存在但这会增加数据库IO。对于中小规模数据直接使用INSERT OR REPLACE更简单高效。6.4 爬虫被封锁或访问变慢症状请求超时增多返回验证码页面或直接无法连接。应对策略降低请求频率这是首要措施。将随机延迟时间加大例如random.uniform(5, 15)。使用代理IP搭建或购买代理IP池在请求时随机切换。对于requests可以传入proxies参数对于Playwright可以在创建browser或context时配置代理。模拟人类行为在Playwright中可以随机移动鼠标page.mouse.move(x, y)随机滚动页面page.evaluate(window.scrollBy(0, Math.random() * 500))。设置超时与重试为网络请求设置合理的超时时间并实现重试机制如最多重试3次每次重试前等待更长时间。6.5 实战技巧与心得从简单到复杂不要一开始就想爬取所有细节。先确保能稳定获取到专业列表和详情页链接再逐步完善详情页的解析逻辑。可以先将解析出的原始HTML片段保存下来离线编写和测试解析代码。数据抓取与数据清洗分离建议将原始HTML或JSON响应保存到文件或数据库的“原始数据”字段中。这样即使后续解析逻辑需要调整也无需重新爬取直接从原始数据中重新解析即可。使用配置文件将查询参数、请求头、数据库路径、延迟时间等配置项写入一个config.py或config.json文件使主脚本更清晰也便于修改。定时增量爬取招生信息每年更新。可以编写一个脚本定期如每周一次运行只爬取crawl_time在最近一段时间内更新过的专业或者通过对比新旧数据来发现更新。尊重robots.txt再次强调检查并遵守目标网站的爬虫协议。对于Disallow的路径不要爬取。将爬虫的User-Agent设置为一个易于识别的名称如MyResearchSpider/1.0并在请求头中附带一个联系邮箱以示友好。这个项目从分析到实现涵盖了Python爬虫实战中大部分核心环节。过程中最考验人的不是编码而是耐心地分析网站结构、处理各种反爬策略和数据异常。每完成一个这样的项目你对网络请求、数据解析和流程控制的掌握就会更深一层。