ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Boss直聘Python岗位爬取与ECharts可视化实战

Boss直聘Python岗位爬取与ECharts可视化实战 简介本资源是一套基于Python实现的Boss直聘招聘网站数据采集与可视化分析完整项目面向爬虫初学者及数据分析入门者解决求职数据获取难、分析展示不直观等实际问题。压缩包共28个文件含8个核心Python脚本如boss.py主爬虫、echart.py可视化模块、3张示例图表png/jpg、1个配置文件app.ini、1个Chrome驱动及README.md说明文档总大小8.58MB结构清晰模块分工明确便于理解爬虫流程与可视化集成逻辑。已有3449人学习下载资源提供可直接运行的反爬适配代码含Selenium模拟访问、清洗后的结构化数据存储方案CSV/JSON、基于ECharts的交互式前端展示页面以及完整的依赖配置requirements.txt和日志调试支持助读者快速掌握动态网页抓取、数据清洗、多维度统计与前端图表嵌入全流程。1. Boss直聘岗位数据爬取与可视化从网页结构解析到ECharts动态图表落地你打开Boss直聘搜索“Python开发”页面瞬间加载出上百条岗位但无法一键导出Excel想对比北上广深的薪资分布、学历要求或公司融资阶段只能靠肉眼滚动、截图、手动统计。这不是信息过载而是原始数据被封装在DOM树和XHR请求里——它真实存在却未被结构化释放。本方案不依赖任何第三方API或付费接口仅用PythonChromeDriver解析真实用户行为路径完整捕获岗位标题、薪资范围、公司规模、行业标签、工作地点等12类字段并通过ECharts实现可交互的地图热力图、薪资箱线图、技能词云及时间趋势折线图。适合HR做竞品人才布局分析、技术团队做招聘策略复盘、高校就业中心做区域就业质量评估——所有代码本地可运行数据全程不上传、不存储至远程服务器符合企业级数据合规基本要求。2. 基于Selenium WebDriver的真实页面渲染与结构化数据提取Boss直聘前端采用Vue.js驱动的单页应用SPA关键岗位列表由JavaScript动态注入传统requestsBeautifulSoup无法获取有效内容。必须模拟真实浏览器行为触发滚动加载、点击“下一页”、等待AJAX返回。WebDriver是当前最稳定的选择它直接控制Chrome内核能处理反爬中的navigator.webdriver检测、window.chrome特征指纹校验等基础防护。2.1 ChromeDriver环境配置与权限修复下载与本地Chrome版本严格匹配的ChromeDriver例如Chrome 124对应chromedriver 124.0.6367.91解压后放入系统PATH路径如/usr/local/bin或C:\Windows\System32。若执行时报错webdriver executable may have wrong permissions需修复文件权限# Linux/macOS chmod x /usr/local/bin/chromedriver # Windows无需此步但需确认.exe文件未被杀毒软件隔离提示不要使用pip install chromedriver-py等自动管理包它们常滞后于Chrome更新导致session not created错误。手动下载并校验版本号是最可靠做法。2.2 启动带规避特征的WebDriver实例Boss直聘会检测navigator.webdriver true等自动化痕迹需在启动参数中注入规避配置from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service def create_driver(): options Options() options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--disable-blink-featuresAutomationControlled) # 关键禁用自动化标识 options.add_experimental_option(excludeSwitches, [enable-automation]) # 移除Chrome正受到自动测试软件控制提示 options.add_experimental_option(useAutomationExtension, False) # 隐藏WebDriver特征注入JS覆盖navigator属性 driver webdriver.Chrome(optionsoptions) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }) }) return driver driver create_driver() driver.get(https://www.zhipin.com/beijing/?kacity-selector)2.2.1 页面加载等待策略显式等待优于time.sleep()Boss直聘列表加载存在异步延迟硬编码time.sleep(3)不可靠。应针对具体元素设置显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 10) # 最长等待10秒 # 等待岗位卡片容器出现classjob-list job_list wait.until(EC.presence_of_element_located((By.CLASS_NAME, job-list))) # 等待首条岗位标题文本可读排除loading状态 first_title wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, .job-card h3)))2.3 岗位数据精准提取XPath与CSS选择器协同定位Boss直聘HTML结构嵌套深且部分字段如薪资、经验要求位于同一div内不同span中需组合定位字段定位方式示例XPath岗位名称//div[classjob-primary]//div[classjob-title]./div[1]/h3/text()薪资范围//div[classjob-primary]//span[classred]./div[1]/span[1]/text()公司名称//div[classinfo-company]//h3[classcompany-name]./div[2]/h3/text()工作地点//div[classjob-location]./div[1]/text()发布时间//div[classjob-card-footer]//span[classtime]./div[2]/span[1]/text()实际提取逻辑def parse_job_card(card): try: title card.find_element(By.XPATH, .//div[classjob-title]).text.strip() salary_text card.find_element(By.XPATH, .//span[classred]).text.strip() # 解析20k-35k为数值区间 salary_match re.search(r(\d)k-(\d)k, salary_text) salary_min, salary_max int(salary_match.group(1)), int(salary_match.group(2)) if salary_match else (0, 0) company card.find_element(By.XPATH, .//h3[classcompany-name]).text.strip() location card.find_element(By.XPATH, .//div[classjob-location]).text.strip().split(·)[0].strip() # 经验与学历在同一行用·分割 exp_edu card.find_element(By.XPATH, .//div[classjob-primary]//p).text.split(·) experience exp_edu[0].strip() if len(exp_edu) 0 else education exp_edu[1].strip() if len(exp_edu) 1 else return { title: title, salary_min: salary_min, salary_max: salary_max, company: company, location: location, experience: experience, education: education, crawl_time: datetime.now().isoformat() } except Exception as e: return None # 跳过异常卡片避免中断整个流程 # 批量提取当前页所有岗位 job_cards driver.find_elements(By.XPATH, //div[classjob-card]) jobs [parse_job_card(card) for card in job_cards if parse_job_card(card)]3. 数据清洗与结构化存储MongoDB文档建模与去重策略爬取数据含大量噪声重复岗位同一公司多页发布、无效薪资“面议”、“年薪制”、地点模糊“全国”、“远程”。需在入库前完成清洗并利用MongoDB的灵活Schema优势存储非结构化字段如技能标签、公司福利。3.1 清洗规则与Pandas预处理import pandas as pd import re df pd.DataFrame(jobs) # 过滤空值和面议岗位 df df.dropna(subset[salary_min, salary_max]) df df[df[salary_min] 0] # 排除0k异常值 # 标准化地点提取城市名“北京朝阳区”→“北京”“上海徐汇区”→“上海” def extract_city(location): city_map {北京: 北京, 上海: 上海, 广州: 广州, 深圳: 深圳, 杭州: 杭州} for key in city_map: if key in location: return city_map[key] return 其他 df[city] df[location].apply(extract_city) # 技能标签提取从岗位描述中抽取此处简化为标题关键词 df[skills] df[title].str.findall(r(Python|Java|SQL|React|Vue|Docker|K8s|算法|机器学习), flagsre.I).apply( lambda x: list(set(x)) if x else [] )3.2 MongoDB集合设计与去重写入创建boss_jobs集合以titlecompanylocation为复合唯一索引避免重复抓取from pymongo import MongoClient client MongoClient(mongodb://localhost:27017/) db client[recruitment] collection db[boss_jobs] # 创建唯一索引防止同岗位多次入库 collection.create_index( [(title, 1), (company, 1), (location, 1)], uniqueTrue ) # 批量插入跳过重复项 for record in df.to_dict(records): try: collection.insert_one(record) except Exception as e: if duplicate key in str(e): continue # 忽略重复错误 else: print(f插入失败: {e})3.2.1 查询优化为可视化高频字段建立支持索引// MongoDB Shell中执行 db.boss_jobs.createIndex({ city: 1, salary_min: 1, salary_max: 1 }) db.boss_jobs.createIndex({ skills: 1 }) // 支持$elemMatch查询4. ECharts动态可视化地图热力图、薪资分布箱线图与技能词云实现ECharts是企业级数据可视化首选其地理坐标映射、响应式缩放、框选交互能力远超Matplotlib。本节聚焦三个核心图表城市岗位热力图需GeoJSON坐标、薪资箱线图展示离散程度、技能词云突出技术栈权重。4.1 城市热力图基于ECharts GeoJSON的坐标映射Boss直聘城市名如“北京”需转换为经纬度。使用高德开放平台免费API或内置中国省级GeoJSONecharts/map/json/china.json# Python端聚合各城市岗位数 city_counts df[city].value_counts().to_dict() # 输出为ECharts所需格式 [{name: 北京, value: 124}, ...] geo_data [{name: city, value: count} for city, count in city_counts.items()]前端HTML模板index.htmldiv idmapChart stylewidth: 100%; height: 500px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script const chartDom document.getElementById(mapChart); const myChart echarts.init(chartDom); // 加载中国地图JSON需提前下载或CDN引用 echarts.registerMap(china, chinaJson); // chinaJson为GeoJSON对象 const option { tooltip: { trigger: item }, visualMap: { min: 0, max: Math.max(...geo_data.map(d d.value)), text: [高, 低], calculable: true, inRange: { color: [#e0f7fa, #006064] } }, series: [{ type: map, map: china, data: geo_data, label: { show: true }, emphasis: { label: { show: true } } }] }; myChart.setOption(option); /script注意ECharts地图默认无背景图若需叠加行政区划底图可在visualMap中启用show: true或使用graphic组件绘制SVG底图——但需自行处理坐标系对齐非必要不推荐。4.2 薪资分布箱线图揭示离散趋势与异常值# 按城市分组计算薪资四分位数 city_stats df.groupby(city).agg({ salary_min: [min, quantile, max], salary_max: [min, quantile, max] }).round(0).reset_index() # 构造箱线图数据ECharts要求[Q1, Q2, Q3, min, max] box_data [] for _, row in city_stats.iterrows(): q1_min row[(salary_min, quantile)] - (row[(salary_min, quantile)] - row[(salary_min, min)]) * 0.5 q3_min row[(salary_min, quantile)] (row[(salary_max, max)] - row[(salary_min, quantile)]) * 0.5 box_data.append([ row[(salary_min, min)], q1_min, row[(salary_min, quantile)], q3_min, row[(salary_max, max)] ])ECharts配置series.type: boxplotoption { tooltip: { trigger: axis, axisPointer: { type: shadow } }, grid: { left: 10%, right: 10% }, yAxis: { type: category, data: city_stats[city].tolist() }, xAxis: { type: value }, series: [{ name: 薪资区间k/月, type: boxplot, data: box_data, itemStyle: { color: #2196F3 } }] };4.3 技能词云TF-IDF加权与ECharts wordCloud集成from collections import Counter import jieba # 合并所有技能标签 all_skills [skill for skills in df[skills] for skill in skills] skill_counter Counter(all_skills) # 构造词云数据ECharts要求[{name: Python, value: 124}, ...] wordcloud_data [{name: k, value: v} for k, v in skill_counter.most_common(50)] # 前端渲染 const wordCloudOption { tooltip: {}, series: [{ type: wordCloud, gridSize: 2, sizeRange: [12, 60], rotationRange: [-90, 90], shape: pentagon, width: 90%, height: 90%, data: wordcloud_data }] };5. 生产就绪技巧滚动分页控制、异常重试与ECharts坐标轴交互增强真实爬取中Boss直聘“下一页”按钮可能因网络抖动未加载或页面滚动到底部后新卡片延迟渲染。需设计健壮的翻页逻辑并为ECharts添加实用交互功能如坐标轴缩放、右键保存图表。5.1 智能分页基于滚动高度与卡片数量的双重判定def scroll_and_load_more(driver, wait, max_pages5): current_height 0 for page in range(max_pages): # 滚动到底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) # 等待新卡片出现比等待“下一页”按钮更可靠 try: wait.until(lambda d: len(d.find_elements(By.XPATH, //div[classjob-card])) 0) except: break # 无新卡片退出 # 获取当前总卡片数 cards driver.find_elements(By.XPATH, //div[classjob-card]) new_height driver.execute_script(return document.body.scrollHeight) # 若滚动后高度未变或卡片数未增视为到底 if new_height current_height or len(cards) page * 30: # 假设每页30条 break current_height new_height scroll_and_load_more(driver, wait, max_pages10)5.2 ECharts坐标轴放大缩小与滑动支持ECharts原生支持dataZoom组件但需适配薪资、时间等数值型轴option { dataZoom: [{ type: slider, show: true, start: 0, end: 100, handleIcon: M10.7,11.9v-1.4C10.7,10.1,10,9.5,9.3,9.5H4.7c-0.7,0-1.3,0.6-1.3,1.4v1.4c0,0.4,0.3,0.7,0.7,0.7h4.6c0.4,0,0.7-0.3,0.7-0.7z, handleSize: 110%, handleStyle: { color: #fff, shadowBlur: 3, shadowColor: rgba(0, 0, 0, 0.6) } }], toolbox: { feature: { saveAsImage: { show: true, title: 保存图表 }, dataView: { show: true, readOnly: false } } } };5.3 右键事件绑定实现图表区域截图与数据导出ECharts不直接支持右键菜单但可通过监听contextmenu事件扩展myChart.on(click, function (params) { console.log(点击点:, params.name, params.value); }); // 绑定右键事件需在myChart.setOption后执行 chartDom.addEventListener(contextmenu, function (e) { e.preventDefault(); const menu document.createElement(div); menu.innerHTML ul styleposition:absolute;background:#fff;border:1px solid #ccc;padding:5px;z-index:1000; li onclickexportData()导出当前数据/li li onclicksaveAsPNG()保存为PNG/li /ul ; menu.style.left e.pageX px; menu.style.top e.pageY px; document.body.appendChild(menu); });其中saveAsPNG()调用ECharts的getConnectedDataURL()方法生成base64图片exportData()则序列化当前图表数据为CSV——这些细节决定了可视化成果能否真正进入业务决策流程。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进