ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python爬虫实战:基于Playwright与SQLite的招聘数据分析系统

Python爬虫实战:基于Playwright与SQLite的招聘数据分析系统 简介本资源是一套面向数据分析初学者与求职者的实战型项目资料包聚焦Boss直聘平台热门技术岗位大数据、人工智能、机器学习等的数据采集、分析与可视化全流程。项目基于Scrapy框架实现分布式爬虫完整覆盖数据清洗、多维度统计薪资/学历/地域/技能要求对比及Matplotlib/Pyecharts可视化呈现助力用户理解行业人才供需现状与能力画像。压缩包共38个文件含13个Python核心脚本spiders/pipelines/settings等、12个JS前端交互文件、4个XML配置及CSV原始数据集辅以README文档与运行说明结构清晰、模块解耦便于学习调试与二次开发整体仅237KB轻量易部署。目前已有580人学习下载提供从环境搭建、代码执行到图表解读的闭环实践路径特别适合作为课程设计、求职准备或数据分析入门项目参考。1. 项目概述与核心价值最近在帮一个做人力资源咨询的朋友做市场调研他需要快速了解几个热门技术岗位在不同城市的供需情况、薪资分布和技能要求。手动去招聘网站一个个翻效率低不说数据还零散很难做横向对比。于是我决定用Python写个工具把Boss直聘上的岗位数据“搬”下来再做成可视化的分析报告。这听起来像是个典型的爬虫数据分析项目但实际做下来你会发现从数据获取、清洗、存储到分析展示每一步都有不少门道和坑。这个项目的核心就是自动化地采集Boss直聘上指定关键词比如“Python开发”、“数据分析师”的岗位信息包括公司、薪资、经验要求、技能标签、职位描述等然后对这些数据进行结构化处理和深度分析最后通过图表直观地展示出招聘市场的趋势和洞察。它不仅能用于个人求职时的市场调研看看哪个城市给钱多、哪个技能最吃香也能用于企业侧的竞品分析、薪资体系制定或者教育机构规划课程方向。对于正在学习Python数据分析、网络爬虫或者对招聘市场感兴趣的朋友来说这是一个非常“接地气”的练手项目涵盖了从数据采集到商业分析的全流程。2. 项目整体设计与技术选型考量做这个项目首先要解决的是“怎么拿数据”和“拿了数据怎么用”两个核心问题。技术栈的选择直接决定了项目的可行性、稳定性和后期维护成本。2.1 数据采集层爬虫策略与反爬对抗Boss直聘作为主流招聘平台其反爬机制相当成熟。直接使用requests库模拟HTTP请求大概率会立刻触发验证码或IP封禁。因此我的方案是采用Selenium或Playwright这类浏览器自动化工具来模拟真人操作。为什么选Selenium/Playwright因为它们能驱动真实的浏览器如Chrome执行点击、翻页、滚动等操作生成的网络请求和浏览器指纹更接近真人可以有效绕过基于请求头或简单JS验证的反爬。相比之下单纯的requestsBeautifulSoup组合在对付现代复杂前端渲染和动态加载的网站时显得力不从心。Selenium vs. Playwright的选择我最终选择了Playwright。虽然Selenium更老牌、社区更大但Playwright是后起之秀由微软开发它在几个关键点上更有优势自动等待Playwright的API设计更智能大部分操作如click,fill内置了等待元素可用的逻辑减少了编写显式等待WebDriverWait的代码量脚本更简洁稳定。录制功能Playwright拥有强大的代码录制器可以快速生成基础操作脚本对于快速探索页面结构和编写爬虫原型非常有帮助。多浏览器支持原生支持Chromium、Firefox和WebKit切换和测试更方便。性能在某些场景下Playwright的执行速度比Selenium更快。注意使用浏览器自动化工具进行数据采集本质上是模拟用户行为务必遵守网站的robots.txt协议控制访问频率在关键操作间添加随机延时如time.sleep(random.uniform(1, 3))避免对目标服务器造成压力。本项目代码仅供学习交流切勿用于商业用途或高频抓取。2.2 数据存储层结构化与持久化采集下来的数据是半结构化的JSON或字典格式我们需要将其持久化存储方便后续分析。这里有几个选择CSV/Excel文件最简单直接适合数据量小几千条以内、一次性分析的情况。用Python的pandas库可以轻松读写。优点是无需搭建数据库环境分享方便。缺点是查询效率低不适合复杂的数据关系和多维度分析。SQLite数据库轻量级、无服务器的单文件数据库。对于本项目这种个人或小规模数据分析来说SQLite是一个绝佳的选择。它避免了安装配置MySQL/PostgreSQL的麻烦通过sqlite3库Python内置或SQLAlchemyORM就能操作既能享受SQL的查询能力又保持了文件的便携性。MySQL/PostgreSQL如果数据量极大数十万条以上或者需要多人协作、高频写入才需要考虑这些专业的数据库。我的选择是SQLite。它完美匹配了本项目的需求数据量通常在万级单人进行分析需要灵活的查询例如“找出北京所有薪资大于30k的Python岗位并按公司规模排序”。我们将建立一张主表jobs字段包括职位ID、职位名称、公司名称、薪资范围拆分为最低薪salary_low和最高薪salary_high、工作经验、学历要求、所在城市、技能关键词、职位详情、发布时间等。2.3 数据分析与可视化层从清洗到洞察原始数据是“脏”的比如薪资是“20-40K·14薪”这样的字符串城市可能是“北京-朝阳区”。因此数据分析的第一步永远是数据清洗。清洗与预处理使用pandas进行数据清洗是标准操作。包括拆分薪资字符串并转换为数值型、提取城市主城区信息、将技能标签从字符串分割为列表、处理缺失值等。这部分代码的健壮性直接决定了分析结果的可信度。分析维度清洗后的数据可以从多个维度切入分析宏观趋势各城市岗位数量分布、平均薪资对比。职位需求不同经验要求如“经验不限”、“1-3年”、“3-5年”的岗位数量和薪资水平。技能图谱通过词频统计找出最常被提及的技能关键词如“Python”, “MySQL”, “Spark”, “机器学习”并可以分析技能组合例如会Python的同时常要求什么其他技能。可视化工具Matplotlib是基础但默认样式不够美观。Seaborn基于Matplotlib提供了更高级的统计图表和更漂亮的默认主题。对于制作信息丰富的仪表盘或交互式图表Plotly或Pyecharts是更好的选择它们可以生成HTML文件在浏览器中实现缩放、悬停查看详情等交互。本项目为了兼顾美观和分享便利性主要使用Seaborn制作静态分析图并使用Pyecharts生成一个综合性的HTML仪表盘。3. 核心模块拆解与实现细节3.1 爬虫引擎基于Playwright的稳健采集器爬虫的核心是稳定、可控地获取每一页的职位列表并进入详情页提取结构化信息。关键实现步骤环境初始化from playwright.sync_api import sync_playwright import pandas as pd import time, random, re def init_browser(headlessFalse): # 调试时可设为False看浏览器操作 playwright sync_playwright().start() # 使用Chromium可配置代理等参数 browser playwright.chromium.launch(headlessheadless, args[--disable-blink-featuresAutomationControlled]) context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36... ) page context.new_page() return playwright, browser, page设置合理的user_agent和视口大小让模拟更逼真。--disable-blink-featuresAutomationControlled这个参数有助于隐藏自动化特征。搜索与列表页遍历def search_jobs(page, keywordPython, city北京, max_pages5): base_url fhttps://www.zhipin.com/web/geek/job?query{keyword}city{city} page.goto(base_url) time.sleep(random.uniform(3, 5)) # 初始加载等待 job_list [] for page_num in range(1, max_pages1): print(f正在采集第 {page_num} 页...) # 等待职位列表加载 page.wait_for_selector(.job-list-box .job-card-wrapper) # 提取当前页所有职位卡片的基本链接和ID cards page.query_selector_all(.job-card-wrapper) for card in cards: job_link_elem card.query_selector(a.job-card-left) if job_link_elem: job_href job_link_elem.get_attribute(href) job_id re.search(r/job_detail/(\w).html, job_href) if job_id: job_list.append({id: job_id.group(1), url: https://www.zhipin.com job_href}) # 尝试点击下一页按钮 next_button page.query_selector(a.next) if next_button and page_num max_pages: next_button.click() time.sleep(random.uniform(2, 4)) # 翻页间隔非常重要 else: break return job_list这里有几个要点一是使用wait_for_selector确保元素加载完成再操作二是通过正则表达式从链接中提取职位ID作为唯一标识三是在翻页后必须加入随机延时这是规避反爬最基本的礼仪。详情页信息解析 获取到职位链接列表后需要逐个访问并解析。这是信息提取的核心因为详情页包含了最全的数据。def parse_job_detail(page, job_url): page.goto(job_url) time.sleep(random.uniform(1.5, 2.5)) job_info {} try: # 职位标题和公司 job_info[title] page.text_content(h1.job-title) job_info[company] page.text_content(div.company-info a) # 薪资解析 (例如“20-40K·14薪”) salary_text page.text_content(span.salary) # 使用正则表达式提取数字 salary_match re.search(r(\d)[Kk]?-?(\d)?[Kk]?, salary_text) if salary_match: low, high salary_match.groups() job_info[salary_low] int(low) if low else None job_info[salary_high] int(high) if high else int(low) # 如果只有一個数字视为月薪范围相同 else: job_info[salary_low] job_info[salary_high] None # 工作地点提取城市去除区 location_full page.text_content(div.location-address) job_info[city] location_full.split(-)[0] if location_full else None # 经验、学历要求通常在同一个区域 exp_edu page.query_selector_all(ul.job-tag-list li) job_info[experience] exp_edu[0].text_content() if len(exp_edu) 0 else job_info[education] exp_edu[1].text_content() if len(exp_edu) 1 else # 职位描述JD - 这是技能分析的关键 jd_element page.query_selector(div.job-detail-section) job_info[description] jd_element.inner_text() if jd_element else # 发布时间 publish_time_elem page.query_selector(div.job-detail-header time) job_info[publish_time] publish_time_elem.get_attribute(datetime) if publish_time_elem else except Exception as e: print(f解析职位 {job_url} 时出错: {e}) # 可以记录错误日志或标记该条数据为异常 return job_info解析详情页的关键在于选择器的稳定性。Boss直聘的页面结构可能会调整所以选择器不能写得太死。这里使用的类名如.job-title,.salary是示例实际运行时需要手动检查页面元素并更新。对于薪资、地点等字符串必须编写健壮的正则表达式或字符串处理逻辑来提取关键信息。3.2 数据存储使用SQLite进行高效管理将爬取的数据存入SQLite便于后续的复杂查询。import sqlite3 from contextlib import closing def init_database(db_pathboss_jobs.db): conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS jobs ( id TEXT PRIMARY KEY, title TEXT, company TEXT, salary_low REAL, salary_high REAL, city TEXT, experience TEXT, education TEXT, description TEXT, publish_time TEXT, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() return conn def save_job_to_db(conn, job_data): sql INSERT OR REPLACE INTO jobs (id, title, company, salary_low, salary_high, city, experience, education, description, publish_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) cursor conn.cursor() try: cursor.execute(sql, ( job_data.get(id), job_data.get(title), job_data.get(company), job_data.get(salary_low), job_data.get(salary_high), job_data.get(city), job_data.get(experience), job_data.get(education), job_data.get(description), job_data.get(publish_time) )) conn.commit() except sqlite3.Error as e: print(f数据库插入错误: {e})使用INSERT OR REPLACE可以避免重复采集同一职位导致的数据重复。为职位ID创建主键确保了唯一性。3.3 数据分析从清洗到洞察数据入库后使用pandas从SQLite中读取数据进行分析。import pandas as pd import numpy as np def load_and_clean_data(db_pathboss_jobs.db): conn sqlite3.connect(db_path) # 读取原始数据 df pd.read_sql_query(SELECT * FROM jobs, conn) conn.close() # 1. 薪资处理计算薪资中位数用于后续分析 df[salary_mid] (df[salary_low] df[salary_high]) / 2 # 2. 城市清洗去除“区”后缀只保留城市名 df[city_clean] df[city].str.replace(r市.*|区.*, , regexTrue) # 3. 从职位描述中提取技能关键词简化示例 skill_keywords [Python, Java, SQL, MySQL, Linux, Docker, Kubernetes, Spark, Hadoop, 机器学习, 深度学习, TensorFlow, PyTorch] for skill in skill_keywords: df[fskill_{skill}] df[description].str.contains(skill, caseFalse, naFalse).astype(int) # 4. 处理缺失值对于数值型薪资可以用中位数填充对于文本型用‘未知’填充 df[salary_mid].fillna(df[salary_mid].median(), inplaceTrue) df[experience].fillna(经验不限, inplaceTrue) df[education].fillna(学历不限, inplaceTrue) return df清洗后的DataFrame就是我们的“金矿”。我们可以轻松地进行各种聚合分析# 各城市平均薪资排名 city_salary df.groupby(city_clean)[salary_mid].mean().sort_values(ascendingFalse) # 不同经验要求的岗位数量 exp_distribution df[experience].value_counts() # 最热门的技能需求基于我们创建的技能布尔列 skill_demand df[[col for col in df.columns if col.startswith(skill_)]].sum().sort_values(ascendingFalse)3.4 可视化呈现用图表讲好数据故事分析结果需要用直观的图表呈现。这里使用Seaborn和Matplotlib。import matplotlib.pyplot as plt import seaborn as sns sns.set_style(whitegrid) # 设置Seaborn样式 def create_visualizations(df): fig, axes plt.subplots(2, 2, figsize(16, 12)) # 1. 各城市岗位数量分布柱状图 city_counts df[city_clean].value_counts().head(10) # 取前10 sns.barplot(xcity_counts.values, ycity_counts.index, axaxes[0, 0], paletteviridis) axes[0, 0].set_title(热门城市招聘岗位数量TOP10) axes[0, 0].set_xlabel(岗位数量) # 2. 各城市平均薪资水平箱线图 # 筛选出岗位数量较多的城市避免长尾 top_cities city_counts.head(8).index df_top_cities df[df[city_clean].isin(top_cities)] sns.boxplot(datadf_top_cities, xsalary_mid, ycity_clean, axaxes[0, 1], paletteSet2) axes[0, 1].set_title(主要城市薪资分布箱线图) axes[0, 1].set_xlabel(月薪中位数 (K)) # 3. 经验要求与薪资关系柱状图误差线 exp_order [经验不限, 1年以内, 1-3年, 3-5年, 5-10年, 10年以上] df[experience] pd.Categorical(df[experience], categoriesexp_order, orderedTrue) exp_salary df.groupby(experience)[salary_mid].agg([mean, std, count]).reindex(exp_order) axes[1, 0].bar(exp_salary.index, exp_salary[mean], yerrexp_salary[std], capsize5, colorskyblue) axes[1, 0].set_title(不同经验要求的平均薪资) axes[1, 0].set_ylabel(平均月薪 (K)) axes[1, 0].tick_params(axisx, rotation45) # 4. 热门技能需求词云这里用条形图模拟实际词云需用wordcloud库 skill_demand df[[col for col in df.columns if col.startswith(skill_)]].sum().sort_values(ascendingFalse).head(15) skill_names [name.replace(skill_, ) for name in skill_demand.index] sns.barplot(xskill_demand.values, yskill_names, axaxes[1, 1], paletterocket) axes[1, 1].set_title(职位描述中最常出现的技能TOP15) axes[1, 0].set_xlabel(出现频次) plt.tight_layout() plt.savefig(boss_job_analysis.png, dpi300, bbox_inchestight) plt.show()箱线图能很好地展示薪资的分布中位数、四分位数、异常值比单纯的平均值更有信息量。将经验要求转换为有序分类变量可以让图表更符合逻辑。4. 项目部署与自动化运行一个完整的项目不能只停留在Jupyter Notebook里。我们需要让它能自动化、周期性地运行。4.1 使用配置文件管理参数将城市、关键词、爬取页数等可变参数抽离到配置文件如config.yaml中提高代码可维护性。# config.yaml search_config: keywords: - Python - 数据分析 - 机器学习 cities: - 北京 - 上海 - 深圳 - 广州 - 杭州 max_pages_per_search: 10 # 每个搜索条件爬取的页数 crawler: headless: true # 是否无头模式 delay_range: [1, 3] # 操作延迟范围秒 timeout: 30000 # 页面加载超时毫秒 database: path: ./data/boss_jobs.db然后在主程序中读取配置import yaml with open(config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f)4.2 构建主程序流程将上述模块组装起来形成一个完整的流水线。def main(): # 1. 读取配置 keywords config[search_config][keywords] cities config[search_config][cities] # 2. 初始化数据库和浏览器 conn init_database(config[database][path]) playwright, browser, page init_browser(headlessconfig[crawler][headless]) try: for city in cities: for keyword in keywords: print(f\n开始采集: {city} - {keyword}) # 3. 搜索并获取职位列表 job_list search_jobs(page, keyword, city, config[search_config][max_pages_per_search]) print(f找到 {len(job_list)} 个职位) for i, job in enumerate(job_list): print(f 正在处理第 {i1}/{len(job_list)} 个: {job[id]}) # 4. 解析详情页 job_detail parse_job_detail(page, job[url]) job_detail[id] job[id] # 确保ID关联 # 5. 存入数据库 save_job_to_db(conn, job_detail) # 6. 随机延迟尊重网站 time.sleep(random.uniform(*config[crawler][delay_range])) finally: # 7. 无论如何都要关闭资源 browser.close() playwright.stop() conn.close() print(采集任务完成资源已释放。) # 8. 启动数据分析与可视化 df load_and_clean_data(config[database][path]) create_visualizations(df) print(数据分析图表已生成。)4.3 计划任务与日志记录为了让项目在服务器上定期运行可以使用系统的crontabLinux/Mac或计划任务Windows来定时执行主脚本。同时完善的日志记录对于排查运行时的错误至关重要。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(boss_crawler.log), logging.StreamHandler() ] ) logger logging.getLogger(__name__) # 在代码中用logger代替print logger.info(f开始采集: {city} - {keyword}) try: # 某些操作 pass except Exception as e: logger.error(f采集过程中发生错误: {e}, exc_infoTrue)5. 常见问题、踩坑记录与优化建议在实际开发和运行中会遇到各种各样的问题。这里记录一些典型情况和解决方案。5.1 爬虫被封锁或出现验证码这是最常见的问题。现象页面无法加载跳转到验证码页面或返回空白数据。排查与解决降低频率这是首要措施。大幅增加关键操作如翻页、点击详情之间的随机延迟时间模拟人类阅读速度。可以将delay_range调整为[3, 8]甚至更长。更换User-Agent在init_browser函数中轮换使用一组不同的、常见的浏览器User-Agent字符串。使用代理IP如果单个IP被封锁需要考虑使用代理IP池。Playwright在创建browser或context时可以配置代理服务器。注意免费的代理IP大多不稳定需要谨慎选择或使用付费服务。识别验证码如果验证码无法绕过可以考虑引入第三方打码平台如超级鹰、图鉴的API进行识别。但这会增加复杂性和成本且需评估其合规性。模拟更真实的行为在页面中随机滚动鼠标、随机移动鼠标轨迹。Playwright提供了page.mouse.move(x, y)等方法可以模拟。实操心得对于Boss直聘这类反爬严格的网站“慢就是快”。不要试图一次性爬取成千上万条数据。将目标拆小比如每天只爬某个城市某个关键词的5页数据分多天完成。这样被封的风险大大降低。我们的目标是获取有代表性的样本进行分析而非全量数据。5.2 页面元素定位失败现象page.wait_for_selector超时或page.text_content返回空值。排查与解决检查选择器网站前端可能改版导致CSS选择器失效。需要手动打开浏览器开发者工具重新检查目标元素的类名或ID。尽量使用相对稳定、语义化的选择器避免使用自动生成的长类名。增加等待策略wait_for_selector默认等待30秒有时可能不够。可以增加超时时间或使用page.wait_for_function等待某个JS变量或条件成立。检查页面状态在操作前打印page.url和page.title确认是否成功跳转到目标页面而不是错误页或验证码页。使用更宽松的选择器如果精确的类名总变可以尝试用XPath通过部分文本或标签结构来定位但XPath通常也更脆弱。5.3 数据解析异常现象薪资、地点等字段解析出来是None或格式错误。排查与解决加强正则表达式薪资格式可能有“20-40K”、“20K以上”、“面议”等多种。需要编写更全面的正则表达式来匹配各种情况并对无法匹配的格式设置默认值或记录日志。防御性编程在parse_job_detail函数中对每个字段的提取都用try...except包裹即使某个字段解析失败也不影响其他字段和整体流程并将错误信息记录到日志中。数据验证存入数据库前可以增加简单的数据验证逻辑比如薪资下限不应大于上限城市名称应在预定义的列表中。5.4 数据分析结果不准确现象平均薪资异常高或低技能统计明显偏离常识。排查与解决检查数据清洗逻辑回顾load_and_clean_data函数。薪资单位换算是否正确“K”是否乘以了1000处理“面议”时是否合理通常应设为NaN并在分析时排除城市清洗是否过度或不足处理异常值在计算平均薪资前应该用箱线图或标准差方法识别并剔除极端异常值比如月薪500K的极端离群点这些可能是爬虫解析错误或虚假招聘信息。样本代表性爬取的数据量是否足够是否只爬了前几页可能都是最新发布的或推广职位尝试爬取更多页面或分不同时间段多次爬取以获得更均衡的样本。技能关键词列表自定义的skill_keywords列表是否完备是否包含了同义词如“Python”和“python”“MySQL”和“mysql”可以考虑用jieba等分词库对职位描述进行分词和词频统计自动发现高频词而不是依赖预设列表。5.5 项目扩展与优化方向增量爬取目前的脚本每次都会重新爬取。可以优化为增量模式只爬取发布时间在最后一次爬取之后的新职位。这需要记录每次爬取的最新publish_time并在搜索时使用站内的时间筛选功能如果提供。数据监控与告警将日志系统升级当连续多次爬取失败或数据量异常骤减时自动发送邮件或钉钉消息告警。构建Web仪表盘使用Flask或Streamlit框架将数据分析结果做成一个简单的Web应用实时展示最新市场动态。Streamlit尤其适合快速构建数据应用。关联分析除了单维度统计可以做更深入的分析。例如使用关联规则Apriori算法分析“会Python的岗位通常还要求什么其他技能”或者构建回归模型分析城市、经验、技能对薪资的影响程度。容器化部署使用Docker将整个项目Python环境、Chromium浏览器、代码打包成一个镜像可以在任何支持Docker的服务器上一键运行极大简化部署流程。这个项目从构思到实现是一个典型的“数据管道”构建过程。它不仅仅是一个爬虫脚本更是一个涵盖了数据工程、数据分析和数据可视化的小型系统。每一步遇到的问题和解决方案都是宝贵的实战经验。希望这份详细的拆解能帮助你更好地理解并实现自己的招聘市场分析工具。记住关键在于理解原理、稳健操作并持续迭代优化。本文还有配套的精品资源点击获取
返回列表