ARTICLE DETAIL

资讯详情

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

招聘网站爬虫+数据分析+可视化:毕业设计全链路技术方案

招聘网站爬虫+数据分析+可视化:毕业设计全链路技术方案 简介这是一套面向计算机、通信、人工智能、自动化等相关专业学生与教师的Python毕业设计完整源码围绕招聘网站数据爬取、清洗、分析与可视化展开适合作为毕业设计、期末课程设计或大作业的参考方案也便于基础较好的学习者在此基础上修改扩展。压缩包共54个文件约6.67MB包含4个py脚本与4个ipynb笔记本用于爬虫与数据分析8个csv与8个xml承载招聘原始及清洗数据另有html、js、css、less等前端文件配合Echarts大屏展示以及png、jpg图片和ttf字体用于词云与图表呈现。项目已通过调试测试答辩评审分达98分目录按数据获取、数据清洗、数据分析、数据可视化等模块划分涵盖前程无忧招聘信息抓取、工作经验与学历分布饼图、福利与招聘信息词云等典型任务可帮助读者掌握从采集到展示的完整链路。目前已有381人学习下载具备较高的借鉴与复用价值。1. 招聘网站爬虫数据分析可视化一套能跑通的毕业设计技术栈长什么样每年毕业季招聘网站的数据都是最容易被拿来做毕业设计的题材之一。原因很直接数据结构相对规整、字段有业务含义、可视化出来好看而且从爬虫到分析再到前端展示一条链路能把 Python 的主要技能点全覆盖。但真正动手的人会发现坑不在能不能爬到而在爬完之后怎么办——字段对不齐、薪资是区间文本、城市名有几十种写法、岗位描述里全是噪声最后可视化出来一堆没法解释的图。这套方案要解决的就是这条完整链路用 requests 抓取招聘列表页和详情页用 SQLAlchemy 把数据落到本地数据库用 pandas 做清洗和聚合分析最后用 ECharts 或 Flask 把结果呈现出来。适合正在做计算机、软件工程、大数据方向毕业设计的人也适合想拿一个完整项目练手 Python 数据分析的入门者。下面按实际搭建顺序拆开讲每一步都给到能直接抄的代码和参数说明。2. 爬虫层怎么搭从请求构造到 SQLAlchemy 落库2.1 先想清楚爬什么列表页与详情页的字段拆分招聘网站的页面结构通常是两层搜索列表页给出岗位名称、公司、薪资、城市、学历要求等摘要信息详情页才有岗位职责、任职要求、福利标签等长文本。做毕业设计时最稳妥的策略是列表页拿结构化字段详情页按需补充。不要一上来就全量抓详情页请求量会翻好几倍调试阶段很容易被封。我一般会先定义一个目标字段表把必须有的和锦上添花的分开字段名来源类型是否必抓job_title列表页字符串是company_name列表页字符串是salary_text列表页字符串是city列表页字符串是education列表页字符串是experience列表页字符串是job_detail详情页长文本否welfare_tags详情页列表否这张表决定了后面数据库建表、清洗逻辑和可视化维度的边界。字段没定清楚就开写后面改表结构会非常痛苦。2.2 requests 请求构造headers、分页与重试列表页通常是 GET 请求带关键词和页码参数。下面是一个最小可运行的抓取函数重点看 headers 和重试逻辑import requests import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } def build_session(): session requests.Session() retry Retry( total3, backoff_factor1.5, status_forcelist[429, 500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.mount(http://, adapter) session.headers.update(HEADERS) return session def fetch_list_page(session, keyword, page): url https://example-job-site.com/search params {keyword: keyword, page: page} resp session.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.text逻辑说明Retry对象负责在遇到 429 或 5xx 时自动重试backoff_factor1.5表示每次重试间隔按 1.5 的倍数递增避免短时间内反复打同一个接口。timeout10是必须加的否则遇到慢响应会一直挂住。分页抓取时每页之间加time.sleep(random.uniform(1.5, 3.5))这个随机区间是我试过比较稳的太短容易触发风控太长抓取效率太低。参数怎么改如果目标站点返回的是 JSON 接口而不是 HTML把resp.text换成resp.json()后续解析逻辑从 BeautifulSoup 换成字典取值。status_forcelist里可以按实际情况加上 403但要注意 403 有时是永久性的重试也没用。2.3 用 SQLAlchemy 建表并写入别再用 CSV 当数据库很多毕业设计代码用 CSV 存爬虫结果写到几万条之后查询和去重都很麻烦。用 SQLAlchemy 定义一个模型后面 pandas 直接read_sql就能拿数据from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime from sqlalchemy.orm import declarative_base, sessionmaker from datetime import datetime Base declarative_base() class JobPost(Base): __tablename__ job_posts id Column(Integer, primary_keyTrue, autoincrementTrue) job_title Column(String(200), nullableFalse) company_name Column(String(200)) salary_text Column(String(100)) city Column(String(50)) education Column(String(50)) experience Column(String(50)) job_detail Column(Text) crawl_time Column(DateTime, defaultdatetime.now) engine create_engine(sqlite:///jobs.db, echoFalse) Base.metadata.create_all(engine) Session sessionmaker(bindengine) def save_jobs(job_list): session Session() try: for job in job_list: exists session.query(JobPost).filter_by( job_titlejob[job_title], company_namejob[company_name], cityjob[city] ).first() if not exists: session.add(JobPost(**job)) session.commit() except Exception as e: session.rollback() print(写入失败:, e) finally: session.close()逻辑说明create_engine里用 SQLite 是为了零配置换成 MySQL 只需要改连接字符串为mysqlpymysql://user:passhost/dbname。去重逻辑用job_title company_name city三个字段组合判断比单字段可靠。session.rollback()在异常时回滚避免半截数据写进去。参数说明String(200)的长度按实际字段调整岗位名称一般不会超过 100 字但公司名偶尔有很长的。crawl_time用defaultdatetime.now自动记录抓取时间后面做时间维度分析时有用。3. 数据清洗与薪资解析把文本变成能算的数值3.1 薪资字段的四种常见格式与统一解析招聘网站的薪资文本五花八门常见的有15-25K、1.5-2万、200-300元/天、面议。要拿薪资做分析必须先把这些转成统一的月薪数值区间。下面这个解析函数覆盖了大部分情况import re def parse_salary(salary_text): if not salary_text or 面议 in salary_text: return None, None text salary_text.strip().lower() # 匹配 15-25k / 15-25千 m re.match(r(\d\.?\d*)-(\d\.?\d*)[k千], text) if m: return float(m.group(1)) * 1000, float(m.group(2)) * 1000 # 匹配 1.5-2万 m re.match(r(\d\.?\d*)-(\d\.?\d*)万, text) if m: return float(m.group(1)) * 10000, float(m.group(2)) * 10000 # 匹配 200-300元/天 m re.match(r(\d)-(\d)元/天, text) if m: return float(m.group(1)) * 22, float(m.group(2)) * 22 # 匹配单个数值 15k m re.match(r(\d\.?\d*)[k千], text) if m: val float(m.group(1)) * 1000 return val, val return None, None逻辑说明按优先级依次匹配四种格式元/天按每月 22 个工作日折算成月薪。返回的是区间下界和上界后面可以取中位数做聚合。面议直接返回None在分析阶段过滤掉。参数说明折算天数 22 是常见做法如果目标岗位多是兼职或实习可以改成 20 或 26。正则里的\d\.?\d*兼容整数和小数比如1.5万和15k都能匹配。3.2 城市名归一化与学历/经验字段映射城市字段常见的问题是北京·朝阳区、上海-浦东新区这种带区域后缀的写法。归一化就是只保留城市主体def normalize_city(city_text): if not city_text: return 未知 for sep in [·, -, , ]: if sep in city_text: city_text city_text.split(sep)[0] city_text city_text.replace(市, ).strip() return city_text if city_text else 未知 EDU_MAP { 不限: 不限, 学历不限: 不限, 大专: 大专, 本科: 本科, 硕士: 硕士, 博士: 博士, } def normalize_education(edu_text): if not edu_text: return 未知 for key in EDU_MAP: if key in edu_text: return EDU_MAP[key] return 其他逻辑说明城市归一化先按分隔符切分取第一段再去掉市字。学历映射用包含判断因为原文可能是本科及以上或统招本科。经验字段类似处理把3-5年、经验不限映射成有序的类别方便后面做分组统计。参数说明EDU_MAP的键按实际数据里出现的写法补充不要只写标准名称。如果数据里出现中专、高中也要加进去否则会全部落到其他。3.3 用 pandas 做聚合哪些维度值得出图清洗完之后用 pandas 从数据库读数据并做聚合import pandas as pd from sqlalchemy import create_engine engine create_engine(sqlite:///jobs.db) df pd.read_sql(SELECT * FROM job_posts, engine) df[salary_low], df[salary_high] zip(*df[salary_text].apply(parse_salary)) df[salary_mid] (df[salary_low] df[salary_high]) / 2 df[city] df[city].apply(normalize_city) df[education] df[education].apply(normalize_education) # 各城市平均薪资 city_salary df.dropna(subset[salary_mid]).groupby(city)[salary_mid].agg([mean, count]) city_salary city_salary[city_salary[count] 10].sort_values(mean, ascendingFalse) # 学历分布 edu_dist df[education].value_counts() # 经验要求与薪资的关系 exp_salary df.dropna(subset[salary_mid]).groupby(experience)[salary_mid].mean()逻辑说明zip(*...)把 apply 返回的元组列表拆成两列。count 10是过滤样本太少的城市否则图表上会出现一堆只有一两条数据的城市没有解释力。经验与薪资的关系是毕业设计里比较有说服力的分析点能看出薪资随经验增长的梯度。参数说明salary_mid用区间中值代替具体薪资是常见做法。如果想让分析更细可以按salary_low和salary_high分别出图但中值更适合做趋势对比。4. 可视化落地ECharts 与 Flask 怎么配合4.1 后端用 Flask 提供 JSON 接口可视化不建议直接把 DataFrame 塞进模板而是后端出 JSON、前端用 ECharts 渲染。Flask 这边只需要几个路由from flask import Flask, jsonify, render_template import pandas as pd from sqlalchemy import create_engine app Flask(__name__) engine create_engine(sqlite:///jobs.db) app.route(/) def index(): return render_template(dashboard.html) app.route(/api/city_salary) def api_city_salary(): df pd.read_sql(SELECT city, salary_text FROM job_posts, engine) df[salary_low], df[salary_high] zip(*df[salary_text].apply(parse_salary)) df[salary_mid] (df[salary_low] df[salary_high]) / 2 df[city] df[city].apply(normalize_city) result df.dropna(subset[salary_mid]).groupby(city)[salary_mid].mean() result result.sort_values(ascendingFalse).head(15) return jsonify({cities: result.index.tolist(), salaries: result.values.round(0).tolist()}) app.route(/api/edu_dist) def api_edu_dist(): df pd.read_sql(SELECT education FROM job_posts, engine) df[education] df[education].apply(normalize_education) counts df[education].value_counts() return jsonify({labels: counts.index.tolist(), values: counts.values.tolist()})逻辑说明每个接口只负责一个图表的数据前端按需请求。round(0)把薪资取整减少前端显示的小数位。head(15)限制城市数量避免柱状图太挤。参数说明如果数据量大可以在接口层加缓存比如用functools.lru_cache或 Flask-Caching。开发阶段不用加但演示时如果每次刷新都查全表响应会明显变慢。4.2 前端 ECharts 配置柱状图与饼图的关键参数前端页面用 ECharts 的 CDN 引入两个图表的配置如下// 城市薪资柱状图 fetch(/api/city_salary) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(cityChart)); chart.setOption({ title: { text: 各城市平均薪资 Top15 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.cities, axisLabel: { rotate: 45 } }, yAxis: { type: value, name: 月薪(元) }, series: [{ type: bar, data: data.salaries, itemStyle: { color: #5470c6 } }] }); }); // 学历分布饼图 fetch(/api/edu_dist) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(eduChart)); chart.setOption({ title: { text: 学历要求分布, left: center }, tooltip: { trigger: item, formatter: {b}: {c} ({d}%) }, series: [{ type: pie, radius: [40%, 70%], data: data.labels.map((label, i) ({ name: label, value: data.values[i] })) }] }); });逻辑说明柱状图的axisLabel.rotate: 45是必须的城市名多了不旋转会重叠。饼图用环形radius设成数组比实心饼图更清爽formatter里{d}%自动算百分比。参数说明color可以换成自己项目的主题色。如果要做响应式加window.addEventListener(resize, () chart.resize())。4.3 把图表嵌进一个页面布局与数据刷新一个 dashboard 页面放四到六个图表比较合适太多会显得杂。布局用简单的 CSS Grid 就行每个图表一个卡片标题写清楚分析维度。数据刷新不需要做实时毕业设计演示时手动刷新页面即可。如果想让页面看起来更完整可以在顶部加一行汇总指标总岗位数、覆盖城市数、平均薪资、最高薪资城市。这些数据从接口里顺带返回就行不用单独建路由。5. 避坑与排查爬虫和可视化里最容易翻车的几个点5.1 请求被拒或返回空页面现象程序不报错但解析出来是空列表或者状态码直接 403。原因headers 里缺少 Referer 或 Cookie或者请求频率太高被临时限制。解决先手动打开目标页面用浏览器开发者工具复制完整的请求头把 Referer 和 Cookie 补上。如果还是不行把请求间隔调到 3 秒以上并且换一个 User-Agent。注意不要用同一个 IP 短时间大量请求这是最常见的翻车原因。5.2 薪资解析返回 None 的比例过高现象清洗后发现超过一半的薪资字段是空值图表数据量不够。原因正则没有覆盖目标网站的实际薪资写法比如15K-25KK 在前面或15-25K·13薪带后缀。解决先把所有解析失败的薪资文本去重打印出来看实际格式再补正则。·13薪这种后缀可以在解析前用split(·)[0]去掉。不要凭想象写正则一定要看真实数据。5.3 SQLAlchemy 写入时主键冲突或重复数据现象第二次运行爬虫后数据库里出现重复岗位或者报 IntegrityError。原因去重逻辑只判断了部分字段或者并发写入时没有加锁。解决去重字段要覆盖能唯一标识一条岗位的组合通常至少三个字段。如果还是重复可以在数据库层加唯一索引UniqueConstraint(job_title, company_name, city)。写入前先查再插的方式在数据量大时效率低可以改成批量查询已有记录再过滤。5.4 ECharts 图表不显示或显示空白现象页面加载了但图表区域是空白的控制台没有明显报错。原因echarts.init执行时容器还没有宽度和高度或者 CDN 加载失败。解决确保图表容器的 div 有明确的width和height不要用百分比高度但父元素没有高度。CDN 建议用国内可访问的地址或者直接下载 echarts.min.js 放到 static 目录。如果控制台报echarts is not defined就是 CDN 没加载成功。5.5 中文显示乱码或字体不一致现象图表里的中文变成方框或者页面中文和图表中文大小不一致。原因ECharts 默认字体不含中文或者页面编码不是 UTF-8。解决在 ECharts 的textStyle里指定fontFamily: Microsoft YaHei, sans-serif。Flask 模板文件保存为 UTF-8 编码HTML 里加meta charsetUTF-8。数据库连接字符串里如果用的是 MySQL加上?charsetutf8mb4。6. 进阶技巧让这套毕业设计从能跑变成能讲6.1 用词云补充岗位技能关键词岗位详情里的技能关键词是很好的加分项。用 jieba 分词加 WordCloud 生成词云能直观看出目标岗位最常要求的技能import jieba from wordcloud import WordCloud import matplotlib.pyplot as plt def generate_wordcloud(texts, output_pathwordcloud.png): merged .join(texts) words jieba.cut(merged) filtered [w for w in words if len(w) 1 and w not in STOPWORDS] wc WordCloud( font_pathC:/Windows/Fonts/msyh.ttc, width1200, height600, background_colorwhite, max_words100 ) wc.generate( .join(filtered)) wc.to_file(output_path)逻辑说明font_path必须指定中文字体路径否则中文会显示成方框。STOPWORDS里放岗位职责、任职要求、负责这类无意义词。max_words100控制词云密度太多会看不清。参数说明width和height按展示位置调整一般 1200x600 够用。如果词云里出现大量无意义的高频词就往STOPWORDS里继续加。6.2 用 Flask 加一个筛选交互静态图表只能看整体加一个城市或学历的下拉筛选演示效果会好很多。实现方式很简单前端下拉框变化时把选中的值作为查询参数传给接口后端在 SQL 里加WHERE条件。不需要上复杂的框架原生fetch加addEventListener就够了。6.3 验证数据质量的两个习惯第一个习惯是每次爬完先跑一遍df.describe()和df.isnull().sum()看数值范围是否合理、空值集中在哪些字段。第二个习惯是随机抽 10 条记录和原页面比对确认字段没有错位。这两个动作花不了几分钟但能避免后面分析阶段发现数据对不上的尴尬。我自己做这类项目时最深的教训是爬虫代码写得再漂亮如果字段定义和清洗规则没提前想清楚后面返工的时间会远超预期。所以每次都是先把目标字段表和薪资格式样本整理出来再动手写代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表