招聘数据可视化闭环系统:双源爬虫+地理编码+热力图
2026/9/15 16:30:34 网站建设 项目流程

简介:本资源是一套完整的基于Python的招聘数据可视化分析系统毕业设计/课程设计项目,面向计算机、数据科学及相关专业本科生,解决招聘数据采集、清洗、分析与可视化落地的实际问题。压缩包共59个文件,含9个核心Python源码(如爬虫handle_crawl_tencent.py、Flask后端run.py)、2个MySQL建表与数据脚本、1个PPTX演示文稿、7张PNG/JPG图表及静态资源(JS/CSS/HTML),整体9.93MB,结构清晰分为tencentflask后端、mysql数据库、tencent_spider爬虫、data_analysis分析模块及文档说明。已有50人学习下载,读者可直接运行Flask服务,复现从腾讯/前程无忧双源爬取、坐标解析、数据入库到多维度图表展示的全流程;配套演示文稿详解系统架构与交互逻辑,README.md与requirements.txt保障环境快速部署,特别适合课程设计答辩与毕业设计实战参考。

1. 这不是又一个“爬完就扔”的招聘爬虫——它是一套可复用、可验证、带坐标映射与双源比对的招聘数据可视化闭环系统

你见过多少个标着“Python招聘分析”的毕业设计?点开一看,80%停在requests.get()+pandas.read_csv()+plt.show()三板斧,数据是静态CSV,城市没经纬度,薪资区间硬编码,地图热力图根本跑不起来。而这个tencentflask项目,从handle_crawl_tencent.pyhandle_crawl_51.py双通道采集开始,就埋了真实工程逻辑:它用getCoord.py主动解析中国各城市坐标.txt实现地理编码对齐,用create_tencent_tables.py生成带索引、外键、时间分区的 MySQL 表结构,再通过handle_insert_data.py做字段类型校验与空值填充——不是把原始JSON塞进数据库就完事。它真正解决的是「数据从网页到可分析坐标系」的断层问题。适合正在做课程设计、需要展示完整ETL链路与空间可视化能力的学生;也适合想快速搭建本地招聘趋势看板的HRBP或校招负责人——所有模块都已解耦,run.py启动即用,static/下的前端资源已预编译,无需额外构建。


2. 数据采集双通道实现:腾讯招聘与前程无忧的差异化反爬策略与字段对齐

2.1 腾讯招聘页面结构解析与动态参数提取

腾讯招聘官网(career.tencent.com)采用前后端分离架构,职位列表由 AJAX 接口返回 JSON 数据,而非静态 HTML。handle_crawl_tencent.py的核心在于逆向分析其请求签名机制。关键代码段如下:

# handle_crawl_tencent.py 第42行起 def build_tencent_url(page_num, keyword="python"): base_url = "https://careers.tencent.com/tencentjob/api/post/Query" # 时间戳与随机数构成签名参数 t = int(time.time() * 1000) r = random.randint(100000, 999999) # 签名算法为 MD5(base_url + str(t) + str(r) + "1234567890") sign = hashlib.md5(f"{base_url}{t}{r}1234567890".encode()).hexdigest() params = { "timestamp": t, "countryId": "", "cityId": "", "bgId": "", "productId": "", "categoryId": "", "parentCategoryId": "", "attrId": "", "keyword": keyword, "pageIndex": page_num, "pageSize": "10", "language": "zh-cn", "area": "cn", "sign": sign, "r": r } return base_url + "?" + urllib.parse.urlencode(params)

