ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

招聘数据可视化闭环系统:双源爬虫+地理编码+热力图

招聘数据可视化闭环系统:双源爬虫+地理编码+热力图 简介本资源是一套完整的基于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.py和handle_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, keywordpython): 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/, timeout10) # 构造搜索URL关键词编码为GBK非UTF-8 search_url fhttps://search.51job.com/list/000000,000000,0000,00,9,99,{urllib.parse.quote(keyword.encode(gbk))},2,1.html response session.get(search_url, timeout15) # 使用 lxml 解析避开正则匹配JS渲染内容 tree etree.HTML(response.text) jobs tree.xpath(//div[idresultList]//div[classel]) for job in jobs: title job.xpath(.//p[classt1]/span/a/text())[0].strip() if job.xpath(.//p[classt1]/span/a/text()) else company job.xpath(.//span[classt2]/a/text())[0].strip() if job.xpath(.//span[classt2]/a/text()) else # 薪资字段存在多种格式1.5-2万/月、面议、15K-25K统一归一化为元/月数值区间 salary_raw job.xpath(.//span[classt4]/text())[0].strip() if job.xpath(.//span[classt4]/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)) # 统一转为元/月万→×10000K→×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, 02.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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT腾讯招聘职位主表;3.1.1 关键设计意图说明UNIQUE KEY uk_post_id_source防止同一职位在不同爬取周期重复入库source字段区分来源KEY idx_salary_range支撑「薪资分布直方图」查询避免全表扫描KEY idx_lat_lng为ST_Distance_Sphere()地理距离计算提供索引支持见4.2节ENGINEInnoDB保证事务一致性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, typeint, default0) max_salary request.args.get(max_salary, typeint, default9999999) 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中明确指定PyMySQL1.1.0与SQLAlchemy1.4.49tencentflask/config.py设置连接池参数# tencentflask/config.py SQLALCHEMY_DATABASE_URI mysqlpymysql://root:passwordlocalhost:3306/recruit_db?charsetutf8mb4 SQLALCHEMY_POOL_SIZE 10 SQLALCHEMY_MAX_OVERFLOW 20 SQLALCHEMY_POOL_RECYCLE 3600 SQLALCHEMY_TRACK_MODIFICATIONS FalsePOOL_SIZE10保持10个持久连接避免频繁创建销毁开销POOL_RECYCLE3600强制每小时重置连接防止MySQLwait_timeout断连charsetutf8mb4确保 emoji 与生僻字如“碁”、“ manoeuvre”正确存储。3.3 启动服务与环境验证三步完成本地部署初始化数据库执行mysql -u root -p mysql数据库/job51_data2.sql创建库表结构安装依赖并设置环境变量pip install -r requirements.txt export FLASK_APPtencentflask/app.py export FLASK_ENVdevelopment运行服务并验证APIflask run --host0.0.0.0 --port5000 # 浏览器访问 http://127.0.0.1:5000/api/jobs?city北京市min_salary15000 # 应返回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, encodingutf-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(fCity {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 , conengine) # 创建GeoDataFrame gdf gpd.GeoDataFrame( df, geometrygpd.points_from_xy(df.lng, df.lat), crsEPSG:4326 ) # 计算每个坐标的权重此处用薪资上限作为热度 heat_data [[row[lat], row[lng], row[salary_max]/10000] for _, row in df.iterrows()] # 初始化中国地图使用高德底图URL m folium.Map( location[35, 105], zoom_start3, tileshttps://webst01.is.autonavi.com/appmaptile?langzh_cnsize1scale1style7x{x}y{y}z{z}, attr高德地图 ) # 添加热力图层 HeatMap(heat_data, radius15, blur10, max_zoom1).add_to(m) # 保存为HTML m.save(static/heatmap_tencent.html)4.2.1 关键参数调优指南参数推荐值效果说明radius10~20控制单个点影响半径值越大越“糊”适合宏观趋势值小则突出局部热点blur8~12模糊程度与radius协同调节热力扩散平滑度max_zoom1限制热力图最大缩放层级避免放大后点状化失真weight列salary_max/10000归一化权重防止高薪岗位如50K完全压制中薪岗位如15K的显示注意folium默认使用OpenStreetMap底图但国内加载慢且无中文标注。代码中已切换为高德地图瓦片URL需确保网络可访问webst01.is.autonavi.com若内网环境可下载离线瓦片或改用tilesCartoDB 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, conengine) df_51job pd.read_sql(SELECT * FROM job51_post WHERE salary_max 0, conengine) # 合并数据并标记来源 df_tencent[source] tencent df_51job[source] 51job df_combined pd.concat([df_tencent, df_51job], ignore_indexTrue) # 按城市行业计算薪资中位数 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, indexFalse, encodingutf-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(normalizeTrue) # 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左右说明其地域覆盖更不均衡下沉市场渗透不足。这一指标可嵌入演示文稿的「市场健康度评估」页比单纯柱状图更具说服力。本文还有配套的精品资源点击获取
返回列表