简介这是一份基于Python的招聘网站爬虫及可视化系统的毕业设计源码包开发环境采用Pycharm与Python3.7项目通过Requests库对招聘站点进行数据采集将岗位信息写入MySQL数据库再借助Echarts在Web前端生成饼图、柱状图、折线图、扇形图等综合分析图表完成从数据采集、存储到可视化展示的完整流程。代码已经完整运行通过答辩评审平均分达96分适合计算机及相关专业学生在毕业设计、课程设计或项目实训中参考也可在此基础上扩展地域薪资分析、职位关键词统计等新功能。压缩包共含59个文件核心部分包括Python爬虫模块、Flask后端代码、SQL建表与数据脚本、HTML/CSS/JS前端资源、页面效果图以及演示PPT整体大小约10.34MB资源目录划分明确便于对照查看采集模块、数据库脚本和前端图表之间的调用关系演示PPT也可直接用于答辩汇报。目前已有919人学习下载适合具备一定Python基础、希望快速上手完整爬虫与可视化项目的中级学习者用来梳清项目架构、理解接口设计并完成二次开发。1. 基于Python的招聘网站爬虫及可视化毕业设计怎么从零落地「基于Python的招聘网站爬虫及可视化」这个题目拆开看是三条独立的技术链路。爬虫解决“怎么拿数据”可视化解决“怎么展示数据”毕业设计决定“你怎么把这两件事完整讲出来”。做这类项目最常见的错误是直接照搬现成源码跑通界面就宣布完成。实际答辩时老师追问最多的反而在技术细节为什么选requests不选Scrapy薪资字段怎么清洗成数值增量爬取怎么设计封IP了怎么办。从零搭建时先把数据链路理清楚HTTP请求、HTML解析、结构化存储、接口出数、前端渲染。顺着这条线把每个环节做完才是一个能演示源码的完整毕业设计。适合理清全流程的在校生也适合想补全爬虫与可视化技能树的工程师。2. 爬虫选型与请求参数调优requests、Scrapy怎么定2.1 requests单线程与Scrapy框架的取舍招聘网站的爬取规模决定requests一开始就是够用的方案。requests直接、透明一个get、一次解析、一次存储逻辑全在眼前。Scrapy要启动多个爬虫得建工程结构写pipelines和middlewares对单站点的爬取任务来说多出来的工程化改造成本高于收益。对比可以看得更清楚。requests配合多线程数据量低于10万条时爬取效率已经够看Scrapy的异步引擎在100万条以上才显现优势。从维护角度看requests脚本打包成函数后调试时能在IDE里断点看每一步的返回值Scrapy的callback链在断点时上下文较绕新手容易迷路。做毕设时目标站点只有两三个优先requests加缓存和重试目标站点有明确反爬策略、需要上代理池和复杂中间件时再考虑Scrapy。对比项requests 多线程Scrapy学习成本低从零可写中高需理解引擎与中间件吞吐量中受GIL和线程数限制高异步IO由框架调度断点调试直接符合直觉回调链略绕适合规模1万10万条页面10万条以上或分布式反爬扩展手写重试/代理切换内置middleware扩展成体系提示答辩里只要讲清楚“量级决定选型”这一句追问基本能接住。2.2 headers、cookies与请求频率的7个参数请求参数不是“能用就行”。招聘站点大多有基础反爬靠请求头识别和频率统计。招聘站比普通站点更在意来源页面Referer错了直接拒连。所以先构造一个最小可用的带参请求再逐步加参数。import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.liepin.com/, Accept-Language: zh-CN,zh;q0.9, } resp requests.get( https://www.liepin.com/zhaopin/?keyPython, headersheaders, timeout(5, 15), verifyFalse, ) print(resp.status_code, resp.url)timeout元组第一项是连接超时第二项是读取超时分别设5秒和15秒比单一数值更贴合网络波动。verifyFalse跳过SSL证书校验只在本地开发用。User-Agent要模拟真实浏览器requests内置的python-requests/2.x那个UA一眼就会被识别。Referer字段在招聘网站上校验很常见必须填站内页面。7个参数依次是UA、Referer、cookie、timeout、proxies、allow_redirects、params。params用于固定查询条件比如城市、页码、关键词cookie要单独管理登录后拿到的值放在环境变量里别硬编码进源码。配合requests.Session可以保持会话s requests.Session() s.headers.update(headers) resp s.get(url, params{key: Python, page: 2})Session对象复用底层TCP连接翻页时能显著减少握手次数。页面请求间隔建议给sleep随机值0.5到2秒之间——固定间隔更容易被频率统计抓规律。2.3 解析策略BeautifulSoup与XPath的选择拿到HTML之后的工序是定位数据节点。BeautifulSoup配lxml解析器对招聘页这种结构相对松散的页面很直观但效率和稳定性略低于直接XPath。我的习惯是能用XPath就用XPath因为招聘网站的li/a/i标签嵌套深时find_all要写三层循环XPath一条路径搞定。from lxml import html doc html.fromstring(resp.text) items doc.xpath(//ul[contains(class,job-list)]/li) for li in items: name li.xpath(.//div[contains(class,job-name)]/text()) salary li.xpath(.//span[contains(class,salary)]/text()) company li.xpath(.//a[contains(class,company-name)]/title)XPath提取逻辑是先定位职位列表的ul节点再对每个li做二次查找。相对路径开头的.//表示从当前节点向下查不每次都从根节点重走。定位class用contains而不是等值判断因为页面class名经常带空格或追加其他类名。text()取的是直接文本目标节点内有嵌套span时要用.//text()拼接所有子文本职位名里很常见。解析结果任何一项为空值时不要直接append进列表先做字段校验缺失的填空字符串或跳过异常节点保证后续入库字段数量一致。3. 数据存储与清洗MySQL入库与字段设计3.1 招聘岗位表怎么设计爬虫抓下来的字段常见的有职位、公司、薪资、城市、经验、学历、发布日期。直接按直觉建表后面做可视化时口径就对不上。两个常见的坑薪资是“15K-25K·14薪”这种字符串经验是“3-5年”不拆开没法聚合。另一个是company字段里可能带城市前缀按城市维度统计就会乱。设计表时给原始字段留一列raw_json清洗后的结构化字段放常规列。原始数据不丢出问题时能回溯。建表SQLCREATE TABLE job ( id BIGINT AUTO_INCREMENT PRIMARY KEY, job_name VARCHAR(128) NOT NULL, company VARCHAR(200) NOT NULL, city VARCHAR(64), salary_low INT, -- 月薪下限单位K salary_high INT, -- 月薪上限单位K salary_months TINYINT, -- 一年发多少个月薪 experience_low TINYINT, -- 经验下限单位年 experience_high TINYINT, education VARCHAR(32), publish_date DATE, raw_json JSON, -- 原始结果便于回溯 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_job_city_name (job_name, company, city, publish_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三个int字段分别是下限、上限、发放月数。“15K-25K·14薪”为例15进salary_low25进salary_high14进salary_months。经验字段同理转成experience_low和experience_high两个数值聚合时用均值或区间。唯一索引放在职位、公司、城市、发布日期四列组合上同一天重复抓取不会产生重复记录。raw_json存节点dict的json.dumps结果容量不大用于排查。3.2 数据清洗薪资区间拆解与去重清洗放在入库前用pandas处理顺手还能做可视化前的聚合。对salary字符串的拆解正则比split稳因为格式不统一有的写“6-8K”有的写“10k-15K”还有“面议”。import pandas as pd import re df pd.DataFrame(records) pat re.compile(r(\d)\s*[-—]\s*(\d), re.IGNORECASE) def parse_salary(s): s str(s) m pat.search(s) if not m: return None, None, 0 low int(m.group(1)) high int(m.group(2)) months re.search(r(\d{2})\s*薪, s) return low, high, int(months.group(1)) if months else 12 df[[salary_low, salary_high, salary_months]] df[salary].apply( lambda x: pd.Series(parse_salary(x)) )正则里的[-—]同时匹配连字符和中文长横线这是招聘文本最常见的变体。re.IGNORECASE处理“K”和“k”。month的识别放到薪资字符串后面不单独去匹配“14薪”因为有些站点会写成“14薪年终奖”。面议、薪资为空的记录salary_low和salary_high保持None可视化聚合时用fillna(0)。去重依赖唯一索引批量写入时用INSERT IGNORE比先select再决定insert省一次网络往返。入库之前还要清理HTML实体nbsp会残留在职位名里在XPath提取后顺手做一次html.unescape和strip。清洗完再校验一条salary_low大于salary_high时交换值这类脏数据在真实页面上占比不低。3.3 连接池与批量插入单插一条肯定慢。用executemany批量插入每批500条配合单连接复用爬1万条也就几十秒。import pymysql from pymysql.cursors import DictCursor conn pymysql.connect( host127.0.0.1, userroot, password****, databasejob_db, charsetutf8mb4, cursorclassDictCursor, autocommitTrue, ) sql INSERT IGNORE INTO job (job_name, company, city, salary_low, salary_high, salary_months, experience_low, experience_high, education, publish_date, raw_json) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) conn.cursor().executemany(sql, batch)批量插入参数按顺序对SQL里的占位符字典转元组时字段顺序不能乱。pymysql连接参数里autocommitTrue省去每批commit。字符集指定utf8mb4职位名里会有emoji或生僻字用utf8会报错。batch大小是经验值太大超过max_allowed_packet太小浪费网络往返。报错时把每批size改成100重试不需要改MySQL服务端参数。Windows上跑毕设pymysql是纯Python包没有编译问题mysqlclient在Windows上装起来环境变量麻烦。数据库连接信息放config.py集中管理不要在爬虫文件里散着写。4. 可视化大屏FlaskECharts的数据呈现4.1 选型为什么用Flask而不是Django可视化部分要回答的是“哪个框架能最快把数据变成图”。Flask的定位正好匹配一个API接口、一个前端页面、一套JSON数据。Django自带admin、ORM、中间件机制对只做一页看板的毕设来说项目结构偏重模板和静态文件组织也要多理解一层。Flask只需要一个app.py和templates下的index.html比Django的manage.pysettingsurls路由体系轻一级。爬虫项目数据访问用的是pymysql直连不太需要ORM管理模型Django的ORM优势用不上。Flask配一个蓝图把数据接口独立出来前后端联调直接对JSON格式开发效率最高。接口只负责出数前端用ECharts的ajax拉JSON再setOption。这样后端不掺和前端渲染答辩时打开浏览器开发者工具能直接看接口返回数据结构证明链路是通的。4.2 ECharts图表与数据接口设计三个图表城市岗位数量柱状图、薪资区间分布饼图、岗位需求趋势折线图。接口统一返回JSON字段名和前端对齐。from flask import Flask, jsonify import pymysql app Flask(__name__) app.route(/api/job/city) def job_city(): sql (SELECT city, COUNT(*) AS cnt FROM job WHERE publish_date DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY city ORDER BY cnt DESC LIMIT 12) cur conn.cursor() cur.execute(sql) rows cur.fetchall() return jsonify({code: 0, data: rows})接口关键是SQL里的时间窗口。如果不对发布时间做限制累计历史数据会冲淡近期趋势柱状图永远是头部城市看不出变化。DATE_SUB配合CURDATE形成滚动窗口答辩时可以现场改7天参数展示不同时间窗口这是加分项。返回结构固定为code/data两个字段前端判断逻辑简单。前端页面templates/index.html里初始化图表fetch(/api/job/city) .then(res res.json()) .then(res { const cities res.data.map(d d.city); const counts res.data.map(d d.cnt); chart.setOption({ xAxis: { data: cities }, series: [{ type: bar, data: counts }] }); });这段JS三步拉接口、map抽出两个数组、setOption灌进图表。不要JSON里所有字段直接塞seriesECharts对格式有要求。城市名做x轴类目数量做y轴值。接口返回空数组时前端要兜底显示“暂无数据”否则ECharts白屏。4.3 大屏布局与刷新策略大屏用CSS Grid分三列左右两列放图表中间列放核心指标数字。核心指标用HTML大字号呈现不画图反而醒目。整体背景深色渐变适配1920x1080即可不过度做自适应。图表组件抽成initChart函数挂到window上方便定时器重绘。数据刷新做两层前端每60秒轮询接口后端不缓存。演示时新爬的数据会自动更新到大屏。注意轮询时先dispose旧chart实例再重新init否则多次setOption会叠加动画和事件监听用ECharts的clear加setOption更轻。演示PPT里放一张数据链路图把“爬虫→MySQL→接口→图表”画出来比堆截图有技术含量。提示大屏刷新间隔不要低于30秒更短的时间既给数据库增加额外压力也看不出业务价值。5. 反爬应对与增量爬取的落地技巧5.1 被封IP后的日志判断与重试机制反爬问题是答辩环节出现频率最高的追问。遇到403、验证码跳转、返回空列表日志要能区分出“网络异常”和“被拦截”。请求后检查status_code和响应文本长度文本里出现“请完成验证”或“访问过于频繁”关键字时按反爬处理而不是普通重试。给请求函数包一层重试最多重试3次指数退避。指数退避初始间隔短重试几次后快速拉长避免反复撞枪口。重试前切换代理或UA没有代理池也能随机换UA能缓解一部分基于UA特征的封禁。import time import random def fetch_with_retry(url, session, max_retries3): for i in range(max_retries): try: resp session.get(url, timeout(5, 15)) if resp.status_code 200 and len(resp.text) 500: return resp if 验证 in resp.text: raise RuntimeError(captcha) except Exception as e: wait 2 ** i random.uniform(0, 1) time.sleep(wait) rotate_ua(session) return Nonewait计算用2的幂次加随机抖动比固定sleep更贴近真实请求规律。rotate_ua负责重新设置session的UA从一个UA列表随机取。try范围覆盖连接超时、HTTP错误和验证码判断统一走到同一套退避逻辑。5.2 增量爬取任务的边界处理增量爬取一是降低对目标站点的压力二是控制自己的数据量。实现方式用时间戳唯一索引双重约束。每次爬取开始记录start_time插入时把start_time写进created_at字段下次启动时只爬最近一天的数据重复条目由唯一索引INSERT IGNORE掉。更精确的做法是维护job_last_id从上一轮最大id继续翻页但招聘网站的列表排序不完全按id跳过窗口可能漏数据。我一般用“按发布时间倒序翻页只取发布7天内的数据”这样既覆盖新增又不会无限增长。数据量超过50万条时再引入布隆过滤器做URL去重毕设阶段用不上但答辩谈方案时提到布隆过滤器的误判率边界会是有力的信息点。增量任务用APScheduler做定时调度每30分钟跑一次日志写到文件。调度器cron表达式用“*/30 * * * *”直观。注意调度任务里不要让数据库连接长期空闲每次任务开始新建连接、结束关闭用上下文管理器包住保证连接不泄漏。这个做法在演示时能连续跑几天不崩是工程上很加分的细节。演示环境把config.py里的连接池容量调到5跑完一次任务主动commit并print调度时间戳现场能直接看到调度日志在滚动。本文还有配套的精品资源点击获取
