ARTICLE DETAIL

资讯详情

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

Python招聘网站爬虫+数据分析+可视化:毕业设计源码实战指南

Python招聘网站爬虫+数据分析+可视化:毕业设计源码实战指南 简介这是一套面向计算机、通信、人工智能、自动化等相关专业学生与教师的Python毕业设计完整源码围绕招聘网站数据爬取、清洗、分析与可视化展开可用于毕业设计、期末课程设计或大作业也适合作为小白进阶练手项目。压缩包共54个文件约6.67MB包含4个py脚本与4个ipynb笔记本承载爬虫、数据清洗及可视化逻辑8个csv与8个xml存放招聘原始及清洗后数据另有html、js、css、less构建Echarts大屏展示页面png、jpg、ttf等用于图表与词云素材。项目按数据获取、数据清洗、数据分析、数据可视化、Echarts大屏展示等模块组织涵盖前程无忧招聘信息抓取、学历与工作经验饼图、福利与招聘信息词云等典型分析场景代码均经调试测试可运行答辩评审分达98分。目前已有381人学习下载基础较好的读者可在此基础上修改调整实现不同功能。1. 招聘网站爬虫数据分析可视化一套毕业设计源码到底该怎么跑通每年到了毕设季招聘网站的数据采集与分析就是计算机专业最热门的选题之一。原因很直接数据源公开、业务逻辑清晰、可视化效果好看答辩时演示起来也直观。但真正动手做的时候大部分人卡在三个地方——爬虫跑两天就被封 IP、数据存进数据库后不知道怎么清洗、可视化页面做出来像课程作业而不是毕设作品。这套「Python 招聘网站爬虫数据分析数据可视化」的源码方案核心就是解决这三个问题用 requests 做增量采集、用 pandas 做岗位画像分析、用 ECharts 做企业级数据看板。适合正在做计算机毕业设计、想找一个能讲清楚技术链路的选题的本科生也适合刚入门 Python 想拿一个完整项目练手的人。下面按实际落地顺序拆开讲。2. 爬虫层从招聘列表页到结构化岗位数据的完整链路2.1 目标站点结构分析与请求策略选型招聘网站的数据采集第一步不是写代码是打开浏览器开发者工具把列表页的请求链路看清楚。以常见的招聘平台为例岗位列表通常有两种加载方式一种是服务端渲染HTML 里直接包含岗位卡片另一种是前端通过 XHR 异步拉取 JSON 数据。前者用 requests BeautifulSoup 就能解析后者需要找到真实的 API 接口地址。我一般会先看 Network 面板里的 XHR 请求筛选出返回 JSON 的那个接口。招聘网站的列表 API 通常长这样# 典型招聘网站列表接口结构示意 https://example.com/api/job/list?city北京keywordPythonpage1pageSize30关键参数就四个城市编码、搜索关键词、页码、每页条数。城市编码一般不是中文而是站点内部定义的数字 ID需要从城市选择页的请求里抓一次。关键词就是你搜的岗位名称比如「Python 开发」「数据分析师」。页码从 1 开始递增每页条数通常上限是 30 或 50设太大容易被风控。选 requests 而不是 Selenium 的理由很简单Selenium 启动浏览器实例的开销太大采集几百页数据时速度差距非常明显。但如果目标站点的列表数据是 JS 动态渲染且没有独立 API那就只能上 Selenium 或 Playwright。判断方法在 Network 面板里看有没有返回岗位数据的 XHR 请求有就用 requests没有就用浏览器自动化。2.2 用 requests 重试机制写一个能跑过夜的采集脚本直接上代码。这个脚本的核心设计是带重试的请求封装、随机延迟、User-Agent 轮换、增量写入 SQLite。import requests import sqlite3 import time import random import logging from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) # 请求头池降低被识别为爬虫的概率 UA_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0, ] def build_session(): 构建带重试机制的 session session requests.Session() retry Retry( total3, # 最多重试3次 backoff_factor1.5, # 重试间隔递增1.5s, 3s, 6s status_forcelist[429, 500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.mount(http://, adapter) return session def init_db(db_pathjobs.db): 初始化 SQLite 数据库建岗位表 conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS job_list ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_name TEXT, company TEXT, salary TEXT, city TEXT, experience TEXT, education TEXT, publish_date TEXT, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(job_name, company, city) ) ) conn.commit() return conn def fetch_page(session, url, params): 请求单页数据返回 JSON headers { User-Agent: random.choice(UA_POOL), Referer: https://example.com/, Accept: application/json, text/plain, */*, } try: resp session.get(url, paramsparams, headersheaders, timeout15) resp.raise_for_status() return resp.json() except requests.exceptions.RequestException as e: logging.error(f请求失败: {e}) return None def parse_jobs(json_data): 从 JSON 响应中提取岗位列表 jobs [] if not json_data or data not in json_data: return jobs for item in json_data[data].get(list, []): jobs.append({ job_name: item.get(jobName, ).strip(), company: item.get(companyName, ).strip(), salary: item.get(salaryDesc, ), city: item.get(cityName, ), experience: item.get(workYear, ), education: item.get(eduLevel, ), publish_date: item.get(publishTime, ), }) return jobs def save_jobs(conn, jobs): 增量写入UNIQUE 约束自动去重 cursor conn.cursor() inserted 0 for job in jobs: try: cursor.execute( INSERT OR IGNORE INTO job_list (job_name, company, salary, city, experience, education, publish_date) VALUES (?, ?, ?, ?, ?, ?, ?) , (job[job_name], job[company], job[salary], job[city], job[experience], job[education], job[publish_date])) if cursor.rowcount 0: inserted 1 except sqlite3.Error as e: logging.warning(f写入失败: {e}) conn.commit() return inserted def crawl(keywordPython, city北京, max_pages50): 主采集流程 session build_session() conn init_db() base_url https://example.com/api/job/list for page in range(1, max_pages 1): params {keyword: keyword, city: city, page: page, pageSize: 30} data fetch_page(session, base_url, params) jobs parse_jobs(data) if not jobs: logging.info(f第 {page} 页无数据采集结束) break inserted save_jobs(conn, jobs) logging.info(f第 {page} 页: 获取 {len(jobs)} 条, 新增 {inserted} 条) # 随机延迟 2-5 秒模拟人工浏览节奏 time.sleep(random.uniform(2, 5)) conn.close() logging.info(采集完成) if __name__ __main__: crawl(keywordPython, city北京, max_pages50)这段代码有几个设计点需要说清楚。Retry的backoff_factor1.5意味着第一次重试等 1.5 秒第二次等 3 秒第三次等 6 秒这个递增节奏比固定间隔更不容易触发风控。UNIQUE(job_name, company, city)这个联合唯一约束是增量采集的关键——同一岗位在不同时间被重复抓到时会自动跳过不需要额外写去重逻辑。随机延迟设在 2 到 5 秒之间是因为固定延迟的请求间隔在服务端日志里呈现为规律性脉冲很容易被识别。参数调整建议如果目标站点反爬较松延迟可以降到 1 到 2 秒如果频繁返回 429把max_pages调小、延迟调大或者换一个时间段再跑。pageSize不要超过站点允许的上限超了会直接返回空数据。2.3 反爬应对请求头、频率控制与代理池的取舍招聘网站常见的反爬手段有三层。第一层是 User-Agent 检测这个用 UA 池就能绕过。第二层是请求频率限制表现为短时间内大量请求后返回 429 或直接封 IP 一段时间。第三层是行为检测比如检查是否有鼠标移动、页面滚动等浏览器行为特征这一层用 requests 很难完全模拟。对于毕业设计级别的采集量几千到几万条我的经验是UA 池 随机延迟 单 IP 限速基本够用。代理池是最后的手段维护成本高而且免费代理的可用率通常不到 10%。如果确实需要代理建议用付费的短效代理按量计费成本可控。还有一个容易被忽略的点采集时间窗口。工作日上午 9 点到 11 点、下午 2 点到 4 点是招聘网站流量高峰期这时候你的请求混在真实用户里相对不容易被标记。凌晨跑虽然没人跟你抢带宽但异常流量特征反而更明显。提示采集前先看目标网站的 robots.txt 和服务条款确认允许采集的范围。毕业设计场景下控制采集量、不对外传播数据是基本的合规底线。3. 数据清洗与存储从原始岗位表到可分析的干净数据集3.1 薪资字段的解析与标准化招聘网站返回的薪资字段是一团乱麻「15-25K·13薪」「8千-1.2万」「200-300元/天」「面议」。要做薪资分析必须先把这些字符串转成可计算的数值。import re import pandas as pd def parse_salary(salary_str): 解析薪资字符串返回 (最低月薪, 最高月薪) 单位元 支持格式15-25K, 8千-1.2万, 200-300元/天, 面议 if not salary_str or salary_str in (面议, 薪资面议): return None, None salary_str salary_str.replace(·13薪, ).replace(·14薪, ).strip() # 匹配 15-25K 或 15-25k match re.search(r(\d\.?\d*)-(\d\.?\d*)[Kk千], salary_str) if match: low float(match.group(1)) * 1000 high float(match.group(2)) * 1000 return low, high # 匹配 8千-1.2万 match re.search(r(\d\.?\d*)千-(\d\.?\d*)万, salary_str) if match: low float(match.group(1)) * 1000 high float(match.group(2)) * 10000 return low, high # 匹配 200-300元/天 match re.search(r(\d)-(\d)元/天, salary_str) if match: low float(match.group(1)) * 22 # 按每月22个工作日折算 high float(match.group(2)) * 22 return low, high # 匹配单一数值 20K match re.search(r(\d\.?\d*)[Kk], salary_str) if match: val float(match.group(1)) * 1000 return val, val return None, None # 读取数据库中的原始数据 conn sqlite3.connect(jobs.db) df pd.read_sql(SELECT * FROM job_list, conn) conn.close() # 应用薪资解析 df[[salary_low, salary_high]] df[salary].apply( lambda x: pd.Series(parse_salary(x)) ) df[salary_avg] (df[salary_low] df[salary_high]) / 2 # 过滤掉薪资缺失的记录 df_clean df.dropna(subset[salary_avg]).copy() print(f原始记录: {len(df)}, 有效薪资记录: {len(df_clean)})parse_salary函数按优先级依次匹配四种格式每种格式的折算逻辑不同。日薪按 22 个工作日折算月薪是因为招聘网站上的日薪岗位通常是实习或兼职直接对比月薪岗位会失真。salary_avg取最低和最高的均值用于后续的分布分析。参数说明22这个折算系数可以根据实际场景调整如果是按周结算的岗位可以改成 4.3 周。dropna会丢弃薪资为「面议」的记录如果这类记录占比超过 30%说明数据质量有问题需要检查采集逻辑。3.2 用 SQLAlchemy 做多表存储与索引优化SQLite 适合单机跑但如果要做更复杂的查询和关联分析建议用 SQLAlchemy 管理表结构同时把清洗后的数据写入独立的分析表。from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, Index from sqlalchemy.orm import declarative_base, sessionmaker from datetime import datetime Base declarative_base() class JobClean(Base): 清洗后的岗位分析表 __tablename__ job_clean id Column(Integer, primary_keyTrue, autoincrementTrue) job_name Column(String(100), nullableFalse) company Column(String(200)) city Column(String(50), indexTrue) # 城市查询频繁加索引 salary_low Column(Float) salary_high Column(Float) salary_avg Column(Float, indexTrue) # 薪资分析频繁加索引 experience Column(String(50)) education Column(String(50)) publish_date Column(String(50)) crawl_time Column(DateTime, defaultdatetime.now) __table_args__ ( Index(idx_city_salary, city, salary_avg), # 联合索引 ) engine create_engine(sqlite:///jobs_analysis.db, echoFalse) Base.metadata.create_all(engine) Session sessionmaker(bindengine) session Session() # 批量写入清洗后的数据 for _, row in df_clean.iterrows(): job JobClean( job_namerow[job_name], companyrow[company], cityrow[city], salary_lowrow[salary_low], salary_highrow[salary_high], salary_avgrow[salary_avg], experiencerow[experience], educationrow[education], publish_daterow[publish_date], ) session.add(job) session.commit() session.close() print(f写入 {len(df_clean)} 条清洗后数据)索引的设计逻辑city和salary_avg各自建了单列索引因为按城市筛选和按薪资排序是最常见的两种查询。idx_city_salary联合索引用于「某城市薪资排名」这类组合查询。索引不是越多越好每多一个索引写入速度就会下降一点。对于毕业设计的数据量几万条两三个索引足够了。注意SQLAlchemy 的echoFalse在生产环境要关掉否则每条 SQL 都会打印到控制台几万条数据写入时日志会刷屏。4. 数据分析岗位画像、薪资分布与技能关键词提取4.1 用 pandas 做城市薪资对比与经验要求分析数据清洗完之后分析部分的核心是回答几个问题哪些城市薪资高、经验要求怎么分布、学历门槛是什么水平。import pandas as pd import numpy as np # 读取清洗后的数据 conn sqlite3.connect(jobs_analysis.db) df pd.read_sql(SELECT * FROM job_clean, conn) conn.close() # 1. 城市薪资对比按城市分组计算薪资中位数和岗位数量 city_stats df.groupby(city).agg( 岗位数量(id, count), 薪资中位数(salary_avg, median), 薪资均值(salary_avg, mean), 薪资最低(salary_avg, min), 薪资最高(salary_avg, max), ).round(0).sort_values(薪资中位数, ascendingFalse) # 只保留岗位数量 10 的城市避免小样本偏差 city_stats city_stats[city_stats[岗位数量] 10] print( 城市薪资排名岗位数10) print(city_stats.head(10)) # 2. 经验要求分布 exp_order [应届生, 1年以内, 1-3年, 3-5年, 5-10年, 10年以上] exp_dist df[experience].value_counts() exp_dist exp_dist.reindex([e for e in exp_order if e in exp_dist.index]) print(\n 经验要求分布 ) print(exp_dist) # 3. 不同经验段的薪资对比 exp_salary df.groupby(experience)[salary_avg].agg([median, count]) exp_salary exp_salary.reindex([e for e in exp_order if e in exp_salary.index]) print(\n 经验段薪资对比 ) print(exp_salary.round(0))groupby之后用agg一次性计算多个统计量比循环计算效率高很多。岗位数量 10这个过滤条件很重要——如果某个城市只抓到 3 条数据它的薪资中位数没有统计意义放在图表里反而误导。经验段的排序用reindex强制按职业发展顺序排列而不是按数量多少排这样图表读起来更符合直觉。4.2 技能关键词提取从岗位描述里挖出高频技术栈岗位名称只能告诉你「招什么岗位」岗位描述里的技能关键词才能告诉你「需要会什么」。如果采集时一并抓了岗位详情页的描述文本可以用 jieba 做分词和关键词统计。import jieba import jieba.analyse from collections import Counter # 假设 df 中有一列 job_desc 存储岗位描述文本 # 如果采集时没抓描述可以用岗位名称做粗粒度分析 # 自定义技术词典提高分词准确率 tech_words [Python, Java, MySQL, Redis, Docker, Kubernetes, 机器学习, 深度学习, 数据分析, 数据挖掘, SQL, Linux, Spark, Hadoop, Flask, Django, TensorFlow, PyTorch] for word in tech_words: jieba.add_word(word) def extract_keywords(text, topK20): 用 TF-IDF 提取关键词 if not text or len(text) 10: return [] keywords jieba.analyse.extract_tags(text, topKtopK, withWeightFalse) return keywords # 汇总所有岗位描述的关键词 all_keywords [] for desc in df[job_desc].dropna(): all_keywords.extend(extract_keywords(desc)) # 统计词频 keyword_freq Counter(all_keywords) print( 技能关键词 TOP 20 ) for word, count in keyword_freq.most_common(20): print(f{word}: {count})jieba.add_word把技术名词加入自定义词典是因为默认词典会把「机器学习」切成「机器」和「学习」「TensorFlow」切成「Tensor」和「Flow」导致统计结果失真。TF-IDF 提取的关键词比单纯词频统计更能反映岗位的核心要求因为它会降低「负责」「工作」「要求」这类通用词的权重。如果采集时没有抓岗位描述退而求其次可以用岗位名称做词频统计但信息量会少很多。建议在爬虫阶段就把详情页的描述文本一并抓下来存储时单独建一列。5. 数据可视化用 ECharts 搭一个能放答辩 PPT 的看板5.1 Flask 后端接口设计与 ECharts 前端对接可视化的架构是Flask 提供 JSON 接口前端用 ECharts 渲染图表。先看后端接口。from flask import Flask, jsonify, render_template import sqlite3 import pandas as pd app Flask(__name__) def query_db(sql): conn sqlite3.connect(jobs_analysis.db) df pd.read_sql(sql, conn) conn.close() return df app.route(/) def index(): return render_template(dashboard.html) app.route(/api/city_salary) def city_salary(): 城市薪资排名接口 df query_db( SELECT city, COUNT(*) as job_count, ROUND(AVG(salary_avg), 0) as avg_salary FROM job_clean GROUP BY city HAVING COUNT(*) 10 ORDER BY avg_salary DESC LIMIT 15 ) return jsonify({ cities: df[city].tolist(), salaries: df[avg_salary].tolist(), counts: df[job_count].tolist(), }) app.route(/api/salary_distribution) def salary_distribution(): 薪资分布直方图接口 df query_db(SELECT salary_avg FROM job_clean WHERE salary_avg IS NOT NULL) # 按 2000 元为区间分桶 bins list(range(0, 60001, 2000)) labels [f{bins[i]//1000}-{bins[i1]//1000}K for i in range(len(bins)-1)] df[bucket] pd.cut(df[salary_avg], binsbins, labelslabels, rightFalse) dist df[bucket].value_counts().sort_index() return jsonify({ labels: dist.index.tolist(), values: dist.values.tolist(), }) app.route(/api/experience_stats) def experience_stats(): 经验要求与薪资关系接口 df query_db( SELECT experience, COUNT(*) as count, ROUND(AVG(salary_avg), 0) as avg_salary FROM job_clean WHERE experience IS NOT NULL AND experience ! GROUP BY experience ORDER BY avg_salary ) return jsonify({ experience: df[experience].tolist(), counts: df[count].tolist(), salaries: df[avg_salary].tolist(), }) if __name__ __main__: app.run(debugTrue, port5000)三个接口分别对应三张图城市薪资排名用柱状图、薪资分布用直方图、经验与薪资关系用双轴图。pd.cut做分桶时rightFalse表示左闭右开区间避免 20000 这个值同时落入两个桶。HAVING COUNT(*) 10在 SQL 层面就过滤掉了小样本城市前端不需要再做判断。5.2 三个核心图表的 ECharts 配置与参数调优前端 HTML 模板里引入 ECharts CDN然后分别初始化三个图表。!DOCTYPE html html head meta charsetutf-8 title招聘数据可视化看板/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script style .chart-container { width: 100%; height: 400px; margin-bottom: 30px; } body { background: #f5f7fa; padding: 20px; } /style /head body div idcityChart classchart-container/div div idsalaryChart classchart-container/div div idexpChart classchart-container/div script // 城市薪资排名 - 横向柱状图 fetch(/api/city_salary).then(r r.json()).then(data { const chart echarts.init(document.getElementById(cityChart)); chart.setOption({ title: { text: 城市薪资排名 TOP15, left: center }, tooltip: { trigger: axis, axisPointer: { type: shadow } }, grid: { left: 15%, right: 10% }, xAxis: { type: value, name: 平均月薪(元) }, yAxis: { type: category, data: data.cities.reverse(), axisLabel: { fontSize: 12 } }, series: [{ type: bar, data: data.salaries.reverse(), itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: #83bff6 }, { offset: 1, color: #188df0 } ]) }, label: { show: true, position: right, formatter: {c}元 } }] }); }); // 薪资分布 - 直方图 fetch(/api/salary_distribution).then(r r.json()).then(data { const chart echarts.init(document.getElementById(salaryChart)); chart.setOption({ title: { text: 薪资分布区间, left: center }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.labels, axisLabel: { rotate: 45 } }, yAxis: { type: value, name: 岗位数量 }, series: [{ type: bar, data: data.values, itemStyle: { color: #5470c6 }, barWidth: 60% }] }); }); // 经验与薪资 - 双轴图 fetch(/api/experience_stats).then(r r.json()).then(data { const chart echarts.init(document.getElementById(expChart)); chart.setOption({ title: { text: 经验要求与薪资关系, left: center }, tooltip: { trigger: axis }, legend: { data: [岗位数量, 平均薪资], bottom: 0 }, xAxis: { type: category, data: data.experience }, yAxis: [ { type: value, name: 岗位数量 }, { type: value, name: 平均薪资(元) } ], series: [ { name: 岗位数量, type: bar, data: data.counts, itemStyle: { color: #91cc75 } }, { name: 平均薪资, type: line, yAxisIndex: 1, data: data.salaries, itemStyle: { color: #ee6666 }, smooth: true } ] }); }); /script /body /html三个图表的配置要点城市排名用横向柱状图是因为城市名较长纵向排列会挤在一起薪资分布用直方图时barWidth: 60%让柱子之间留出间隙视觉上更清晰经验与薪资用双轴图是因为岗位数量和薪资的量级差了一个数量级共用一根 Y 轴会导致其中一个系列被压扁。echarts.graphic.LinearGradient给柱子加了渐变填充这是让图表看起来「不像课程作业」的最简单手段。答辩 PPT 里放一张带渐变和标签的图表观感提升非常明显。提示ECharts 的 CDN 地址建议固定版本号不要用latest否则某天 CDN 更新后图表配置可能不兼容。6. 避坑与排查采集、清洗、可视化三个环节的翻车记录6.1 爬虫跑着跑着返回空数据现象前 20 页正常返回第 21 页开始data.list为空数组但 HTTP 状态码是 200。原因触发了服务端的频率限制但站点没有返回 429而是返回空数据。这种「软封禁」比直接报错更隐蔽。解决在parse_jobs里加一个判断——如果连续 3 页返回空数据就主动停止采集并记录日志。同时把延迟从 2-5 秒调大到 5-10 秒等 30 分钟后再从断点继续。6.2 薪资解析把「15-25K·13薪」算成了 15K现象parse_salary对带「·13薪」的字符串解析结果偏低。原因replace(·13薪, )只处理了 13 薪但实际数据里还有「·14薪」「·15薪」「·16薪」没被替换掉的部分干扰了正则匹配。解决用正则统一处理·\d薪的模式而不是逐个字符串替换。import re salary_str re.sub(r·\d薪, , salary_str)6.3 SQLite 并发写入报 database is locked现象爬虫脚本和 Flask 应用同时运行时偶尔报sqlite3.OperationalError: database is locked。原因SQLite 默认的锁机制是写操作独占爬虫在批量写入时Flask 的读请求会被阻塞。解决爬虫写入和 Flask 读取用不同的数据库文件。爬虫写jobs.db清洗后的数据写入jobs_analysis.dbFlask 只读jobs_analysis.db。两个文件之间用清洗脚本做同步。6.4 ECharts 图表在答辩电脑上显示空白现象本地开发时图表正常拷到答辩教室的电脑上打开是空白页。原因教室电脑没有网络ECharts 的 CDN 加载失败。解决把echarts.min.js下载到本地static/目录HTML 里改成script src/static/echarts.min.js/script。答辩前一定要在离线环境下测一遍。6.5 岗位名称里的空格导致去重失效现象同一家公司的同一个岗位被重复写入数据库。原因岗位名称里混入了全角空格或不可见字符UNIQUE约束认为它们是不同的字符串。解决写入前对job_name和company做strip()和全角转半角处理。def normalize_text(text): 全角转半角去除首尾空白 if not text: return result [] for char in text.strip(): code ord(char) if code 0x3000: # 全角空格 code 0x20 elif 0xFF01 code 0xFF5E: # 全角字符范围 code - 0xFEE0 result.append(chr(code)) return .join(result)7. 进阶技巧让这套源码在答辩时多拿 10 分的三个细节第一个细节是数据时效性标注。在可视化看板的标题下方加一行小字「数据采集时间2025年X月X日样本量XXXX条」。答辩老师看到这行字第一反应是「这个学生知道数据有时效性」印象分直接上去。实现方式很简单在 Flask 接口里加一个/api/meta返回采集时间和总记录数前端渲染到页面上。第二个细节是异常值处理的可视化呈现。薪资分布图里如果出现 50K 以上的极端值不要直接删掉而是在图表里用不同颜色标出来旁边加注释「高薪异常值可能为管理岗或数据录入误差」。这展示的是数据分析思维比单纯画一张漂亮的图更有说服力。第三个细节是代码的可复现性。在项目根目录放一个requirements.txt和一个run.sh让答辩老师或者未来的你能一条命令跑通整个流程。#!/bin/bash # run.sh - 一键运行完整流程 set -e echo 1. 安装依赖 pip install -r requirements.txt echo 2. 采集数据 python crawler.py echo 3. 清洗数据 python clean.py echo 4. 启动可视化 python app.pyrequirements.txt里锁定版本号不要写requests2.0这种模糊版本直接写requests2.31.0。答辩现场如果老师让你重新跑一遍版本不兼容导致报错就尴尬了。最后一个习惯采集脚本里永远留一个--max-pages参数默认值设小一点比如 5 页。演示的时候用 5 页快速跑通正式采集时再改成 50 页。我见过太多人在答辩现场跑全量采集等了十分钟还没跑完老师已经不耐烦了。希望帮到你。本文还有配套的精品资源点击获取
返回列表