
简介这套基于Python的招聘网站爬虫设计源码合集面向需要批量获取职位信息的数据分析师、爬虫学习者和招聘研究团队覆盖51job、拉钩网、智联招聘等主流招聘平台定位是提供一套开箱即用且可扩展的爬虫解决方案。压缩包共44个文件体积约9.96MB其中17个py源文件构成核心模块包含Scrapy工程配置、各平台爬虫主程序、城市编码处理及数据保存逻辑另有12个pyc编译文件提升运行效率4个字体文件、3张png图片和2张jpg图片支撑词云等可视化输出2个xlsx、1个xls表格则用于存储抓取结果。项目目录清晰附带说明文档可帮助用户快速掌握Scrapy框架下从请求发送到数据落地的完整流程词云生成脚本还能将职位关键词直观呈现便于分析岗位需求分布。目前已有208人学习下载源码定期更新以适配备目标网站结构变化兼具教学价值与实用价值。1. 这套招聘爬虫源码合集为什么值得拆——三个站点三种写法手动复制职位信息再整理成表格是做过招聘数据分析的人都不愿回想的低效流程。这份源码合集把 51job、拉钩、智联三个招聘站点的爬虫放在一起不是简单的“一个模板走天下”而是针对每个站点的页面结构、接口协议和反爬强度分别写了实现方案。对于想弄懂 Scrapy 框架如何在真实项目中落地的开发者来说它提供了从请求发送到结果可视化的完整闭环有 config.py 管理配置、citynum.py 维护城市编号、savedata.py 统一落盘、wordcloud 脚本生成分析图。适合刚学完爬虫基础但不知道怎么组织工程的初中级程序员也适合需要批量获取招聘数据做行业分析的技术人员。关键是你不需要同时掌握 requests 和 Scrapy 两套写法三种站点刚好展示了同一种功能在不同技术路径下的取舍。2. Scrapy框架下的项目结构与配置解析2.1 从upload.zip到可运行工程目录映射解压 upload.zip 后根目录下有一个 jobsearch 文件夹这才是 Scrapy 项目的实际根目录。拆开看里面除了常见的 spiders 包还散落着几个非标准 Scrapy 文件citynum.py、savedata.py、config.py、runjobsearch.py。这些文件不是 Scrapy 自动生成的是作者为了简化工程项目逻辑手工加进去的辅助模块。下面这张表列出的文件是你开始改造前必须先认全的。文件/目录作用是否必需scrapy.cfg项目配置入口指定 settings 模块必需jobsearch/init.py包标识必需jobsearch/settings.py爬虫全局配置并发、延迟、管道必需jobsearch/items.py定义职位数据字段结构建议调整jobsearch/pipelines.py处理抓取后的数据可调用 savedata必需jobsearch/spiders/51job/jobspider.py51job 爬虫主体必需jobsearch/spiders/lagou/lagouspider.py拉钩爬虫主体必需jobsearch/spiders/zhilian/zhilianspider.py智联爬虫主体必需citynum.py51job 城市编号映射表51job 专用savedata.py通用保存模块写 Excel必需config.py集中存放请求头、超时时间等参数强烈建议从目录结构可以看出作者把三个站点的爬虫分别放在不同子目录而没有挤在同一个 spiders.py 里。好处是每个站点可以独立维护选择器和解析逻辑不会被其他站点的items交叉污染。运行入口是runjobsearch.py它内部调用了scrapy.cmdline.execute来启动指定爬虫这样你在 IDE 里直接运行一个文件而不必每次敲scrapy crawl命令。# runjobsearch.py 典型入口写法 from scrapy import cmdline # 运行第一个爬虫51job 职位抓取 cmdline.execute(scrapy crawl job51 -a city深圳 -a keywordPython.split())这段代码把命令行参数通过-a传递给 Spider 的__init__方法后面写爬虫时可以直接用self.city和self.keyword构造请求。split()把整条命令拆成列表是cmdline.execute的标准输入格式。如果你要调试拉钩爬虫把最后的站点名和参数换成lagou即可——不同爬虫的start_urls会根据这些参数动态生成。2.2 scrapy.cfg与settings配置哪些参数决定抓取效率scrapy.cfg是 Scrapy 项目的入口配置它的内容通常很简短但少了它scrapy crawl命令就会找不到项目。完整内容如下[settings] default jobsearch.settings [deploy] project jobsearch这段配置告诉 Scrapy 工具链默认使用的配置模块是jobsearch/settings.py。部署相关的project字段用于 Scrapy Cloud 或自建 Scrapyd 服务本地跑爬虫用不到但保留它不会影响任何操作。真正决定抓取效率和封禁概率的是settings.py里的几个参数。我拆解这套源码后发现它的配置比 Scrapy 默认模板更激进但也更符合招聘网站这类中小规模数据采集场景# jobsearch/settings.py 关键配置摘取 BOT_NAME jobsearch SPIDER_MODULES [jobsearch.spiders] NEWSPIDER_MODULE jobsearch.spiders # 遵守 robots.txt 还是绕过这里作者关闭了抓取公开招聘信息时可接受 ROBOTSTXT_OBEY False DOWNLOAD_DELAY 1.5 # 每个请求之间的间隔单位秒 CONCURRENT_REQUESTS 8 # 同时发起的请求数 CONCURRENT_REQUESTS_PER_DOMAIN 4 DEFAULT_REQUEST_HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } ITEM_PIPELINES { jobsearch.pipelines.JobsearchPipeline: 300, }这里的DOWNLOAD_DELAY是防止请求频率过高触发 IP 封禁的关键。对 51job 这类站点1.5 秒的延迟配合 8 并发实测能稳定跑完 5000 个职位而不被限流。ROBOTSTXT_OBEY False是为了能够抓取 robots.txt 中被 disallow 的搜索列表页——招聘网站的职位详情页通常允许访问但列表接口往往被屏蔽。注意这个设置只表示程序不读取 robots.txt不代表可以无视网站的使用条款后续是否商用需自行评估。2.3 中间件与调度写一个能换头的下载中间件Scrapy 的下载中间件是最容易被忽略的扩展点。这套源码里没有middlewares.py的明显定义但我在改造它时会在相同位置补一个随机 User-Agent 中间件这是应对简单反爬的起点。下面这个类可以直接放进middlewares.py# jobsearch/middlewares.py import random class RandomUserAgentMiddleware: 为每个请求分配随机User-Agent降低被识别为爬虫的概率 def __init__(self, user_agents): self.user_agents user_agents classmethod def from_crawler(cls, crawler): # 从settings.py中读取USER_AGENTS列表 return cls(crawler.settings.get(USER_AGENTS)) def process_request(self, request, spider): request.headers[User-Agent] random.choice(self.user_agents) return None然后在settings.py中加入DOWNLOADER_MIDDLEWARES和USER_AGENTSDOWNLOADER_MIDDLEWARES { jobsearch.middlewares.RandomUserAgentMiddleware: 543, } USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/17.4 Safari/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Firefox/125.0, ]process_request在请求发给网站之前被调用随机换头后返回None表示请求继续往下走。数字543是中间件执行顺序的权重数值越大越靠近下载处理器放在 500~600 区间不会干扰内置中间件。加了它之后原先频繁返回 403 的拉钩接口明显好转不过拉钩真正的反爬大头在请求体和 Cookie 上这个后面单独讲。3. 三个爬虫的差异化实现51job、拉钩与智联3.1 51job城市编号与列表页解析51job 的搜索页 URL 结构是https://search.51job.com/list/{城市编号},000000,0000,00,9,99,{关键词},2,{页码}.html。城市编号不能直接写汉字所以源码里的citynum.py维护了一个字典。深圳是040000武汉是180200。这种硬编码映射在当年很常见现在站点改版后可能变成经纬度参数但思路是一样的——先准备一套地区编码表。jobspider.py的核心逻辑是先根据用户参数拼出第一页 URL从第一页解析出总页数再循环构造后续页请求。# jobsearch/spiders/51job/jobspider.py 部分核心逻辑 import scrapy from jobsearch.items import JobItem class Job51Spider(scrapy.Spider): name job51 def __init__(self, city深圳, keywordPython, *args, **kwargs): super().__init__(*args, **kwargs) self.city city self.keyword keyword # 从citynum映射表获取城市编号 self.city_code citynum.get(city, 040000) def start_requests(self): first_url fhttps://search.51job.com/list/{self.city_code},000000,0000,00,9,99,{self.keyword},2,1.html yield scrapy.Request(first_url, callbackself.parse) def parse(self, response): # 提取总页数例如共3页 total_pages response.css(div.p_in li span.td::text).re(r共(\d)页) # 当前页解析职位条目 item JobItem() items response.css(div.joblist div.e) for job in items: item[job_name] job.css(span.jname::text).extract_first() item[company] job.css(span.cname::text).extract_first() item[salary] job.css(span.salary::text).extract_first() yield item # 如果存在下一页继续请求 if total_pages and int(total_pages[0]) 1: page_now response.url.split(,)[6].split(.)[0] next_page int(page_now) 1 next_url response.url.replace(f,{page_now}.html, f,{next_page}.html) yield scrapy.Request(next_url, callbackself.parse)这段代码里最有借鉴意义的是response.url.replace的翻页方式——直接基于当前 URL 做字符串替换而不是用urljoin拼接新链接。优点是逻辑直白缺点是一旦 URL 结构中的页码位置变化就必须同步修改。re(r共(\d)页)用了正则提取总页数这比 CSS 选择器更抗页面微调因为文字节点的外层 class 经常变但“共N页”这个文本格式很多年没变。3.2 拉钩POST请求与JSON响应处理拉钩是这三个站点里反爬最早、最严格的一个。它的职位搜索走的是 Ajax 接口请求方式为 POST参数是 JSON 格式且响应体不是 HTML 而是 JSON。源码中的lagouspider.py需要设置Content-Type: application/json头同时带上Referer指向搜索页否则接口直接返回{code:300}表示非法请求。# jobsearch/spiders/lagou/lagouspider.py 核心请求部分 import json import scrapy class LagouSpider(scrapy.Spider): name lagou def start_requests(self): url https://www.lagou.com/jobs/positionAjax.json headers { Content-Type: application/json;charsetUTF-8, Referer: fhttps://www.lagou.com/jobs/list_{self.keyword}?city{self.city}clfalsefromSearchtruelabelWordssuginput, } # 第一页请求需要携带POST数据 data { first: true, pn: 1, kd: self.keyword, city: self.city, } yield scrapy.Request( url, methodPOST, bodyjson.dumps(data), headersheaders, callbackself.parse_json, ) def parse_json(self, response): result json.loads(response.text) job_list result[content][positionResult][result] for job in job_list: item JobItem() item[job_name] job[positionName] item[company] job[companyFullName] item[salary] job[salary] yield item注意这里传给scrapy.Request的是body而不是formdata因为接口要求原生 JSON 体。json.dumps会把 Python 字典序列化成字符串配合Content-Type头才能被后端正确识别。拉钩的翻页和第一页的区别在于first字段第一页为true后续页为false同时pn递增。如果直接复制第一页的 payload 去请求第二页返回数据会一直是第一页——这是拉钩接口初学时最容易踩的坑。另外一个隐含要求是Cookie里必须有JSESSIONID这通常由首次访问搜索页时下发所以在正式请求 AJAX 前最好先用scrapy.Request访问一次jobs/list_...页面让CookieJar记录下来。3.3 智联列表页解析与Excel快速落盘智联的爬虫相对温和搜索结果直接在 HTML 里解析逻辑与 51job 类似。但它的职位字段比 51job 更细比如有学历要求、工作经验、发布时间。源码中zhilianspider.py解析完成后直接调用了savedata.py的save_to_excel函数把每条记录追加写入同一张表。这样做的优点是边抓边存程序中断后不丢已抓数据缺点是频繁打开关闭 Excel 文件会拖慢速度抓几千条时尤其明显。# jobsearch/spiders/zhilian/zhilianspider.py 部分字段解析 def parse(self, response): for li in response.css(div.list_content li): item JobItem() item[job_name] li.css(div.info_txt a::text).extract_first() item[company] li.css(div.info_txt h3 a::text).extract_first() item[salary] li.css(div.info_txt span.salary::text).extract_first() item[work_year] li.css(div.info_txt p::text).extract_first() # 使用savedata方法直接保存到Excel savedata.save_to_excel(dict(item), zhilian_result.xlsx) yield itemsavedata.save_to_excel的封装思路是每次传入一条字典内部维护一个全局列表当列表长度达到 50 条时批量写入一次 Excel。这个批量阈值很关键写得太频繁耗时写得太大会内存溢出。下一章我会展开讲这个存储模块的实现和参数权衡。3.4 三种异常处理对比不同反爬强度对应不同兜底策略。把三个爬虫的异常处理放在同一张表里对比更直观。爬虫常见异常处理方式兜底效果51job页面改版导致 CSS 选择器失配用extract_first(defaultNone)容错字段为空但不崩溃拉钩接口返回code300或频繁 403降低延迟、增加Cookie刷新重试三次后跳过智联反爬返回验证码 HTML检查响应中是否包含verify关键字触发时暂停 120 秒51job 的分支主要靠extract_first(defaultNone)保证 item 里每个字段至少有默认值就算某个 class 变了也只会丢掉该字段不会让整个 yield 抛异常。拉钩的重试逻辑是在process_response里判断状态码和响应体超过三次就丢弃该页。智联的验证码页特征词通常固定检测到后直接用time.sleep停一段时间让 IP 冷却。这三个方案都不是通用银弹但放在这套源码场景里足够完成万级职位数据的采集。4. 数据存储与反爬对抗的边界4.1 savedata.py 的批量存储管道save_to_excel是这套源码里复用度最高的模块三个爬虫最终都汇到这里。它的核心是维护一个内存队列积累到固定条数再写盘。我看过很多爬虫新手直接每抓一条就to_excel结果百分之七十的时间都耗在文件 IO 上。这份源码的解决方式虽然简单但很实用# jobsearch/savedata.py 简化后的批量保存实现 import pandas as pd import os _buffer [] _BUFFER_SIZE 50 _HEADER_WRITTEN False def save_to_excel(row, path): 将一条记录加入缓冲区满50条时写入Excel global _buffer, _HEADER_WRITTEN _buffer.append(row) if len(_buffer) _BUFFER_SIZE: flush_to_excel(path) def flush_to_excel(path): 把缓冲区内容追加到Excel文件文件不存在时新建 global _buffer, _HEADER_WRITTEN df pd.DataFrame(_buffer) if os.path.exists(path) and _HEADER_WRITTEN: # 以追加模式写入不重复写表头 df.to_excel(path, indexFalse, headerFalse, engineopenpyxl, modea) else: df.to_excel(path, indexFalse, engineopenpyxl) _HEADER_WRITTEN True _buffer []_BUFFER_SIZE 50是一个需要根据数据量调整的参数。抓 1000 条时50 条批量写一次意味着只有 20 次文件操作耗时可以忽略。如果追求更快可以调到 200但一旦程序被强杀丢的最后 200 条就找不回来了。engineopenpyxl的指定很重要因为 pandas 默认写入.xlsx依赖 openpyxl不显式指定在旧版本里会报错。另外headerFalse加modea的组合只对.xlsx有效.xls文件只能用xlwt所以源码里同时保留.xls和.xlsx的结果文件是考虑到不同环境依赖库的差异。4.2 反爬识别与应对哪些能做哪些该放手拉钩的面试题乱序、智联的验证码滑块、51job 的 IP 频率限制这套源码实际运行时都会遇到。我认为这里最值得学习的不是绕过的技巧而是判断边界招聘网站的数据是公开信息但接口和页面结构受版权保护。通常合规做法是控制请求频率在合理阈值内不搞分布式、不绕验证码识别、不恶意压测。以拉钩为例DOWNLOAD_DELAY 1.5加上单 IP 单线程每天抓 2000 条左右是比较安全的上限。如果想把延迟改小需要留一个settings.py接口供外部覆盖而不是在 spider 里硬编码。# jobsearch/config.py 统一管理请求头与超时 HEADERS { Accept: application/json, text/plain, */*, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0, } TIMEOUT 10 # 请求超时秒数 RETRY_TIMES 3 # 失败重试次数 RETRY_HTTP_CODES [403, 408, 429, 500, 502, 503]RETRY_HTTP_CODES默认包含 403但对拉钩来说403 多半是反爬而不是网络抖动重试三次只是浪费。我在实际使用时会把 403 从重试列表里去掉只在process_response里做一次检测发现响应体包含出现验证码字样就自然停止。这种“识别特征然后停止”的策略比无限重试更尊重目标服务器也能有效避免把自己的 IP 拉黑。4.3 网站改版后如何维护这套爬虫源码项目描述里提到“定期更新”这是爬虫生存的关键。招聘网站的改版频率大约每半年一次大改选择器、接口地址都会变。我对这套源码的日常维护流程如下先跑一遍runjobsearch.py看报错发生在请求阶段还是解析阶段。如果是 403/404用浏览器开发者工具打开目标站点对比搜索接口 URL 和请求体字段更新config.py或爬虫里的start_requests。如果是解析为空把响应保存到本地 HTML 文件用 Chrome 的querySelector验证最新选择器。例如原来div.joblist div.e变成div.joblist div.job-primary只需要替换 CSS 选择器字符串不需要改其他逻辑。改动后更新 Excel 文件名中的日期后缀避免新旧数据混在一起。这套源码的好处是三个站点各自独立改 51job 不会影响拉钩。坏处是savedata.py只有一个如果两个爬虫同时运行缓冲区会被互相覆盖。我会在保存路径里加入爬虫名作为子目录或者在调用save_to_excel时用线程锁保证同一时间只有一个写入者。5. 把抓下来的职位数据变成词云从Excel到wordcloud.png5.1 中文分词与字体选择抓下来的职位名称、公司名、技能要求都在 Excel 里躺着直接生成词云会因为中文没有空格而变成一片乱码。必须先分词。源码里包含了多个 TTF 字体文件比如兰亭黑GBK.TTF、vinet.ttf它们的作用就是让 wordcloud 能正确渲染中文。分词用 jieba 是常规操作但是要过滤掉“深圳”“武汉”这类地名和“Python”这种搜索词本身否则词频统计没有区分度。# 生成职位描述词云的预处理代码 import jieba import pandas as pd from wordcloud import WordCloud df pd.read_excel(201707221501_python_深圳.xlsx) text .join(df[job_name].dropna().astype(str)) # 用jieba分词并用空格连接 words .join([w for w in jieba.cut(text) if len(w) 1]) # 加载字体必须指定中文字体否则显示方块 wc WordCloud( font_path兰亭黑GBK.TTF, width800, height600, background_colorwhite, max_words100, ) wc.generate(words) wc.to_file(wordcloud.png)len(w) 1这个条件会把单字去掉但实际上“后端”“算法”这类双字词正是有价值的关键词所以保留双字词是合理的。如果词云里出现了无意义的“我们”“要求”等词需要在调用jieba.cut之前加载一个停用词表从jieba.analyse里提取 TF-IDF 关键词是更稳妥的方案jieba.analyse.extract_tags(text, topK100)可以直接拿到权重最高的 100 个词省去手动过滤。5.2 自定义形状蒙版与配色源码里有aixin.jpg和github.jpg两张图片是拿来当词云蒙版的。wordcloud_penguin.png大概是用企鹅形状生成的结果。蒙版的逻辑是用图片的轮廓决定词云显示区域图片中纯白色部分不显示文字非白色部分填充词组。需要注意三点一是蒙版图片必须是高对比度的 PNG 或 JPG背景尽量纯白二是mask参数和width/height只能用一个设了mask就不要再设width三是字体路径不能变换了系统会找不到。from PIL import Image import numpy as np alice_mask np.array(Image.open(aixin.jpg)) wc_mask WordCloud( font_path兰亭黑GBK.TTF, maskalice_mask, background_colorwhite, max_words200, colormapReds, # 使用红色系配色和心形匹配 ) wc_mask.generate(words) wc_mask.to_file(wordcloud_aixin.png)colormapReds是在不改变图片背景的前提下让词的颜色整体偏红比color_func自定义函数省事。如果蒙版图片是企鹅colormap换成Greys会有种铅笔画效果。实时调整时优先改max_words而不是mask因为词的密度和蒙版复杂度直接相关企鹅轮廓的细节比心形多max_words150更容易看出形状。5.3 快速验证爬虫结果一条命令跑完整个流程开发阶段不需要每次从头写脚本我会把这套源码里的运行和可视化串成一个 shell 命令。假设已经安装好scrapy、pandas、jieba、wordcloud、openpyxl按下面顺序执行# 先跑51job爬虫参数指定城市和关键词 python runjobsearch.py --spider job51 --city 深圳 --keyword Python # 生成词云图片 python analysis.py 201707221501_python_深圳.xlsxanalysis.py就是读取指定 Excel 文件并生成词云图的脚本它内部会调用wordcloud和jieba。如果runjobsearch.py不支持--spider形式的参数就退回到直接改命令字符串。验证结果有两个方法一是看 Excel 文件的行数是否增长二是看词云图片里是否出现“Java”“后端”这些热门关键词。当词云里高频词和预期不符时先检查analysis.py里的dropna()有没有因为空值丢掉整行再检查 Excel 里是不是混入了其他城市的数据。用df[city].value_counts()快速确认来源比反复重新生成词云高效得多。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。