资讯动态

Python招聘网站爬虫实战:Requests爬取与Pyecharts可视化分析

发布时间:2026/10/20 17:39:48 来源:尧图企业网站定制
简介这份资源是一套基于Python构建的招聘岗位数据爬取与可视化分析系统面向数据分析初学者、人力资源研究者及求职者帮助其快速获取并理解招聘市场动态。系统以Requests库为核心针对智联招聘、前程无忧、拉勾网、BOSS直聘等平台设计爬虫抓取职位名称、薪资、地点、经验与学历等字段并完成数据清洗与整理再借助Matplotlib、Seaborn或Plotly等库生成图表、热图等可视化结果揭示热门职位、薪资分布与地区就业趋势。压缩包共61个文件约10.32MB包含9个py源码、12个pyc编译文件、7个xml配置、7张png与6张jpg图表、5个txt说明、5个js脚本、2个sql数据库文件及xls、pptx、md等覆盖爬虫、分析、展示与文档全流程。目前已有66人学习。读者可参考模块化架构与反爬应对思路结合SQL数据与演示文稿快速复现从采集到可视化的完整案例并用于招聘策略分析或职业选择参考。1. 从招聘网站扒数据这件事到底值不值得自己写一套招聘网站的数据分析需求一直很旺盛尤其是做行业薪酬调研、岗位技能图谱、城市人才流动分析这类活儿的时候手里没点一手数据根本没法开口。市面上现成的数据平台要么贵得离谱要么字段裁剪得只剩标题和薪资区间连岗位职责都被吞了。所以不少人把目光投向了自己爬——用 Python 的 Requests 库去抓智联招聘、前程无忧、拉勾网、BOSS直聘这些平台的实时招聘信息再落库做可视化分析。这套思路听起来简单但真动手你会发现反爬策略、动态渲染、IP 封禁、字段清洗这几座大山一座比一座难翻。今天要拆的这个资源包就是一套完整的「爬取 清洗 存储 可视化」全链路代码适合有 Python 基础、想快速拿到可运行招聘数据分析系统的从业者。它不是玩具脚本而是把请求头伪装、Cookie 池、分页逻辑、异常重试、数据去重这些工程细节都塞进去了的实战项目。2. 拆开代码包Requests 爬虫的骨架与招聘网站的反爬边界2.1 为什么选 Requests 而不是 Selenium 或 Scrapy这个资源包的核心爬虫层用的是 Requests BeautifulSoup 组合没有上 Selenium也没有用 Scrapy 框架。原因很直接招聘网站的列表页和详情页大部分数据仍然挂在 HTML 里尤其是智联招聘和前程无忧的 PC 端页面岗位名称、公司、薪资、地点、学历要求这些字段都能通过 CSS 选择器直接抠出来。Requests 的优势是轻、快、可控单机跑一天抓几万条不是问题。Selenium 虽然能对付动态渲染但资源消耗大而且招聘网站对 WebDriver 的检测越来越严稍不注意就弹验证码。Scrapy 适合大规模分布式但学习曲线陡调试起来不如 Requests 直观。这套代码的定位就是「单机可跑、改改就能用」所以选了最朴素的方案。不过要注意BOSS直聘和拉勾网的详情页有部分数据是异步加载的Requests 直接请求只能拿到骨架 HTML。代码里对这两个平台做了特殊处理先请求列表页拿到岗位 ID再拼接详情页 URL最后用正则从返回的 JSON 片段里提取数据。这种「半动态」的抓法比硬上 Selenium 稳得多后面避坑章节会细说。2.2 请求头与 Cookie 池的配置逻辑招聘网站识别爬虫的第一道门槛就是请求头。代码里把 User-Agent 列表单独放在一个配置文件里每次请求随机抽一个同时带上 Accept、Accept-Language、Referer 这些字段。Referer 特别关键——智联招聘会检查 Referer 是否来自站内搜索页直接请求详情页会被 302 跳转到登录页。代码里的做法是先请求一次搜索列表页把响应里的 Cookie 存下来再带着这个 Cookie 去请求详情页。# config.py 请求头与 Cookie 池配置片段 import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15, Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0, ] def build_headers(refererNone): headers { User-Agent: random.choice(USER_AGENTS), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, } if referer: headers[Referer] referer return headers这段代码的逻辑是每次请求都从 UA 池里随机取一个避免同一个 UA 高频访问被标记。build_headers函数接受一个可选的referer参数在请求详情页时传入列表页的 URL模拟用户从搜索页点进详情页的自然行为。参数说明USER_AGENTS列表建议至少放 5 个不同浏览器和系统的组合太少容易被关联Referer必须和当前请求的域名一致跨域 Referer 反而会触发风控。2.3 分页抓取与去重策略招聘网站的列表页一般用page参数翻页但每个平台的分页规则不一样。智联招聘是page1,2,3...前程无忧是page1,2,3...但每页条数固定 50 条拉勾网用的是pn参数从 0 开始。代码里为每个平台单独写了分页构造器统一封装成get_page_url(keyword, city, page)函数。去重这块用的是「岗位 ID 公司 ID」联合主键。每个平台的岗位详情页 URL 里都带唯一 ID比如智联的jobdetail/xxxxx前程无忧的job/xxxxx。代码在入库前会先查一次 SQLite 里是否已存在该 ID存在就跳过。这样即使中途断点续爬也不会产生重复数据。# pipeline.py 去重入库片段 import sqlite3 def save_job(job_data): conn sqlite3.connect(jobs.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS jobs ( job_id TEXT PRIMARY KEY, title TEXT, company TEXT, salary TEXT, city TEXT, education TEXT, experience TEXT, publish_date TEXT, source TEXT ) ) try: cursor.execute( INSERT INTO jobs (job_id, title, company, salary, city, education, experience, publish_date, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) , ( job_data[job_id], job_data[title], job_data[company], job_data[salary], job_data[city], job_data[education], job_data[experience], job_data[publish_date], job_data[source] )) conn.commit() except sqlite3.IntegrityError: pass # 主键冲突说明已存在直接跳过 finally: conn.close()这里用job_id作为主键INSERT时如果冲突会抛IntegrityError捕获后静默跳过。参数说明jobs.db是 SQLite 数据库文件放在项目根目录source字段记录数据来源平台方便后续按平台筛选分析。注意不要用INSERT OR IGNORE因为有些平台返回的岗位 ID 可能为空需要额外判断。3. 数据清洗与字段标准化把脏数据变成能分析的表格3.1 薪资字段的解析与归一化招聘网站上的薪资写法五花八门「15-25K·13薪」「8千-1.2万」「300元/天」「面议」。代码里写了一个parse_salary函数用正则把各种格式统一成「月薪下限-上限单位元」的格式。比如「15-25K·13薪」会被解析成15000-25000同时把 13 薪折算成月均薪资系数存到另一个字段。「面议」和「300元/天」这类无法标准化的标记为None并在分析时排除。# cleaner.py 薪资解析片段 import re def parse_salary(raw): if not raw or 面议 in raw: return None, None # 处理 15-25K·13薪 或 15-25K match re.search(r(\d\.?\d*)-(\d\.?\d*)[Kk千], raw) if match: low float(match.group(1)) * 1000 high float(match.group(2)) * 1000 # 处理 13 薪 if 13薪 in raw: low low * 13 / 12 high high * 13 / 12 return int(low), int(high) # 处理 8千-1.2万 match re.search(r(\d\.?\d*)千-(\d\.?\d*)万, raw) if match: return int(float(match.group(1)) * 1000), int(float(match.group(2)) * 10000) return None, None逻辑说明先排除「面议」再用正则匹配「数字-数字K」的格式乘以 1000 转成元。如果带「13薪」把下限和上限都乘以 13/12 折算成月均。第二种格式「8千-1.2万」单独处理因为单位不统一。参数说明返回的low和high都是整数单位元无法解析的返回(None, None)后续分析时用dropna过滤。3.2 城市与学历字段的映射表城市字段在不同平台写法不同智联写「北京·朝阳区」前程无忧写「北京-朝阳区」拉勾写「北京」。代码里维护了一个城市映射表把「北京·朝阳区」「北京-朝阳区」都归一成「北京」。学历字段同理「本科及以上」「统招本科」「本科」统一成「本科」「大专」「专科」统一成「大专」。原始值归一化值处理方式北京·朝阳区北京按「·」分割取第一段北京-朝阳区北京按「-」分割取第一段本科及以上本科正则替换「及以上」为空统招本科本科正则替换「统招」为空大专大专直接映射专科大专映射表替换这张表放在mapping.py里清洗时直接查表。注意城市字段有些是「北京 朝阳区」带空格的代码里先用strip()去掉首尾空格再用正则[·\-\s]分割。学历字段有些写「不限」保留原样分析时单独统计。3.3 数据存储与导出格式选择清洗后的数据默认存 SQLite因为单机跑不需要额外装数据库服务一个文件就能带走。代码里也提供了导出 CSV 和 Excel 的接口方便直接丢给 Pandas 做分析。如果数据量超过 50 万条建议换成 PostgreSQL 或 MySQLSQLite 的写入性能会明显下降。# exporter.py 导出 CSV 片段 import pandas as pd import sqlite3 def export_to_csv(db_pathjobs.db, outputjobs_clean.csv): conn sqlite3.connect(db_path) df pd.read_sql(SELECT * FROM jobs WHERE salary_low IS NOT NULL, conn) df.to_csv(output, indexFalse, encodingutf-8-sig) conn.close() print(f已导出 {len(df)} 条有效数据到 {output})逻辑说明从 SQLite 读取salary_low不为空的数据用 Pandas 导出 CSV。encodingutf-8-sig是为了 Excel 打开不乱码。参数说明db_path是数据库路径output是导出文件名。如果要做可视化直接读这个 CSV 就行。4. 可视化分析用 Pyecharts 把招聘数据变成能汇报的图表4.1 薪资分布直方图与城市热力图可视化层用的是 Pyecharts因为它生成的 HTML 文件可以直接嵌到报告里交互性也比 Matplotlib 好。代码里预置了三个图表薪资分布直方图、城市岗位数量热力图、学历要求饼图。薪资直方图用Bar组件把薪资区间分成 0-5K、5-10K、10-15K、15-20K、20-30K、30K 六个桶统计每个桶的岗位数量。# visualize.py 薪资分布直方图片段 from pyecharts.charts import Bar from pyecharts import options as opts import pandas as pd def salary_distribution(csv_pathjobs_clean.csv): df pd.read_csv(csv_path) bins [0, 5000, 10000, 15000, 20000, 30000, float(inf)] labels [0-5K, 5-10K, 10-15K, 15-20K, 20-30K, 30K] df[salary_bin] pd.cut(df[salary_low], binsbins, labelslabels) counts df[salary_bin].value_counts().reindex(labels) bar ( Bar() .add_xaxis(labels) .add_yaxis(岗位数量, counts.tolist()) .set_global_opts( title_optsopts.TitleOpts(title招聘岗位薪资分布), xaxis_optsopts.AxisOpts(name薪资区间), yaxis_optsopts.AxisOpts(name岗位数量) ) ) bar.render(salary_distribution.html)逻辑说明用pd.cut把薪资下限分到六个区间value_counts统计每个区间的数量reindex保证顺序一致。Pyecharts 的Bar组件生成 HTML 文件浏览器打开就能看。参数说明bins是分桶边界labels是桶的标签可以根据实际数据调整。注意float(inf)表示无穷大用来兜住 30K 以上的数据。4.2 技能关键词词云与岗位要求提取岗位职责字段里藏着大量技能关键词代码里用 Jieba 分词 自定义词典提取。自定义词典放在skill_dict.txt里包含「Python」「Java」「MySQL」「机器学习」「深度学习」这些常见技能词。分词后过滤掉停用词统计词频再用 Pyecharts 的WordCloud组件生成词云。# skill_cloud.py 技能词云片段 import jieba from pyecharts.charts import WordCloud from collections import Counter def skill_wordcloud(csv_pathjobs_clean.csv): jieba.load_userdict(skill_dict.txt) df pd.read_csv(csv_path) all_text .join(df[description].dropna().tolist()) words jieba.lcut(all_text) # 过滤停用词和单字 stopwords set([的, 了, 和, 与, 及, 等, 负责, 要求, 熟悉]) filtered [w for w in words if len(w) 1 and w not in stopwords] counter Counter(filtered) top100 counter.most_common(100) wc ( WordCloud() .add(, top100, word_size_range[12, 60]) .set_global_opts(title_optsopts.TitleOpts(title岗位技能关键词词云)) ) wc.render(skill_cloud.html)逻辑说明先用jieba.load_userdict加载自定义技能词典保证「机器学习」不会被切成「机器」和「学习」。然后合并所有岗位描述分词后过滤停用词和单字取词频前 100 生成词云。参数说明skill_dict.txt每行一个技能词word_size_range控制词云中字体大小范围。注意description字段可能为空用dropna()过滤。4.3 用 Flask 搭一个本地数据看板如果不想每次手动打开 HTML 文件代码里还提供了一个 Flask 应用把生成的图表嵌到网页里启动后访问http://127.0.0.1:5000就能看到所有图表。Flask 的路由很简单每个图表对应一个页面首页放导航链接。# app.py Flask 看板片段 from flask import Flask, render_template app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/salary) def salary(): return render_template(salary_distribution.html) app.route(/skill) def skill(): return render_template(skill_cloud.html) if __name__ __main__: app.run(debugTrue, port5000)逻辑说明Flask 默认从templates目录找 HTML 文件把 Pyecharts 生成的 HTML 放到templates下即可。debugTrue方便调试正式跑的时候关掉。参数说明port5000是默认端口如果被占用改成 5001 或其他。注意 Pyecharts 生成的 HTML 是完整页面直接render_template会嵌套一层建议用iframe嵌入或者把图表配置抽出来单独渲染。5. 避坑与排查招聘网站爬虫的五个血泪教训5.1 现象请求返回 200 但内容为空原因招聘网站对高频请求做了软封禁返回的 HTML 里没有岗位数据只有一个空壳页面或者跳转脚本。这种情况不会报错但解析出来全是空值。解决在解析前先检查响应内容里是否包含关键字段比如「岗位名称」「薪资」这些关键词。如果连续 10 次请求都为空暂停 5 分钟再继续。代码里加了一个check_response函数检测到空内容就触发time.sleep(300)。5.2 现象Cookie 突然失效详情页全部跳登录原因智联招聘和前程无忧的 Cookie 有效期很短尤其是未登录状态下大概 15-30 分钟就过期。代码里如果一直用同一个 Cookie 池跑到一半就会全部失效。解决每请求 50 次详情页后重新请求一次列表页刷新 Cookie。代码里用了一个计数器达到阈值就调用refresh_cookie()函数。另外建议把请求间隔设在 2-4 秒之间太快容易触发风控。5.3 现象BOSS直聘返回的 JSON 里字段名和文档不一致原因BOSS直聘的接口字段名经常变今天叫jobName明天可能叫job_name。代码里如果硬编码字段名过几天就跑不通了。解决用dict.get()加多个候选键名比如data.get(jobName) or data.get(job_name) or data.get(title)。同时把原始 JSON 存一份到日志里字段变了能快速定位。5.4 现象SQLite 写入速度越来越慢原因每次INSERT都单独commit数据量到几万条后写入速度断崖式下降。解决改成批量插入每 500 条commit一次。代码里用了一个batch列表攒够 500 条就执行executemany然后commit。另外给job_id建索引查询去重时速度会快很多。5.5 现象Pyecharts 生成的 HTML 在浏览器里空白原因Pyecharts 默认从 CDN 加载 JS 资源如果网络环境访问不了 CDN图表就渲染不出来。解决在Bar()初始化时指定init_optsopts.InitOpts(js_host本地路径)或者直接用pyecharts的离线模式。代码里把 JS 文件下载到了static目录初始化时指向本地路径。6. 进阶技巧把爬虫跑成定时任务与增量更新这套代码跑通之后下一步就是让它自动跑。我一般会用 Linux 的crontab或者 Windows 的任务计划程序每天凌晨跑一次增量更新。增量更新的逻辑是只抓取最近 24 小时发布的岗位通过列表页的「发布时间」字段过滤。代码里加了一个--since参数支持传入1d、7d、30d这样的时间范围。# 每天凌晨 2 点跑增量更新 0 2 * * * cd /path/to/project python main.py --since 1d --output jobs_incremental.csv参数说明--since 1d表示只抓最近一天的数据--output指定导出文件名。如果要做全量更新把--since去掉即可。注意增量更新时数据库里的去重逻辑依然生效不会重复插入。另一个技巧是加代理池。虽然 Requests 本身支持代理但免费代理稳定性太差我一般会买几个付费代理轮询使用。代码里预留了proxy_pool.py文件把代理 IP 列表填进去就行。不过要注意代理只能解决 IP 封禁问题Cookie 和请求头该伪装还得伪装。验证数据质量的方法也很简单跑完之后用 Pandas 做一次describe()看薪资字段的均值、中位数、最大最小值是否合理。如果出现薪资下限为 0 或者上限超过 100 万说明解析逻辑有漏洞需要回去检查正则。我习惯在每次跑完增量后手动抽查 10 条数据确认字段没有错位。从那以后我每次改完解析逻辑都强制走一遍pytest里的单元测试确保不会把之前修好的坑又踩回去。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