提示:该签名密钥"1234567890"是硬编码在源码中的,实际部署时应移至环境变量。若签名失效,需抓包对比Referer头(必须为https://careers.tencent.com/)与User-Agent(建议使用 Chrome 115+ 的 UA 字符串),否则返回403

2.2 前程无忧反爬应对:Session维持与字段标准化映射

handle_crawl_51.py面对的是前程无忧(51job.com)更复杂的 DOM 渲染与 JS 混淆。它不依赖 Selenium,而是通过模拟登录态 + 关键词搜索接口组合实现稳定采集:

# handle_crawl_51.py 第68行 session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", "Referer": "https://www.51job.com/" }) # 先访问首页触发 cookie 初始化 session.get("https://www.51job.com/", timeout=10) # 构造搜索URL(关键词编码为GBK,非UTF-8) search_url = f"https://search.51job.com/list/000000,000000,0000,00,9,99,{urllib.parse.quote(keyword.encode('gbk'))},2,1.html" response = session.get(search_url, timeout=15) # 使用 lxml 解析,避开正则匹配JS渲染内容 tree = etree.HTML(response.text) jobs = tree.xpath('//div[@id="resultList"]//div[@class="el"]') for job in jobs: title = job.xpath('.//p[@class="t1"]/span/a/text()')[0].strip() if job.xpath('.//p[@class="t1"]/span/a/text()') else "" company = job.xpath('.//span[@class="t2"]/a/text()')[0].strip() if job.xpath('.//span[@class="t2"]/a/text()') else "" # 薪资字段存在多种格式:"1.5-2万/月"、"面议"、"15K-25K",统一归一化为"元/月"数值区间 salary_raw = job.xpath('.//span[@class="t4"]/text()')[0].strip() if job.xpath('.//span[@class="t4"]/text()') else "" salary_min, salary_max = parse_salary(salary_raw) # 调用自定义函数
2.2.1 薪资字段标准化函数parse_salary()
def parse_salary(s): if not s or "面议" in s: return 0, 0 # 匹配数字+单位组合,支持"1.5-2万/月"、"15K-25K"、"8000-12000元/月" pattern = r'(\d+\.?\d*)[-~—](\d+\.?\d*)(?:万|K|k|元)?[/每]?(?:月|年|天)' match = re.search(pattern, s) if match: low, high = float(match.group(1)), float(match.group(2)) # 统一转为"元/月":万→×10000,K→×1000,年→÷12 if '万' in s: low, high = low * 10000, high * 10000 elif 'K' in s or 'k' in s: low, high = low * 1000, high * 1000 if '年' in s: low, high = low / 12, high / 12 return int(low), int(high) # 单值如"15K"处理 single_pattern = r'(\d+\.?\d*)(?:万|K|k|元)?[/每]?(?:月|年|天)' single_match = re.search(single_pattern, s) if single_match: val = float(single_match.group(1)) if '万' in s: val *= 10000 elif 'K' in s or 'k' in s: val *= 1000 if '年' in s: val /= 12 return int(val * 0.8), int(val * 1.2) # 估算区间 return 0, 0

2.3 双源数据字段对齐表:解决平台间语义鸿沟

腾讯字段名51job字段名标准化字段名类型说明
RecruitPostName职位名称job_titleVARCHAR(255)统一去空格、去"急聘"/"校招"等修饰词
LocationName工作地点cityVARCHAR(50)通过getCoord.py映射为标准城市名(如"北京"→"北京市")
Requirement职位描述job_descTEXT清洗HTML标签,保留换行与核心技能关键词
LastUpdateTime发布时间publish_dateDATE解析为YYYY-MM-DD格式,忽略时分秒
Salary薪资salary_min,salary_maxINT单位:元/月,已归一化

注意citys.txt文件中存储了51job原始城市名与标准名的映射关系(如"北京""北京市"),而中国各城市坐标.txt提供标准名对应的(lat, lng)getCoord.py的作用正是将爬取的城市字符串,经两次查表后输出经纬度,为后续地图可视化打下基础。


3. MySQL数据建模与Flask后端服务:从原始数据到可查询API的落地路径

3.1 带业务约束的MySQL表结构设计

mysql数据库/job51_data2.sql并非简单建表,而是体现了招聘数据特有的业务规则。以tencent_job_post表为例:

CREATE TABLE `tencent_job_post` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `post_id` VARCHAR(64) NOT NULL COMMENT '腾讯职位唯一ID', `job_title` VARCHAR(255) NOT NULL DEFAULT '', `city` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '标准城市名', `lat` DECIMAL(10,8) DEFAULT NULL COMMENT '纬度', `lng` DECIMAL(11,8) DEFAULT NULL COMMENT '经度', `salary_min` INT(11) DEFAULT 0 COMMENT '月薪下限(元)', `salary_max` INT(11) DEFAULT 0 COMMENT '月薪上限(元)', `experience` VARCHAR(50) DEFAULT '' COMMENT '经验要求', `education` VARCHAR(50) DEFAULT '' COMMENT '学历要求', `publish_date` DATE DEFAULT NULL, `crawl_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `source` ENUM('tencent','51job') NOT NULL DEFAULT 'tencent', PRIMARY KEY (`id`), UNIQUE KEY `uk_post_id_source` (`post_id`,`source`), KEY `idx_city` (`city`), KEY `idx_publish_date` (`publish_date`), KEY `idx_salary_range` (`salary_min`,`salary_max`), KEY `idx_lat_lng` (`lat`,`lng`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='腾讯招聘职位主表';
3.1.1 关键设计意图说明
  • UNIQUE KEY uk_post_id_source:防止同一职位在不同爬取周期重复入库,source字段区分来源;
  • KEY idx_salary_range:支撑「薪资分布直方图」查询,避免全表扫描;
  • KEY idx_lat_lng:为ST_Distance_Sphere()地理距离计算提供索引支持(见4.2节);
  • ENGINE=InnoDB:保证事务一致性,handle_insert_data.py中的批量插入会启用INSERT ... ON DUPLICATE KEY UPDATE

3.2 Flask路由与数据库交互:RESTful API设计实践

tencentflask/app.py定义了核心数据接口,以/api/jobs为例,支持多条件组合查询:

# tencentflask/app.py 第127行 @app.route('/api/jobs', methods=['GET']) def get_jobs(): city = request.args.get('city', '').strip() min_salary = request.args.get('min_salary', type=int, default=0) max_salary = request.args.get('max_salary', type=int, default=9999999) keyword = request.args.get('keyword', '').strip() query = JobPost.query if city: query = query.filter(JobPost.city == city) if min_salary > 0: query = query.filter(JobPost.salary_max >= min_salary) if max_salary < 9999999: query = query.filter(JobPost.salary_min <= max_salary) if keyword: # 使用MySQL全文索引提升标题/描述检索效率 query = query.filter(or_( JobPost.job_title.like(f'%{keyword}%'), JobPost.job_desc.like(f'%{keyword}%') )) jobs = query.order_by(JobPost.publish_date.desc()).limit(50).all() return jsonify([{ 'id': j.id, 'job_title': j.job_title, 'city': j.city, 'salary_min': j.salary_min, 'salary_max': j.salary_max, 'publish_date': j.publish_date.strftime('%Y-%m-%d') if j.publish_date else None } for j in jobs])
3.2.1 数据库连接配置与连接池管理

requirements.txt中明确指定PyMySQL==1.1.0SQLAlchemy==1.4.49tencentflask/config.py设置连接池参数:

# tencentflask/config.py SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:password@localhost:3306/recruit_db?charset=utf8mb4' SQLALCHEMY_POOL_SIZE = 10 SQLALCHEMY_MAX_OVERFLOW = 20 SQLALCHEMY_POOL_RECYCLE = 3600 SQLALCHEMY_TRACK_MODIFICATIONS = False
  • POOL_SIZE=10:保持10个持久连接,避免频繁创建销毁开销;
  • POOL_RECYCLE=3600:强制每小时重置连接,防止MySQLwait_timeout断连;
  • charset=utf8mb4:确保 emoji 与生僻字(如“碁”、“ manoeuvre”)正确存储。

3.3 启动服务与环境验证:三步完成本地部署

  1. 初始化数据库
    执行mysql -u root -p < mysql数据库/job51_data2.sql创建库表结构;

  2. 安装依赖并设置环境变量

    pip install -r requirements.txt export FLASK_APP=tencentflask/app.py export FLASK_ENV=development
  3. 运行服务并验证API

    flask run --host=0.0.0.0 --port=5000 # 浏览器访问 http://127.0.0.1:5000/api/jobs?city=北京市&min_salary=15000 # 应返回JSON格式职位列表,状态码200

提示:若出现pymysql.err.OperationalError: (1045, "Access denied"),检查config.py中数据库用户名密码是否与本地MySQL一致;若报ModuleNotFoundError: No module named 'tencentflask',确认当前目录为项目根目录(含tencentflask/子文件夹)。


4. 地理坐标映射与热力图生成:让招聘数据真正“落”在地图上

4.1getCoord.py的城市标准化与坐标注入流程

中国各城市坐标.txt是一个制表符分隔的文本文件,格式为:

北京市 39.9042 116.4074 上海市 31.2304 121.4737 广州市 23.1291 113.2644 ...

getCoord.py的核心逻辑是建立两级映射字典:

# getCoord.py 第22行 def load_city_coord_map(): coord_map = {} with open('中国各城市坐标.txt', 'r', encoding='utf-8') as f: for line in f: parts = line.strip().split('\t') if len(parts) == 3: city_name, lat, lng = parts[0], float(parts[1]), float(parts[2]) coord_map[city_name] = (lat, lng) return coord_map def standardize_city_name(raw_city): # 处理常见别名:如"北京"→"北京市","上海"→"上海市" alias_map = { '北京': '北京市', '上海': '上海市', '广州': '广州市', '深圳': '深圳市', '杭州': '杭州市', '成都': '成都市', '武汉': '武汉市', '西安': '西安市' } return alias_map.get(raw_city.strip(), raw_city.strip() + '市') # 在 handle_insert_data.py 中调用 coord_map = load_city_coord_map() for record in batch_records: std_city = standardize_city_name(record['city']) if std_city in coord_map: record['lat'], record['lng'] = coord_map[std_city] else: record['lat'], record['lng'] = 0, 0 # 默认坐标,便于后续排查
4.1.1 坐标缺失处理策略

std_city不在coord_map中时,不直接丢弃记录,而是写入(0,0)并记录日志:

# handle_insert_data.py 第89行 if std_city not in coord_map: app.logger.warning(f"City '{std_city}' not found in coordinate map, using (0,0)") record['lat'], record['lng'] = 0, 0

这样可在后续SQL中快速定位异常城市:SELECT DISTINCT city FROM tencent_job_post WHERE lat = 0 AND lng = 0;

4.2 基于GeoPandas与Folium的热力图生成

tex.py是可视化核心脚本,它不依赖前端JavaScript,而是生成静态HTML地图:

# tex.py 第35行 import geopandas as gpd import folium from folium.plugins import HeatMap # 从MySQL读取带坐标的职位数据 df = pd.read_sql(""" SELECT city, lat, lng, salary_max FROM tencent_job_post WHERE lat != 0 AND lng != 0 AND salary_max > 0 """, con=engine) # 创建GeoDataFrame gdf = gpd.GeoDataFrame( df, geometry=gpd.points_from_xy(df.lng, df.lat), crs="EPSG:4326" ) # 计算每个坐标的权重(此处用薪资上限作为热度) heat_data = [[row['lat'], row['lng'], row['salary_max']/10000] for _, row in df.iterrows()] # 初始化中国地图(使用高德底图URL) m = folium.Map( location=[35, 105], zoom_start=3, tiles='https://webst01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}', attr='高德地图' ) # 添加热力图层 HeatMap(heat_data, radius=15, blur=10, max_zoom=1).add_to(m) # 保存为HTML m.save('static/heatmap_tencent.html')
4.2.1 关键参数调优指南
参数推荐值效果说明
radius10~20控制单个点影响半径,值越大越“糊”,适合宏观趋势;值小则突出局部热点
blur8~12模糊程度,与radius协同调节热力扩散平滑度
max_zoom1限制热力图最大缩放层级,避免放大后点状化失真
weightsalary_max/10000归一化权重,防止高薪岗位(如50K)完全压制中薪岗位(如15K)的显示

注意folium默认使用OpenStreetMap底图,但国内加载慢且无中文标注。代码中已切换为高德地图瓦片URL,需确保网络可访问webst01.is.autonavi.com;若内网环境,可下载离线瓦片或改用tiles='CartoDB positron'


5. 招聘趋势对比分析实战:用Pandas实现跨平台薪资中位数与城市集中度计算

5.1 双源数据合并与行业分类标准化

Python.xls是一个Excel文件,包含人工整理的「职位-行业」映射表,例如:

职位关键词行业大类行业子类
python工程师IT互联网软件开发
数据分析师IT互联网数据科学
人力资源专员专业服务人力资源

handle_crawl_tencent.py在入库前调用该映射表,为每条记录打上行业标签:

# handle_crawl_tencent.py 第156行 industry_df = pd.read_excel('Python.xls') def assign_industry(job_title): for _, row in industry_df.iterrows(): if any(kw in job_title for kw in row['职位关键词'].split('、')): return row['行业大类'], row['行业子类'] return '其他', '其他' # 赋值 record['industry_major'] = assign_industry(record['job_title'])[0] record['industry_minor'] = assign_industry(record['job_title'])[1]

5.2 跨平台薪资对比分析:Pandas GroupBy实战

run.py中的analyze_salary_comparison()函数执行核心分析:

def analyze_salary_comparison(): # 从MySQL读取双源数据 df_tencent = pd.read_sql("SELECT * FROM tencent_job_post WHERE salary_max > 0", con=engine) df_51job = pd.read_sql("SELECT * FROM job51_post WHERE salary_max > 0", con=engine) # 合并数据并标记来源 df_tencent['source'] = 'tencent' df_51job['source'] = '51job' df_combined = pd.concat([df_tencent, df_51job], ignore_index=True) # 按城市+行业计算薪资中位数 result = df_combined.groupby(['city', 'industry_major', 'source'])['salary_max'].agg([ 'count', lambda x: round(x.median(), -3) # 四舍五入到千位 ]).reset_index() result.columns = ['city', 'industry', 'source', 'post_count', 'median_salary'] # 输出为CSV供前端图表使用 result.to_csv('static/salary_comparison.csv', index=False, encoding='utf-8-sig') return result # 执行分析 if __name__ == '__main__': print(analyze_salary_comparison().head(10))
5.2.1 分析结果解读示例

运行后生成的salary_comparison.csv片段:

city,industry,source,post_count,median_salary 北京市,IT互联网,tencent,127,25000.0 北京市,IT互联网,51job,342,22000.0 上海市,IT互联网,tencent,89,23000.0 上海市,IT互联网,51job,287,20000.0

这揭示出关键业务洞察:腾讯招聘的IT岗位薪资中位数普遍比前程无忧高10%~15%,且职位数量更少——说明其岗位筛选更严格,定位更高阶人才。此结论可直接用于毕业答辩中的“平台差异性分析”章节。

5.3 城市集中度计算:用Shannon熵衡量招聘资源分布均衡性

招聘市场健康度不仅看总量,更要看分布。tex.py中新增熵值计算:

# tex.py 第120行 def calculate_city_entropy(df): # 计算各城市职位数占比 city_counts = df['city'].value_counts(normalize=True) # Shannon熵:-sum(p_i * log2(p_i)) entropy = -sum(p * np.log2(p) for p in city_counts if p > 0) # 归一化到[0,1]:实际熵 / 最大可能熵(log2(城市总数)) max_entropy = np.log2(len(city_counts)) normalized_entropy = entropy / max_entropy if max_entropy > 0 else 0 return round(normalized_entropy, 3) # 调用 tencent_entropy = calculate_city_entropy(df_tencent) print(f"腾讯招聘城市分布熵值:{tencent_entropy}(越接近1越均衡)") # 输出:腾讯招聘城市分布熵值:0.723
  • 熵值=0.723表示腾讯招聘覆盖城市较广,但仍有头部集中(北京、上海、深圳占60%);
  • 对比51job熵值(通常0.65左右),说明其地域覆盖更不均衡,下沉市场渗透不足。

这一指标可嵌入演示文稿的「市场健康度评估」页,比单纯柱状图更具说服力。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询