DrissionPage实战:高效稳定爬取拉勾网招聘数据
1. 项目概述与核心思路
最近在帮一个做招聘分析的朋友处理数据,他需要定期获取拉勾网上特定技术岗位的招聘信息来做趋势分析。传统的爬虫方案,比如requests+BeautifulSoup或者Selenium,要么容易被反爬机制拦截,要么运行效率低下、资源消耗大。在尝试了多种方案后,我最终选择了DrissionPage这个库,它完美地解决了“既要模拟浏览器行为绕过复杂验证,又要保持高效率和低资源占用”的矛盾。今天,我就把这个从环境搭建、核心代码编写到数据清洗存储的完整实战过程,以及我踩过的几个“坑”和优化技巧,详细地分享出来。
简单来说,DrissionPage是一个将浏览器自动化(如Selenium)和网络请求(如requests)能力融合在一起的Python库。它最大的魅力在于,你可以用类似requests的简洁语法直接发送HTTP请求获取数据,但当遇到需要执行JavaScript、处理动态加载内容或绕过一些前端检测时,又能无缝切换到“浏览器模式”,用真实的浏览器环境来渲染页面。对于拉勾网这种前端技术栈复杂、反爬措施较多的网站,这种“双模式”策略非常有效。我们的目标不仅仅是“能爬下来”,更是要“稳定、高效、可维护地爬下来”。
2. DrissionPage核心优势与Selenium/Requests对比
在深入代码之前,我们有必要搞清楚为什么选择DrissionPage,而不是更广为人知的Selenium或纯requests方案。这决定了我们后续编码的底层逻辑和应对策略。
2.1 传统方案的痛点
我最初尝试用requests直接模拟请求。拉勾网的数据接口通常是XHR(Ajax)请求,通过浏览器开发者工具的“网络(Network)”面板可以找到。但问题很快出现了:首先,请求头(Headers)需要模拟得非常精确,特别是User-Agent、Referer和一些自定义的签名参数(如x-anti-request-token),这些参数往往由前端JavaScript动态生成,静态分析困难。其次,拉勾网对频繁的、规律性的请求非常敏感,很容易触发IP限制或要求验证码,单纯靠设置请求间隔和代理IP,维护成本很高。
于是转向Selenium。它能完美地渲染页面,执行所有JS,拿到最终呈现的HTML。但它的缺点同样明显:速度慢。每个页面加载都需要启动浏览器内核、下载资源、渲染DOM,爬取几十页数据就可能耗时几分钟。资源占用高,一个浏览器实例就会占用几百MB内存。稳定性依赖浏览器驱动,Chrome版本一升级,对应的chromedriver可能就失效了,需要手动更新匹配。
2.2 DrissionPage的融合之道
DrissionPage的设计哲学是“用正确的方式做正确的事”。它内部维护了两个核心对象:Session对象(类似requests.Session)和Page对象(类似Selenium的WebDriver)。
- Session模式(混合模式):这是默认且推荐的模式。在此模式下,
DrissionPage会先用Session对象发送HTTP请求获取初始HTML。如果发现页面内容是通过JS动态加载的(比如拉勾网的职位列表),它会自动分析网络请求,找到真正的数据接口(通常是JSON格式),然后继续用Session去请求这个接口,直接拿到结构化数据。这个过程绕过了不必要的页面渲染,速度极快。 - Page模式(浏览器模式):当遇到Session模式无法解决的问题时,比如需要处理复杂的登录状态、滑动验证码,或者页面结构过于复杂难以解析接口,可以一键切换到Page模式。此时它会启动一个后台浏览器(默认基于Chromium),完全模拟用户操作。你可以在两种模式间无缝切换,共享cookies和会话状态。
对于拉勾网,我们的策略是:主要使用Session模式直接请求数据接口,仅在首次可能需要处理登录或验证时,短暂使用Page模式。这样既保证了速度,又具备了应对复杂情况的能力。
注意:直接爬取公开数据需遵守网站的
robots.txt协议,并合理设置请求间隔(如每次请求后time.sleep(random.uniform(2, 5))),避免对目标服务器造成压力。本教程仅用于技术学习交流。
3. 环境准备与DrissionPage安装配置
工欲善其事,必先利其器。稳定的环境是成功的一半。
3.1 创建独立的Python虚拟环境
我强烈建议为每个爬虫项目创建独立的虚拟环境。这可以避免不同项目间的库版本冲突。使用venv(Python 3.3+内置)非常简单:
# 在项目目录下,创建名为 `venv` 的虚拟环境 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate激活后,命令行提示符前会出现(venv)字样。
3.2 安装DrissionPage及相关库
在激活的虚拟环境中,使用pip安装:
pip install DrissionPage pandasDrissionPage: 核心库。pandas: 用于将爬取的数据规整地保存为CSV或Excel文件,进行初步清洗和分析非常方便。
安装过程会自动处理DrissionPage的依赖,包括它所需的浏览器内核(默认会下载一个便携版的Chromium,无需单独安装Chrome和驱动)。第一次运行代码时如果本地没有,它会自动下载,你也可以通过配置指定已安装的Chrome路径。
3.3 初始化DrissionPage并配置基础参数
新建一个Python文件,比如lagou_spider.py,开始编写代码。首先进行基础配置:
from DrissionPage import SessionPage, ChromiumPage import pandas as pd import time import random import logging # 配置日志,方便调试和记录运行状态 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # 初始化一个SessionPage对象,这是我们工作的主力 session_page = SessionPage() # 设置请求头,模拟真实浏览器 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.lagou.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', } session_page.headers.update(headers) # 设置全局请求超时和重试 session_page.set.timeout(10) # 请求超时10秒 session_page.set.retry(2) # 失败重试2次 session_page.set.interval(2, 5) # 设置随机请求间隔为2-5秒,这是礼貌爬虫的关键配置详解与心得:
SessionPagevsChromiumPage: 我们主要用SessionPage,因为它轻量。ChromiumPage是备用的“重型武器”。- 请求头(Headers):
User-Agent是必须的。Referer(来源页)对于拉勾这类网站很重要,很多接口会校验它。直接复制你浏览器中正常访问时的Headers是最稳妥的。 set.interval(): 这个功能太实用了。它会在每次网络请求后自动休眠一个随机时间(这里2-5秒),模拟人类操作的不规律性,极大降低被封IP的风险。比自己在代码里写time.sleep()更优雅、更统一。- 日志(Logging): 爬虫运行时,你肯定不想只盯着控制台看。配置好日志,所有信息(INFO)、警告(WARNING)、错误(ERROR)都会记录到文件,出问题时回溯时间线非常方便。
4. 拉勾网页面分析与核心爬取策略
接下来是核心环节:分析拉勾网,找到数据入口和规律。
4.1 手动分析数据流
- 打开拉勾网职位搜索页: 在浏览器中访问
https://www.lagou.com/,搜索“Python”,进入列表页。 - 打开开发者工具: 按F12,切换到“网络(Network)”面板。刷新页面,在筛选器中选择“XHR”或“Fetch”。
- 寻找数据接口: 滚动页面加载更多职位时,你会看到名为
positionAjax.json或类似名称的请求。点击它,查看“标头(Headers)”和“响应(Response)”。- 请求URL: 通常是
https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false。注意,它可能是一个POST请求,而不是GET。 - 请求参数: 在“负载(Payload)”或“查询参数(Query String Parameters)”中,你会看到像
first(是否首页)、pn(页码)、kd(关键词)等参数。 - 响应数据: 在“响应(Response)”标签页里,是结构清晰的JSON数据,包含了职位列表(
content->positionResult->result)和总页数等信息。我们的目标就是直接请求这个接口,解析这个JSON。
- 请求URL: 通常是
4.2 构造请求参数与处理反爬
拉勾网的接口通常需要携带一些特定的参数和请求头。以下是我分析出的一个较稳定的参数构造方式:
def construct_request_params(keyword='Python', city='全国', page=1): """构造请求拉勾网Ajax接口的参数""" params = { 'city': city, 'needAddtionalResult': 'false', # 关键参数 'first': 'true' if page == 1 else 'false', # 第一页为true,后续为false 'pn': str(page), # 页码 'kd': keyword, # 搜索关键词 } # 对于POST请求,这些参数通常放在data里。但观察发现拉勾网当前接口是GET带查询参数。 # 我们需要根据实际抓包情况调整。这里以GET为例。 return params # 此外,接口可能还需要一个特定的Referer,格式为:f'https://www.lagou.com/jobs/list_{keyword}?city={city}' def get_referer(keyword, city): return f'https://www.lagou.com/jobs/list_{keyword}?city={city}'关键点与避坑指南:
first参数: 这是最容易出错的地方。爬取第一页时,first必须为true,服务器会返回一些初始化的上下文信息。从第二页开始,必须为false,否则可能返回空数据或错误。很多爬虫卡在只能爬第一页,问题就出在这里。- 动态Token: 拉勾网的请求头里可能包含一个
x-antispam-token或类似的令牌,由前端JS生成,每次请求都变化。如果遇到403错误,可能就是它在作祟。应对策略有两种:- 用Page模式先获取一次: 用
ChromiumPage打开一次页面,让浏览器环境执行JS生成Token,然后从页面里提取出来,再交给SessionPage使用。DrissionPage可以方便地在两种模式间共享cookies和部分上下文。 - 逆向JS: 难度较大,但一劳永逸。需要分析前端JS代码,找到生成Token的算法并用Python复现。对于学习而言,第一种方案更可行。
- 用Page模式先获取一次: 用
- Cookie处理:
DrissionPage的SessionPage对象会自动管理Cookie。但有时初始Cookie是必要的。你可以先用浏览器正常访问一次拉勾网,然后把Cookie字符串复制出来,通过session_page.cookies.add()方法手动设置。
5. 核心爬取函数实现与数据解析
掌握了接口规律,我们就可以编写核心的爬取函数了。
5.1 单页数据爬取函数
def fetch_one_page(session_page, keyword, city, page_num): """爬取单页职位数据""" url = 'https://www.lagou.com/jobs/positionAjax.json' # 1. 构造请求参数和头信息 params = construct_request_params(keyword, city, page_num) referer = get_referer(keyword, city) session_page.headers['Referer'] = referer # 动态更新Referer logger.info(f"正在爬取: 关键词『{keyword}』, 城市『{city}』, 第{page_num}页") try: # 2. 发送GET请求(根据实际情况可能是POST) # 如果是POST,使用:session_page.post(url, data=params) resp_page = session_page.get(url, params=params) # 3. 检查请求是否成功 if resp_page.status_code != 200: logger.error(f"请求失败,状态码: {resp_page.status_code}") return None # 4. 解析JSON响应 # DrissionPage的响应对象可以直接.json()解析 json_data = resp_page.json() # 5. 检查拉勾网返回的业务状态码 # 拉勾网的成功状态码通常是0或1000,具体看接口返回 if json_data.get('code') not in [0, 1000]: logger.warning(f"接口返回错误: {json_data.get('msg')}") # 可能是反爬触发,这里可以加入重试或切换模式的逻辑 return None # 6. 提取职位列表 position_list = json_data.get('content', {}).get('positionResult', {}).get('result', []) total_count = json_data.get('content', {}).get('positionResult', {}).get('totalCount', 0) logger.info(f"第{page_num}页爬取成功,获取到{len(position_list)}条职位,总计{total_count}条") return position_list, total_count except Exception as e: logger.exception(f"爬取第{page_num}页时发生异常: {e}") return None代码解析与技巧:
resp_page.json():DrissionPage的响应对象和requests库的响应对象API高度相似,直接调用.json()方法即可将响应内容解析为Python字典,非常方便。- 异常处理: 网络请求充满不确定性,必须用
try...except包裹。日志记录异常详情(logger.exception会打印堆栈跟踪),便于调试。 - 状态码判断: 除了HTTP状态码200,还要判断拉勾网自定义的业务码(如
code)。非成功码通常意味着参数错误、频率过高或触发了反爬。
5.2 多页爬取与循环控制
单页爬取完成后,我们需要一个循环来爬取所有页。
def fetch_all_pages(session_page, keyword='Python', city='全国', max_pages=10): """爬取多页数据,直到达到最大页数或没有更多数据""" all_positions = [] current_page = 1 total_count = 0 while current_page <= max_pages: # 调用单页爬取函数 result = fetch_one_page(session_page, keyword, city, current_page) if result is None: # 如果爬取失败,等待稍长时间后重试一次 logger.warning(f"第{current_page}页爬取失败,等待10秒后重试...") time.sleep(10) result = fetch_one_page(session_page, keyword, city, current_page) if result is None: logger.error(f"第{current_page}页重试后仍失败,可能触发反爬,停止爬取。") break page_positions, page_total = result # 如果是第一页,更新总职位数,并计算实际最大页数(每页通常15条) if current_page == 1 and page_total > 0: total_count = page_total # 拉勾网每页固定显示15条数据 actual_max_pages = min(max_pages, (page_total + 14) // 15) logger.info(f"预计总页数: {actual_max_pages} (基于总数{page_total})") max_pages = actual_max_pages # 用实际计算出的页数覆盖输入值 # 将当前页数据添加到总列表 if page_positions: all_positions.extend(page_positions) # 如果当前页返回的数据条数不足15(非第一页),通常意味着已经是最后一页 if current_page > 1 and len(page_positions) < 15: logger.info(f"第{current_page}页数据不足15条({len(page_positions)}条),视为最后一页,停止爬取。") break # 翻页,并等待随机时间间隔(已在session_page.set.interval中设置,这里可额外加一点) current_page += 1 time.sleep(random.uniform(1, 3)) # 页间额外等待 logger.info(f"爬取结束。共爬取{len(all_positions)}条职位信息。") return all_positions循环控制逻辑精讲:
- 页数计算: 从第一页的响应中获取
totalCount(总职位数),拉勾网每页固定15条,由此计算出真实的总页数,避免无意义的空请求。 - 终止条件: 两个关键终止条件:a) 达到设定的最大页数
max_pages;b)当前页(非第一页)返回的职位数少于15条。这是一个非常可靠的、判断是否到达末页的启发式规则。 - 失败重试: 单页失败后,等待更长时间(10秒)再重试一次。如果还失败,则可能IP被临时封禁,应停止爬取,避免进一步恶化。
- 节奏控制: 虽然
session_page.set.interval控制了请求间隔,但在翻页这个更宏观的层面,再增加一个time.sleep(random.uniform(1, 3)),让爬虫行为更“人性化”。
6. 数据清洗、解析与存储
爬取到的原始JSON数据包含很多字段,我们需要提取关键信息并保存。
6.1 定义数据解析函数
def parse_position_info(position_dict): """从单条职位JSON数据中解析出我们需要的信息""" # 使用字典的get方法安全获取值,避免KeyError info = { '职位ID': position_dict.get('positionId'), '职位名称': position_dict.get('positionName'), '公司名称': position_dict.get('companyFullName'), '公司规模': position_dict.get('companySize'), '融资阶段': position_dict.get('financeStage'), '所在城市': position_dict.get('city'), '工作区域': position_dict.get('district'), '薪资范围': position_dict.get('salary'), '工作经验': position_dict.get('workYear'), '学历要求': position_dict.get('education'), '职位标签': ', '.join(position_dict.get('positionLables', [])) if position_dict.get('positionLables') else '', '技能标签': ', '.join(position_dict.get('skillLables', [])) if position_dict.get('skillLables') else '', '职位优势': position_dict.get('positionAdvantage', '').replace('\n', ' '), # 替换换行符 '发布时间': position_dict.get('createTime'), '最后更新时间': position_dict.get('updateTime'), '经度': position_dict.get('longitude'), '纬度': position_dict.get('latitude'), '业务类型': position_dict.get('industryField'), '第一类别': position_dict.get('firstType'), '第二类别': position_dict.get('secondType'), '职位详情页URL': f"https://www.lagou.com/jobs/{position_dict.get('positionId')}.html" if position_dict.get('positionId') else '', } return info字段处理技巧:
- 安全访问: 一律使用
.get(‘key’, default_value)方法。原始数据中某些字段可能缺失,直接使用[‘key’]会引发程序崩溃。 - 列表转字符串: 像
positionLables(职位标签)和skillLables(技能标签)是列表,我们用‘, ‘.join(…)将其合并为一个用逗号分隔的字符串,方便后续存入CSV。 - 构造URL: 职位详情页的URL有固定格式,我们可以用职位ID拼出来,无需从JSON里找。
- 清洗文本: 对于
positionAdvantage(职位优势)这类可能包含换行符\n的文本字段,将其替换为空格,避免破坏CSV文件的结构。
6.2 使用Pandas进行数据整合与存储
def save_to_csv(data_list, filename='lagou_positions.csv'): """将解析后的数据列表保存为CSV文件""" if not data_list: logger.warning("没有数据可保存!") return # 1. 将字典列表转换为Pandas DataFrame df = pd.DataFrame(data_list) # 2. 数据清洗(示例:处理空值、去重) # 删除所有字段都为空的记录(理论上不会有) df.dropna(how='all', inplace=True) # 根据职位ID去重,保留最新的一条(按发布时间排序) df['发布时间'] = pd.to_datetime(df['发布时间'], errors='coerce') df.sort_values('发布时间', ascending=False, inplace=True) df.drop_duplicates(subset=['职位ID'], keep='first', inplace=True) # 3. 调整列的顺序(可选,让CSV更易读) columns_order = ['职位名称', '公司名称', '薪资范围', '所在城市', '工作经验', '学历要求', '发布时间', '职位标签', '技能标签', '职位详情页URL'] # 只保留我们调整顺序的列,其他列按原顺序附在后面 existing_columns = [col for col in columns_order if col in df.columns] other_columns = [col for col in df.columns if col not in existing_columns] final_columns = existing_columns + other_columns df = df[final_columns] # 4. 保存到CSV文件 df.to_csv(filename, index=False, encoding='utf-8-sig') # 使用utf-8-sig编码解决Excel打开中文乱码问题 logger.info(f"数据已保存至 {filename}, 共 {len(df)} 条记录。") print(df.head()) # 打印前几行预览 return dfPandas操作心得:
utf-8-sig编码: 这是解决用Excel打开CSV文件时中文乱码的“银弹”。utf-8-sig会在文件开头添加一个BOM头,Excel就能正确识别编码。- 数据去重: 爬虫运行多次或翻页时可能有重复数据。根据
职位ID去重是最可靠的。先按发布时间排序(ascending=False降序,最新的在前),再drop_duplicates并keep=’first’,就能确保保留的是最新的一条记录。 - 列顺序调整: 把最重要的信息(职位、公司、薪资)放在CSV文件的前几列,方便直接查看。这是一个提升数据可用性的小细节。
7. 主函数与完整流程串联
最后,我们将所有模块组合起来,形成一个完整的、可执行的脚本。
def main(): """主函数,串联整个爬取流程""" # 初始化Session session = SessionPage() session.headers.update({ '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', }) session.set.timeout(10) session.set.retry(2) session.set.interval(2, 5) # 核心:设置自动随机延迟 # 配置爬取参数 search_keyword = 'Python' search_city = '上海' max_pages_to_crawl = 5 # 首次测试可先爬5页 logger.info(f"开始爬取拉勾网职位信息,关键词: {search_keyword}, 城市: {search_city}") # 执行多页爬取 raw_position_list = fetch_all_pages(session, search_keyword, search_city, max_pages_to_crawl) if not raw_position_list: logger.error("未能爬取到任何数据,程序退出。") return # 解析每一条职位数据 parsed_data_list = [] for pos in raw_position_list: parsed_info = parse_position_info(pos) parsed_data_list.append(parsed_info) logger.info(f"共解析了 {len(parsed_data_list)} 条职位信息。") # 保存数据 filename = f'lagou_{search_keyword}_{search_city}_{time.strftime("%Y%m%d_%H%M%S")}.csv' save_to_csv(parsed_data_list, filename) # 可选:简单的数据摘要 if parsed_data_list: df_summary = pd.DataFrame(parsed_data_list) print("\n=== 数据摘要 ===") print(f"城市分布:\n{df_summary['所在城市'].value_counts().head()}") print(f"\n薪资范围分布:\n{df_summary['薪资范围'].value_counts().head()}") print(f"\n热门技能标签(前10):") # 将所有技能标签字符串合并再分割,统计词频 all_skills = ' '.join(df_summary['技能标签'].dropna().astype(str)).split(', ') from collections import Counter skill_counter = Counter([s.strip() for s in all_skills if s.strip()]) for skill, count in skill_counter.most_common(10): print(f" {skill}: {count}次") if __name__ == '__main__': main()完整流程要点:
- 模块化: 每个函数职责单一(如获取、解析、保存),主函数
main()清晰地将它们串联起来。这样的代码易于调试、维护和复用。 - 文件命名: 保存的CSV文件名包含了关键词、城市和时间戳(如
lagou_Python_上海_20231027_143022.csv),这样多次运行也不会覆盖旧文件,便于数据管理和归档。 - 即时反馈: 在控制台打印简单的数据摘要(城市分布、薪资分布、热门技能),让你在爬取完成后立刻对数据有个直观的了解,而无需马上打开CSV文件。
8. 高级技巧、常见问题与排查实录
在实际操作中,你几乎一定会遇到下面这些问题。这里是我总结的应对方案。
8.1 应对反爬策略升级
拉勾网的反爬机制不是一成不变的。如果你的爬虫突然失效,可以按以下步骤排查和升级:
请求返回
403或400错误,数据为空:- 检查点1:请求头。用浏览器无痕模式访问拉勾网,复制所有请求头,特别是
Cookie,User-Agent,Referer,X-Requested-With,X-Anti-Request-Token等。在代码中完全模拟。DrissionPage的session.headers.update()可以批量更新。 - 检查点2:动态参数。如果发现
x-antispam-token这类动态变化的参数,采用“Page模式先行”策略:from DrissionPage import ChromiumOptions # 配置浏览器选项(无头模式,不显示图形界面) co = ChromiumOptions() co.headless(True) # 设置为True则在后台运行 # 创建Page对象 page = ChromiumPage(co) page.get('https://www.lagou.com/jobs/list_Python?city=上海') # 等待页面加载完成,特别是包含Token的JS执行完毕 page.wait.load_start() time.sleep(3) # 适当等待 # 关键:从Page对象中提取cookies和headers,应用到Session对象 # DrissionPage 可以方便地传递cookies # 假设Token在某个全局变量或Meta标签里,这里需要你根据实际情况定位元素 # 例如:token = page.ele('tag:meta[name="anti-token"]').attr('content') # session_page.headers['X-Anti-Request-Token'] = token # 更简单的方式:直接用这个Page对象去请求接口(效率低,但可靠) # json_data = page.get('接口URL').json() # 拿到数据后,可以关闭浏览器 page.quit()
- 检查点1:请求头。用浏览器无痕模式访问拉勾网,复制所有请求头,特别是
IP被限制,返回验证码页面:
- 根本解决方法:使用代理IP池。
DrissionPage的Session模式可以方便地设置代理。proxy = { 'http': 'http://your-proxy-ip:port', 'https': 'https://your-proxy-ip:port', } session_page.set.proxies(proxy) # 为本次会话设置代理 # 或者对单个请求设置:session_page.get(url, proxies=proxy) - 行为模拟: 确保
set.interval()设置的随机延迟足够大(例如3-8秒)。不要在短时间内发起大量请求。模拟人的浏览节奏,比如每爬取3-5页,随机休眠30-60秒。
- 根本解决方法:使用代理IP池。
8.2 数据解析中的细节处理
- 薪资字段解析: 拉勾网的薪资是“
8k-16k”这样的字符串。如果你想进行数值分析(计算平均薪资),需要解析它:def parse_salary(salary_str): """将‘8k-16k’解析为最低和最高薪资(单位:千)""" if not salary_str or 'k' not in salary_str: return None, None try: # 去除‘k’,按‘-’分割 parts = salary_str.lower().replace('k', '').split('-') if len(parts) == 2: low = float(parts[0]) high = float(parts[1]) return low, high elif len(parts) == 1: # 可能为‘15k以上’ val = float(parts[0]) return val, None except: pass return None, None # 在parse_position_info函数中调用,并新增‘最低薪资’,‘最高薪资’两列 - 公司福利标签:
positionLables字段包含“五险一金”、“年终奖”、“带薪年假”等标签。你可以统计哪些福利最常出现,作为分析公司吸引力的一个维度。
8.3 稳定性与可维护性优化
- 实现断点续爬: 爬取大量数据时,网络中断或程序崩溃很常见。可以设计一个检查点机制,将已爬取的页码或职位ID记录到一个文件(如
checkpoint.json)中。程序启动时读取这个文件,跳过已爬取的内容。 - 将配置参数外置: 不要将关键词、城市、最大页数等硬编码在脚本里。可以创建一个
config.py文件或使用argparse库接收命令行参数,这样不用修改代码就能改变爬取目标。# 使用argparse示例 import argparse parser = argparse.ArgumentParser(description='拉勾网职位爬虫') parser.add_argument('--keyword', type=str, default='Python', help='搜索关键词') parser.add_argument('--city', type=str, default='上海', help='城市') parser.add_argument('--pages', type=int, default=10, help='最大爬取页数') args = parser.parse_args() # 然后在main()中使用 args.keyword, args.city, args.pages - 数据存储多样化: 除了保存为CSV,你还可以直接存入数据库(如SQLite、MySQL)。
Pandas的DataFrame有to_sql方法,可以很方便地实现。import sqlite3 # 建立连接 conn = sqlite3.connect('lagou_jobs.db') # 将DataFrame写入数据库表,如果表存在则替换 df.to_sql('positions', conn, if_exists='replace', index=False) conn.close()
8.4 常见错误速查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
返回{"success":false, "msg":"您操作太频繁,请稍后再访问"} | 请求频率过高,IP被临时限制。 | 1. 大幅增加set.interval()的等待时间范围(如5-15秒)。2. 每爬几页后,用 time.sleep(random.randint(30, 60))长休眠一次。3. 使用代理IP。 |
| 只能爬到第一页数据,后续页为空 | first参数设置错误,或翻页时Referer等头信息未更新。 | 1. 确保第一页first=true,后续页first=false。2. 翻页时,更新 Referer为新的列表页URL。3. 检查页码 pn参数是否正确递增。 |
status_code为403 | 请求头不完整或被识别为爬虫,动态Token缺失。 | 1. 使用Page模式先获取一次页面,捕获完整的Headers和Cookies。 2. 检查并补齐所有必要的请求头,特别是从浏览器复制来的那些。 |
| 解析JSON时出错 | 服务器返回的不是JSON,可能是HTML错误页面(如验证码)。 | 1. 在解析前打印resp_page.text[:500]看看返回了什么。2. 如果是HTML,说明触发了反爬,需要按上述方法处理。 3. 增加异常处理,跳过错误页面。 |
| 程序运行一段时间后卡死或无响应 | 可能内存泄漏或网络连接未正常关闭。 | 1. 确保在try...except...finally块中或使用with语句管理资源。2. 对于 ChromiumPage对象,爬取完成后务必调用.quit()关闭浏览器。3. 为爬虫设置总超时时间。 |
这个项目从环境搭建到数据落地的完整流程就是这样。DrissionPage这个库确实极大地简化了在“简单请求”和“浏览器模拟”两种模式间切换的复杂度,让爬虫的编写更聚焦于业务逻辑本身。最深的体会是,面对现代网站的反爬,尊重规则、模拟真人、留有冗余是长期稳定运行的关键。不要试图用极限速度去挑战对方的防御底线,一个每天能稳定跑8小时的中速爬虫,远比一个全速跑10分钟就被封的爬虫有价值得多。