
简介这是一套面向Python初学者与数据采集实践者的招聘网站爬虫实战源码聚焦人力资源领域数据获取痛点助力企业人才筛选与岗位分析。资源包含33个文件涵盖10个核心Python脚本含主爬虫pachong.py及多个模块化子项目、14个文本文件含readme.txt使用说明与HTML缓存样本、4个XML配置文件、3个CSV结构化输出样例以及IDE和Git相关配置文件整体压缩包仅2.93MB轻量易部署。已有511人学习下载适合开展网络爬虫入门训练、反爬策略理解与招聘数据清洗实践。读者可直接运行项目复现完整采集流程获得从网页请求、HTML解析、字段抽取到CSV落地的全链路代码范例目录按功能分层如zhilianzhaopin_python*系列子模块、htmltxt缓存区、to_csv导出区结构清晰便于二次开发与调试同时附带多份真实抓取的HTML样本与日志记录为排错与规则适配提供可靠依据。1. 这不是“写个脚本抓点数据”那么简单一个真实招聘网站爬虫项目的底层逻辑我做招聘数据相关项目快八年了从最早用Excel手动整理BOSS直聘的岗位列表到后来给猎头公司搭整套人才画像系统再到去年帮一家HR SaaS厂商重构他们的职位数据清洗管道——所有这些起点都是同一个动作从招聘网站上稳定、合规、可持续地获取结构化岗位信息。很多人看到“Python招聘网站数据爬虫”这个标题第一反应是“哦又一个教requestsBeautifulSoup的入门教程”。但现实里你真把它当个练手小项目去跑不出三天就会被封IP、被验证码卡死、被反爬规则逼到重写三次代码。这不是技术难度问题而是对招聘网站运行机制、数据价值链条和工程落地约束的系统性认知缺失。核心关键词——Python、招聘网站、数据爬虫、源码——背后藏着三重真实需求第一层是业务需求HR要批量比对薪资带宽、技术栈热度、JD关键词分布第二层是工程需求要扛住每分钟上百次请求、自动识别动态渲染、应对JS加密字段、处理Cookie会话漂移第三层是合规需求必须绕开robots.txt限制区域、避开登录态敏感数据、控制请求频次不干扰网站服务。这四词组合本质上是在问“如何用Python构建一条合法、稳定、可维护的数据采集流水线而非一次性的网页截图脚本”它适合三类人正在搭建企业人才数据库的HRBP、需要竞品岗位分析的招聘经理、以及刚学完基础语法、想接触真实Web生态的Python学习者——但最后这类人最容易踩坑因为网上90%的“源码”只展示了最光鲜的第一步却把后面八步的血泪教训全删了。我见过太多人卡在第一步连目标网站都选错了。智联、前程无忧、猎聘、BOSS直聘、拉勾表面都是招聘平台底层架构天差地别。智联用传统服务端渲染XPath能直接定位BOSS直聘大量依赖WebSocket推送实时消息静态HTML里根本没数据拉勾的职位详情页URL带加密参数不还原算法就拿不到完整JD。所谓“通用爬虫”根本不存在每个网站都是独立战场。所以这篇内容不提供“一键复制粘贴就能跑”的万能源码而是带你拆解为什么选这个技术栈、为什么这样设计请求链路、为什么某个字段要单独加解密模块、为什么日志里必须记录User-Agent变更轨迹。所有代码细节都锚定在真实运维场景里——比如某次凌晨三点报警发现拉勾的反爬策略升级所有请求返回403而根源竟是他们把Referer校验从域名级收紧到了路径级。这种细节只有天天盯着监控面板的人才懂。2. 项目整体设计与思路拆解为什么放弃“简单粗暴”选择“分层防御”2.1 不是写爬虫是建数据管道四个不可妥协的设计原则很多初学者把爬虫当成“发HTTP请求→解析HTML→存CSV”三步流程这在测试环境能跑通上线后必然崩。真实招聘数据采集必须遵循四个硬性原则它们直接决定了架构选型第一请求隔离原则。同一IP不能同时请求职位列表页和详情页。招聘网站的风控系统会关联行为链如果A IP在1秒内先刷10个列表页再瞬间跳转到50个详情页立刻触发流量异常模型。我们采用“双代理池”设计——列表页走数据中心代理高并发、低延迟详情页走住宅代理IP分散、会话真实两者完全隔离。实测下来列表页QPS可压到80详情页维持在3-5整体成功率从62%提升到91.7%。第二渲染解耦原则。BOSS直聘和脉脉的职位卡片是React动态渲染但关键字段如“薪资范围”“工作年限”藏在JSON-LD结构化数据里而智联的薪资却是CSS隐藏文字伪元素拼接。我们不统一用Selenium模拟浏览器——太重、太慢、太不稳定。而是分三层处理纯静态页用lxml直接解析JS渲染页用Playwright无头模式截取DOM快照JSON-LD数据则用正则预提取json.loads二次校验。这样既避免了全量渲染的资源消耗又保证了字段抽取的准确率。第三状态保鲜原则。招聘网站的Cookie有效期极短BOSS直聘的token 15分钟失效猎聘的session_id 30分钟过期。我们放弃“登录一次用一整天”的幻想改为“按需续签”每次请求前检查Cookie时效失效则自动触发模拟登录流程账号密码短信验证码双因子登录成功后更新全局会话池。这个模块单独封装成SessionManager类内部维护LRU缓存最多保留20个有效会话避免账号被锁。第四字段契约原则。不同网站对“工作经验”字段的表述五花八门“3-5年”“三年以上”“5年经验优先”“应届生亦可”。我们不直接存原始文本而是定义标准化字段契约experience_min整数、experience_max整数、experience_flexible布尔值。解析时调用ExperienceNormalizer模块用规则引擎模糊匹配双重校验。比如遇到“5年以上”设min5, maxNone, flexibleTrue遇到“3-5年”设min3, max5, flexibleFalse。这样下游分析时薪资与经验的相关性计算才有意义。2.2 技术栈选型为什么不用Scrapy而用RequestsPlaywright组合网上90%的招聘爬虫教程推荐Scrapy但我在线上项目中已全面弃用。原因很实在Scrapy的中间件机制在面对招聘网站复杂的反爬时调试成本太高。比如BOSS直聘的请求头校验不仅检查User-Agent还验证sec-ch-ua-mobile、sec-fetch-site等Chrome专用头Scrapy默认不生成这些字段你得写一堆DownloaderMiddleware去伪造而每个网站的头规则都不一样维护起来像在填无底洞。我们最终选定Requests Playwright组合理由非常务实Requests负责80%的稳态请求列表页、搜索页、公司主页等静态或半静态页面用Requests发GET请求配合requests.adapters.HTTPAdapter设置连接池pool_connections10, pool_maxsize20复用TCP连接。关键技巧是所有Headers都从真实浏览器抓包中复制包括Accept-Encoding: gzip, deflate, br和Sec-Fetch-Dest: document而不是用requests默认的简版头。Playwright负责20%的动态攻坚仅在必须执行JS的场景启用比如拉勾的职位详情页需要滚动到底部触发“查看更多公司信息”或者猎聘的薪资弹窗需要点击展开。Playwright的优势在于它能导出完整的浏览器上下文包括localStorage、IndexedDB我们把登录态保存为context.zip下次直接加载省去重复登录。实测单个详情页解析耗时从Selenium的3.2秒降到1.7秒内存占用减少65%。Pandas不是用来存数据的而是做数据契约校验的所有解析后的数据先塞进DataFrame用df[salary].apply(validate_salary_format)做字段清洗再用df.to_sql()写入数据库。这样能在入库前拦截90%的脏数据比如“面议”“年薪20W提成”这类非结构化文本直接标为salary_raw字段存档不污染主表。这套组合没有炫技成分全是被线上事故逼出来的选择。去年有次猎聘改版把职位列表的分页参数从page2改成offset20Scrapy的CrawlSpider规则全失效我们花了6小时重写Rule而我们的Requests方案只需改一行params{offset: page*20}10分钟搞定。2.3 架构图解一个请求从发出到落库的七道关卡整个数据管道不是线性流程而是七层防御式架构每一层都有明确职责和熔断机制路由层Router根据URL域名分发请求。zhilian.com走静态解析流zhipin.com走Playwright流liepin.com走登录态会话流。配置文件router_rules.yaml定义路由策略支持热更新。代理层ProxyManager维护两个独立代理池。列表代理池含50个数据中心IP轮询使用详情代理池含200个住宅IP按地理位置分组北上广深优先每个IP绑定独立User-Agent指纹。会话层SessionManager管理登录态。每个账号对应一个SessionSlot对象包含cookie、token、last_used时间戳。请求前调用get_valid_session(domain)自动剔除过期会话并触发续签。渲染层Renderer静态页直接返回HTMLJS页交由Playwright集群处理。我们部署了3台Playwright WorkerDocker容器每台限10个并发浏览器实例防内存溢出。解析层Parser按网站定制Xpath/CSS选择器。关键字段如job_title、company_name、salary_text必须有fallback逻辑。例如拉勾的薪资字段首选//span[classsalary]失败则尝试//meta[itempropbaseSalary]/content。归一化层Normalizer将原始字段映射到标准Schema。salary_text经SalaryExtractor模块输出salary_min、salary_max、salary_currencyjob_description经JdCleaner模块删除HTML标签、广告语、联系方式。存储层Storage数据先写入Kafka Topictopic_job_raw经Flink实时清洗后落库到MySQL分表按城市分表job_shanghai_2024q3。原始HTML存OSS保留30天供审计。这个架构最大的好处是可替换性。去年BOSS直聘升级了Canvas指纹检测我们只替换了Renderer层的Playwright配置增加--disable-blink-featuresAutomationControlled参数其他六层完全不动。如果是Scrapy单体架构就得重跑整个Pipeline。3. 核心细节解析与实操要点那些教科书绝不会写的“脏活”3.1 招聘网站的反爬暗礁从User-Agent到Canvas指纹的实战对抗你以为设置个随机User-Agent就万事大吉招聘网站的反爬早已进化到硬件级。以BOSS直聘为例他们检测的不仅是HTTP头还有浏览器指纹的17个维度Canvas指纹在页面注入一段JS让Canvas绘制特定图形读取toDataURL()生成的哈希值。我们用Playwright的page.add_init_script()注入补丁canvas_patch const originalToDataURL HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL function() { return data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAXAv7czFnnAAAAAElFTkSuQmCC; }; page.add_init_script(canvas_patch)这段代码把所有Canvas输出强制替换为固定PNG的base64既绕过检测又不影响页面功能。WebGL指纹读取gl.getParameter(gl.VENDOR)返回显卡厂商。我们禁用WebGL渲染playwright.launch(headlessTrue, args[--disable-webgl])代价是部分3D效果页面无法加载但招聘网站根本不用WebGL。字体枚举通过document.fontsAPI检测已安装字体。Playwright默认只加载系统基础字体我们额外添加--font-render-hintingnone参数关闭字体微调。更隐蔽的是请求时序指纹。智联会分析两次请求的间隔时间人类操作有随机停顿机器请求则呈现固定周期。我们给Requests请求加了动态延时import random def get_delay(): # 基于正态分布模拟人类操作间隔μ1.2s, σ0.3s delay max(0.5, random.gauss(1.2, 0.3)) return delay # 请求前 time.sleep(get_delay())这个细节让智联的“机器人行为”误判率从73%降到12%。3.2 动态参数破解拉勾网加密URL参数的逆向实战拉勾网的职位详情页URL形如https://www.lagou.com/jobs/xxxxx.html?showxxxxx其中show参数是AES加密的。直接抓包看Network面板发现每次刷新页面show值都变但对应的职位ID不变。这说明加密密钥是页面JS生成的。我们用Playwright打开页面注入JS钩子page.evaluate( // 监听所有fetch请求捕获加密参数生成逻辑 const originalFetch window.fetch; window.fetch function(...args) { if (args[0].includes(jobs/)) { console.log(加密参数来源:, args[0]); } return originalFetch.apply(this, args); } )然后在Console里搜索show定位到encrypt.js文件。反编译后发现密钥来自window._lg_encrypt_key而这个变量在页面初始化时由window.__INITIAL_STATE__里的securityKey字段生成。最终破解方案用Playwright加载页面后执行page.evaluate(window._lg_encrypt_key)获取密钥再用PyCryptodome解密from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def decrypt_show_param(encrypted_show: str, key: str) - str: # 拉勾使用AES-128-CBCIV固定为16个0 iv b\x00 * 16 cipher AES.new(key.encode(), AES.MODE_CBC, iv) decrypted unpad(cipher.decrypt(bytes.fromhex(encrypted_show)), AES.block_size) return decrypted.decode()这个过程花了我整整两天但换来的是100%的URL解析成功率。网上所谓的“万能解密脚本”根本没处理IV和Padding跑不通。3.3 数据质量生死线薪资字段的12种“面议”陷阱与清洗策略招聘网站里“面议”是最常见的脏数据但它的形态有12种之多每种都需要不同策略原始文本类型处理策略“面议”纯面议salary_min None, salary_max None, salary_negotiable True“15K-25K·13薪”含年终奖提取15000-25000bonus_multiplier13“年薪20W提成”含浮动salary_min200000, salary_maxNone, salary_typeannual, has_bonusTrue“月薪8K-12K13薪”括号注释正则(\d)K-(\d)K.*?(\d)薪→min8000, max12000, bonus13“200元/天”日薪salary_min200*22, salary_max200*22, salary_unitmonthly按22天月薪换算我们用pyparsing库构建语法树而不是简单正则from pyparsing import Word, nums, oneOf, Optional, Group salary_expr ( Word(nums) Optional(K | k | W | w) - Word(nums) Optional(K | k | W | w) Optional(oneOf(· * x)) Optional(Word(nums) 薪) )这样能精准匹配“15K-25K·13薪”而不会把“Java开发工程师”里的“K”误匹配。实测清洗准确率达99.2%远超正则方案的83%。3.4 可维护性设计为什么要把选择器写进YAML配置文件新手常把XPath硬编码在Python里# 危险网站一改版就全挂 title tree.xpath(//div[classjob-name]/h1/text())[0]我们全部外置到selectors/lagou.yamllagou: job_list: items: //div[classlist-item] title: .//div[classp-top]/h3/text() company: .//div[classcompany]/text() salary: .//span[classsalary]/text() job_detail: jd: //div[classjob-detail]/descendant::text()Python代码只做配置加载import yaml with open(selectors/lagou.yaml) as f: selectors yaml.safe_load(f) title tree.xpath(selectors[lagou][job_list][title])[0]好处立竿见影当拉勾把job-name改成position-title运维只需改YAML不用动Python代码发布零风险。去年我们用这套机制在3小时内完成了对5个招聘网站的选择器批量更新而传统硬编码方式平均每个网站要修2小时。4. 实操过程与核心环节实现从零搭建可运行的最小可行系统4.1 环境准备与依赖安装避坑指南不要用pip install -r requirements.txt一键安装。招聘爬虫的依赖有隐性冲突playwright和selenium共存会导致Chrome驱动混乱。我们只装Playwrightpip install playwright然后playwright install chromium不用firefox或webkit因为招聘网站兼容性最好。lxml必须指定版本pip install lxml4.9.3。新版lxml 5.x在解析某些招聘网站的乱码HTML时会崩溃报错XMLSyntaxError: Document is empty降级到4.9.3完美解决。pycryptodome不能装crypto已被弃用必须装pycryptodome且Windows下要提前装Visual Studio Build Tools否则编译失败。虚拟环境创建命令python -m venv crawler_env source crawler_env/bin/activate # Linux/Mac # crawler_env\Scripts\activate # Windows pip install --upgrade pip pip install requests pandas pymysql kafka-python pyyaml pyparsing pip install playwright playwright install chromium提示Playwright安装后务必运行playwright show-trace测试是否能启动浏览器有些云服务器缺少字体库会报错需apt-get install fonts-liberation。4.2 核心模块代码实现SessionManager与Parser的工业级写法SessionManager带自动续签的会话池import time import json import requests from urllib.parse import urljoin from typing import Dict, Optional, Tuple class SessionManager: def __init__(self, config_file: str): with open(config_file) as f: self.config json.load(f) self.sessions: Dict[str, dict] {} self.lock threading.Lock() def _login_zhipin(self, account: dict) - Optional[dict]: BOSS直聘模拟登录含短信验证码 s requests.Session() # 1. 获取登录页提取csrf_token resp s.get(https://www.zhipin.com/login/) csrf re.search(rcsrfToken:([^]), resp.text).group(1) # 2. 发送手机号获取验证码 s.post(https://www.zhipin.com/wapi/zppassport/mobile/sendSmsCode.json, json{mobile: account[phone], csfrToken: csrf}) # 3. 这里应接入短信平台API实际项目中我们用阿里云SMS SDK # code get_sms_code(account[phone]) code 123456 # 测试用 # 4. 提交验证码登录 login_resp s.post(https://www.zhipin.com/wapi/zppassport/mobile/login.json, json{mobile: account[phone], code: code, csfrToken: csrf}) if login_resp.json()[zpData][code] 0: return { cookies: requests.utils.dict_from_cookiejar(s.cookies), headers: {User-Agent: account[user_agent]}, last_used: time.time() } return None def get_session(self, domain: str) - dict: 获取有效会话自动续签 with self.lock: # 查找未过期会话 for sid, sess in self.sessions.items(): if sess[last_used] time.time() - 1800: # 30分钟有效期 sess[last_used] time.time() return sess # 无有效会话新建 account self.config[accounts][domain][0] new_sess self._login_zhipin(account) if new_sess: sid f{domain}_{int(time.time())} self.sessions[sid] new_sess return new_sess raise Exception(fSession creation failed for {domain})Parser带Fallback的字段抽取器from lxml import html import re class LagouParser: def __init__(self, selector_config: dict): self.selectors selector_config def parse_list_page(self, html_content: str) - list: tree html.fromstring(html_content) items [] for item in tree.xpath(self.selectors[items]): try: title self._safe_xpath(item, self.selectors[title], default) company self._safe_xpath(item, self.selectors[company], default) salary self._safe_xpath(item, self.selectors[salary], default) # Fallback如果salary为空尝试从data-salary属性取 if not salary: salary_attr item.get(data-salary) if salary_attr: salary salary_attr items.append({ title: title.strip(), company: company.strip(), salary_raw: salary.strip(), url: urljoin(https://www.lagou.com/, item.xpath(.//a/href)[0] if item.xpath(.//a/href) else ) }) except Exception as e: # 记录解析错误但不停止整个批次 print(fParse error on item: {e}) continue return items def _safe_xpath(self, element, xpath, default): 安全XPath提取避免IndexError result element.xpath(xpath) return result[0].strip() if result else default4.3 完整运行流程从启动到入库的12个步骤一个典型职位采集任务的执行流程加载路由配置读取router_rules.yaml确定lagou.com走Playwright流。获取代理IP从代理池取出一个住宅IP绑定Chrome User-Agent指纹。获取会话调用SessionManager.get_session(lagou.com)检查是否有有效会话无则触发登录。构造请求https://www.lagou.com/jobs/positionAjax.json?city上海kdPythonpn1。发送请求用Requests发GETHeaders包含X-Requested-With: XMLHttpRequest拉勾要求。响应校验检查HTTP状态码、Content-Type: application/json、响应体是否含success:true。解析JSON提取content.positionResult.result数组每个元素是职位摘要。生成详情URL对每个职位拼接https://www.lagou.com/jobs/{positionId}.html。详情页渲染用Playwright打开URL等待.job-detail元素出现截图存档。字段抽取用LagouParser解析HTML获取JD、公司规模、融资阶段等字段。数据归一化调用SalaryExtractor.extract(salary_raw)、JdCleaner.clean(jd_text)。写入Kafka序列化为JSON发送到job_rawTopic由Flink消费入库。整个流程封装在main.py的crawl_lagou_shanghai_python()函数里支持命令行参数python main.py --site lagou --city 上海 --keyword Python --pages 54.4 部署与监控如何让爬虫在服务器上“自己活下来”本地跑通不等于生产可用。我们用Supervisor管理进程配置/etc/supervisor/conf.d/crawler.conf[program:crawler-lagou] command/opt/crawler_env/bin/python /opt/crawler/main.py --site lagou --city 上海 --keyword Python directory/opt/crawler usercrawler autostarttrue autorestarttrue startretries3 redirect_stderrtrue stdout_logfile/var/log/crawler/lagou.log关键监控指标写入Prometheuscrawler_requests_total{sitelagou,status200}成功请求数crawler_parse_errors_total{sitelagou,fieldsalary}薪资字段解析失败数crawler_proxy_failures_total{proxy_typeresidential}住宅代理失败数告警规则当rate(crawler_parse_errors_total[1h]) 0.1错误率超10%发企业微信告警。去年有次拉勾改版薪资字段从span classsalary移到div style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />