简介:这是一份基于Python的北京市大数据岗位招聘数据全流程实践项目,覆盖爬虫采集、数据清洗、数据分析与可视化展示。项目以Boss直聘为目标站点,演示Scrapy/BeautifulSoup请求解析、pandas预处理与统计聚合,并结合matplotlib、seaborn等库输出地域分布、薪资区间、职位需求变化等图表,适合正在学习爬虫与数据分析的Python开发者作为综合练手案例。压缩包共111个文件,总大小6.58MB,其中包含4个Python爬虫脚本、1个CSV数据文件、1个Excel数据文件,另有大量前端展示文件(html/css/js)、地图组件及数据库文件,方便直接运行或二次开发。目前已有727人学习下载。内含完整源代码、采集数据及可视化页面,目录结构清晰,能够帮助读者快速理解从网络爬虫到数据落库、从统计分析到图表呈现的完整链路,在真实业务场景中提升数据获取与解读能力。
1. 北京招聘数据分析项目到底值不值得做:先看清这条链路再说
把「大数据」三个字先放一边,这个项目最实在的价值,是把 Python 爬虫、Pandas 数据清洗、SQLite 存储、可视化大屏这四段技能,用一条完整的链路串起来。北京市大数据岗位的招聘信息每天都在更新,爬下来之后整理成结构化表格,算出薪资分布、技能需求、学历门槛,再做成能交互的可视化大屏——这套流程做完,你对「数据分析师拿到原始数据之后发生了什么」就有了完整的概念。
这个项目适合两类人:一类是正在准备数据分析相关岗位面试的求职者,需要一份能讲清楚细节的项目经历;另一类是毕业设计选了数据可视化方向的学生,需要一套能跑通、能截图展示、能写进论文的完整实现。数据量级没有想象中大,一个月能采集一万多条有效岗位记录就够用,真正的工程量在清洗和口径统一上。接下来我把每个环节的落地方式和参数选择逐个拆开讲,照着做能少走大半弯路。
2. 招聘数据爬虫:字段设计、requests 与 Selenium 双方案
2.1 先定表结构再写爬虫:岗位数据的 8 个必备字段
写爬虫之前先把目标表结构定下来,这是最容易省事的一步。很多新手上来就写requests.get(),拿到 HTML 之后发现想要的字段散落在五六个地方,最后清洗阶段被迫反复回去补爬,非常被动。我一般会先想清楚最终分析需要哪些维度,再决定页面里提取什么。
北京招聘数据分析至少需要这 8 个字段:岗位名称、公司名称、薪资区间、学历要求、经验要求、工作地点、技能标签、发布日期。如果后续想分析公司规模对薪资的影响,再加一个公司规模字段。不要把整段 JD 存进去——JD 文本太大,而且后续技能抽取是单独的逻辑,存原始文本反而拖慢读写。
CREATE TABLE job_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_title TEXT NOT NULL, company_name TEXT, salary_raw TEXT, salary_min INTEGER, salary_max INTEGER, education TEXT, experience TEXT, location TEXT, skills TEXT, publish_date TEXT, source_url TEXT UNIQUE, crawl_time TEXT, UNIQUE(company_name, job_title, salary_raw, publish_date) );这个建表语句里有几个关键设计:source_url加 UNIQUE 约束,保证同一链接不会重复入库;最后的复合 UNIQUE 作为兜底,防止同一 URL 内容更新后产生重复记录;薪资字段同时保留salary_raw原始字符串和salary_min/max整数,方便后续直接聚合计算。工作地点单独存,为可视化那一步按区域聚合做准备。
2.2 requests 抓静态页面:UA 池和随机延时是底线
招聘平台的首页和搜索结果页大部分内容可以直接用 requests 拿到,前提是请求头够友好。UA 池和随机延时是底线,我自己吃过教训:固定间隔 1 秒跑 200 个请求,到第 150 个左右开始返回 403,连验证码页面都不给。后来改成随机延时,同样的请求量再没触发过风控。
import random import time import requests from fake_useragent import UserAgent ua = UserAgent() def fetch_page(url: str, retries: int = 3): headers = { "User-Agent": ua.random, "Accept": "text/html,application/xhtml+xml", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://www.zhipin.com/" } for attempt in range(retries): try: resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.text elif resp.status_code == 403: wait = 30 * (attempt + 1) # 指数退避 print(f"HTTP 403,等待 {wait}s 后重试") time.sleep(wait) else: time.sleep(5) except requests.RequestException as e: print(f"请求异常: {e},重试 {attempt + 1}/{retries}") time.sleep(3) return None这里的核心参数有两个:timeout=10防止某个慢接口拖住整个任务,retries=3配合指数退避给临时风控留出恢复时间。指数退避里30 * (attempt + 1)意味着第一次失败等 30 秒,第二次 60 秒,第三次 90 秒。用ua.random每次换 UA,比固定写一个 Chrome UA 靠谱得多,fake_useragent库会随机生成各版本浏览器标识。
拿到 HTML 之后用 BeautifulSoup 解析,按标签定位字段:
from bs4 import BeautifulSoup def parse_job_card(html: str): soup = BeautifulSoup(html, "html.parser") jobs = [] for card in soup.select(".job-card"): title = card.select_one(".job-title") company = card.select_one(".company-name") salary = card.select_one(".salary") if not title or not salary: continue jobs.append({ "job_title": title.get_text(strip=True), "company_name": company.get_text(strip=True) if company else "", "salary_raw": salary.get_text(strip=True), "source_url": card.get("href", "") }) return jobsselect_one返回第一个匹配项,get_text(strip=True)把标签内部文本提取出来并去掉首尾空格。这里有个细节:没有工资信息的卡片直接continue跳过——这种一般是外包岗或「薪资面议」,留到后面拆薪资逻辑反而容易出错。选择器.</ job-card只是示例,实际平台需要你打开浏览器开发者工具,找到岗位卡片对应的真实 class 名再替换。
2.3 Selenium 处理动态渲染:无头浏览器与显式等待
部分招聘页面是 JavaScript 动态渲染的,requests 拿到的是空壳 HTML,这时候换成 Selenium。常见的做法是启动 headless Chrome,模拟滚动加载分页数据,等页面关键元素出现后再提取。
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options = Options() options.add_argument("--headless=new") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--window-size=1920,1080") driver = webdriver.Chrome(options=options) driver.get("https://www.example.com/jobs") wait = WebDriverWait(driver, 10) cards = wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".job-card")) ) for card in cards: title = card.find_element(By.CSS_SELECTOR, ".job-title").text salary = card.find_element(By.CSS_SELECTOR, ".salary").text print(title, salary) driver.quit()--headless=new是 Chrome 109 之后的推荐写法,旧的--headless在新版上可能直接失效。--window-size=1920,1080必须带上,很多前端框架在窄屏下会切换成移动端布局,导致 class 名完全不同。WebDriverWait是显式等待,最多等 10 秒直到.job-card元素出现——这里不要用time.sleep(3)这种固定等待,页面加载速度波动很大,固定等待要么浪费要么不够。
Selenium 方案的定位是补 requests 的缺口,不是全部替代。我的建议是先用 requests 跑一部分页面,把能拿到的数据量统计一下,确认哪些页面需要 Selenium 再开浏览器,能省下大量采集时间。另外无头模式被网站识别的问题确实存在,普通学习项目不用过度纠结,后面避坑章节会展开讲。
2.4 增量去重与采集频率:控制总量比追求数量重要
招聘数据爬虫有一个常见误区:总想把目标平台所有岗位都爬下来。实际上北京一日新增的大数据相关岗位可能有几百条,连续采集一个月也就一万多条,这个量级对分析足够。更重要的是维护一个去重逻辑,避免同一条岗位在数据里出现三次。
import hashlib import sqlite3 def gen_content_hash(record: dict) -> str: raw = f"{record['company_name']}|{record['job_title']}|{record['salary_raw']}" return hashlib.md5(raw.encode()).hexdigest() def save_if_new(conn: sqlite3.Connection, record: dict): content_hash = gen_content_hash(record) exists = conn.execute( "SELECT id FROM job_posts WHERE content_hash = ?", (content_hash,) ).fetchone() if exists: return False conn.execute( "INSERT INTO job_posts (job_title, company_name, salary_raw, content_hash) " "VALUES (?, ?, ?, ?)", (record["job_title"], record["company_name"], record["salary_raw"], content_hash) ) conn.commit() return True用 MD5 生成内容指纹,比直接比对多个字段效率更高。company_name + job_title + salary_raw三者拼接作为指纹,同一个公司在同一时间段发布的同名岗位大概率是重复职位。
采集频率这块,我的建议是每天只跑一次增量任务,固定在北京时间凌晨防御性较强,避开白天用户活跃高峰期。单次任务最多请求 300 个页面,超过就直接停下来第二天继续。爬虫要做的是「可持续更新数据」,不是「一次性榨干站点」,尤其招聘数据这种按天更新的业务,今天没爬到明天还有,没必要冒险。
3. 数据清洗与存储:薪资拆解、技能抽取与 SQLite 落库
3.1 薪资字段拆解:正则处理「15-25K·14薪」
招聘网站的薪资展示格式五花八门,常见的有「20-40K·14薪」「8k-12k」「25-50K·15薪」「10K」这几种。分析前必须统一拆成三个数字:下限、上限、中位数。正则表达式是最稳的解法,一次性把区间两端和月薪单位提取出来。
import re def parse_salary(salary_raw: str) -> tuple: """ 返回 (salary_min, salary_max, salary_mid) 输入: "20-40K·14薪" 或 "8k-12k" """ if not salary_raw: return (0, 0, 0) pattern = r"(\d+(?:\.\d+)?)\s*[-~—]?\s*(\d+(?:\.\d+)?)?\s*[Kk]" match = re.search(pattern, salary_raw) if not match: return (0, 0, 0) low = float(match.group(1)) high = float(match.group(2)) if match.group(2) else low return (int(low), int(high), int((low + high) / 2))\d+(?:\.\d+)?表示匹配整数或小数,[-~—]?兼容了连字符、波浪线、中文破折号三种分隔写法。中位数取区间两端平均值,后续聚合就按salary_mid算。这里有个经验:不要把「14薪」「16薪」直接算进月薪分析,不同公司的薪酬结构差异太大,统一按月薪中位数比较才有意义。想分析年薪就把salary_mid * 薪数,但要在清洗时单独存年薪字段,别覆盖月薪。
3.2 技能标签抽取:关键词库匹配与词频归一化
岗位描述里技能词的写法也不统一:「Python」和「python」大小写不同,「C++」和「C/C++」算同一个技能,「Hadoop/Hive/Spark」经常连在一起写。技能抽取的策略是维护一份技能关键词表,对文本做大小写归一化后逐一匹配。
SKILL_KEYWORDS = [ "Python", "Java", "SQL", "Spark", "Hadoop", "Hive", "Flink", "Kafka", "Redis", "MySQL", "Pandas", "NumPy", "Docker", "K8s", "Linux", "Spring", "TensorFlow", "PyTorch", "Selenium", "Airflow", "Linux", "Shell", "HBase", "Elasticsearch", "FineBI", "Tableau" ] def extract_skills(text: str) -> list: text_lower = text.lower() matched = [] for skill in SKILL_KEYWORDS: if skill.lower() in text_lower: matched.append(skill) return list(set(matched)) # 同一岗位内技能去重匹配之后要把结果用逗号拼成一个字段存进skills列,后续分析时用FIND_IN_SET或在 Python 里str.split(',')展开统计。大小写统一转小写再匹配,避免「Python」和「python」被当成两个技能。K8s 和 Kubernetes 这种同义不同写的问题,靠词表里同时收录并做别名映射解决,更简单的方案是统一替换成规范名。
技能抽取这块不需要做得太深,够分析就行。几轮做下来你会看到 Python、SQL、Java 稳居前三,这个结论本身就能支撑论文里「北京大数据岗位技术栈分布」的分析章节。
3.3 SQLAlchemy 定义 ORM 模型并落库
清洗完成的数据用 SQLAlchemy 写入 SQLite,比直接拼 SQL 更安全,也方便后续切换到 MySQL。SQLAlchemy 的好处是模型定义即文档,字段类型一目了然。
from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.orm import declarative_base, sessionmaker Base = declarative_base() class JobPost(Base): __tablename__ = "job_posts" id = Column(Integer, primary_key=True) job_title = Column(String(100)) company_name = Column(String(100)) salary_raw = Column(String(50)) salary_min = Column(Integer) salary_max = Column(Integer) salary_mid = Column(Integer) education = Column(String(30)) experience = Column(String(50)) location = Column(String(50)) skills = Column(String(500)) publish_date = Column(String(20)) source_url = Column(String(500), unique=True) crawl_time = Column(String(20)) engine = create_engine("sqlite:///beijing_jobs.db") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session()unique=True在数据库层面挡住了重复 URL,即使清洗环节漏掉了重复记录,入库时也会报错而不是静默写入。SQLite 文件单文件存储,整个项目可以塞进一个文件夹,交给别人复现时拷走.db文件就够了。单机分析场景下 SQLite 性能完全够用,只有数据量超过几百万行才需要考虑 MySQL。
3.4 清洗质量校验:空值率、异常值和重复率三张表
清洗完先别急着分析,花十分钟跑一遍质量校验。招聘数据最容易出的问题是:薪资解析失败返回 0、部分字段大量为空、重复率异常高。我会用一条 SQL 快速看全局。
SELECT COUNT(*) AS total, SUM(CASE WHEN salary_mid = 0 THEN 1 ELSE 0 END) AS zero_salary, SUM(CASE WHEN skills IS NULL OR skills = '' THEN 1 ELSE 0 END) AS no_skills, SUM(CASE WHEN location IS NULL OR location = '' THEN 1 ELSE 0 END) AS no_location, COUNT(DISTINCT source_url) AS distinct_url, COUNT(*) - COUNT(DISTINCT source_url) AS dup_count FROM job_posts;zero_salary超过总数 3% 就要回看正则,说明有规模不小的薪资格式没被覆盖。no_skills超过 10% 也要查,说明关键词词表覆盖面不足。dup_count超过 5% 的时候,优先检查内容指纹生成逻辑,而不是直接改数据——真想连同名岗位一起清理,得对比发布时间字段确认是否真的重复。
提示:质量校验的输出结果存成
quality_report.csv,做可视化之前先看这份报告,能避免「图表做完了才发现数据是脏的」这种返工。
4. 北京招聘数据分析与可视化:从指标计算到 pyecharts 大屏
4.1 市场综合指标:平均薪资、中位薪资与岗位总量
分析环节先算三个全局指标:岗位总量、平均薪资、薪资中位数。平均薪资会被少数高管岗拉高,中位数更能代表市场常态,两个都算、图表里都展示,是最稳妥的做法。
import pandas as pd df = pd.read_sql("SELECT * FROM job_posts", engine) total_jobs = len(df) avg_salary = df["salary_mid"].mean().round(1) median_salary = df["salary_mid"].median().round(1) print(f"岗位总量: {total_jobs}") print(f"平均月薪(中位值): {avg_salary}K") print(f"月薪中位数: {median_salary}K")这三个数直接放进可视化大屏顶部的 KPI 卡片里。额外可以算一个「薪资区间分布」,把月薪分成 10K 以下、10-20K、20-30K、30-40K、40K 以上五档,统计每档岗位数占比。分档后的柱状图比原始薪资分布直方图更容易读出结论。
4.2 技能薪资溢价:Python/Java/Spark 谁更值钱
技能分析是招聘数据最有价值的部分。把skills字段展开成一行一技能的长表,再按技能分组计算平均薪资,就能看到不同技术方向的身价差异。
skills_df = df.dropna(subset=["skills"]).copy() skills_df["skill_list"] = skills_df["skills"].str.split(",") rows = [] for _, row in skills_df.iterrows(): for skill in row["skill_list"]: rows.append({"skill": skill.strip(), "salary_mid": row["salary_mid"]}) skill_salary = pd.DataFrame(rows) skill_stats = ( skill_salary.groupby("skill") .agg(avg_salary=("salary_mid", "mean"), job_count=("salary_mid", "count")) .sort_values("job_count", ascending=False) .head(20) ) print(skill_stats)groupby之后按job_count排序取前 20,避免只出现一两次但薪资奇高的冷门技能干扰判断。展示时做双轴图:柱状图是岗位数量、折线是平均薪资,一眼能看出「需求量大的技能薪资不一定最高」这类反直觉结论。
4.3 学历与经验结构分布:饼图与堆叠图的数据口径
学历分布直接按清洗后的education字段分组计数,经验要求同理。需要注意口径:有些平台把「大专」和「本科」混在一起写「大专/本科」,清洗时要把这种合并项里更低的学历作为主判断,否则饼图会出现重叠类别。
edu_order = ["大专", "本科", "硕士", "博士", "不限"] edu_dist = ( df.groupby("education")["id"] .count() .reindex(edu_order, fill_value=0) .reset_index() ) edu_dist.columns = ["education", "count"] print(edu_dist)reindex保证图表里类别的排列顺序固定,不会每次运行都变。经验字段同理拆成「应届/1-3年/3-5年/5-10年/10年以上」五档,学历和经验做成交叉堆叠图,能看出「硕士学历集中出现在哪些经验段」这类更细的结构特征。
4.4 可视化大屏:pyecharts 组件布局与数据对接
可视化部分我选用 pyecharts,它支持链式调用、生成独立 HTML,不需要额外部署前端服务。大屏默认布局是一行三列:顶部 KPI 卡片、左侧技能榜单、中间学历饼图、右侧薪资分布柱状图、底部词云。
from pyecharts.charts import Bar, Pie, WordCloud from pyecharts import options as opts salary_hist = Bar() salary_hist.add_xaxis(["0-10K", "10-20K", "20-30K", "30-40K", "40K+"]) salary_hist.add_yaxis("岗位数", salary_dist_list) salary_hist.set_global_opts( title_opts=opts.TitleOpts(title="北京大数据岗位薪资分布"), yaxis_opts=opts.AxisOpts(name="岗位数"), ) salary_hist.render("charts/salary_dist.html")链式调用的核心是set_global_opts里的title_opts与axis_opts,标题和坐标轴名称必须写清楚,否则看图表的人不知道横轴是薪资档位还是经验年限。每个render()生成独立 HTML 文件,多个图表放在同一个charts/目录下,最后用 iframe 拼到一张总览页里。
词云图用岗位名字段生成,能直观反映当前市场需求量最大的岗位关键词。WordCloud 组件默认支持中文分词,但是要保证数据里没有乱码——清洗阶段所有文本统一utf-8编码,从源头上避免这个问题。
4.5 数据看板整体串联与手动刷新
所有图表生成后,写一个run_analysis.py统一调度:读库 → 算指标 → 生成图表 HTML。改成一个带手动刷新的大屏页面,用 Flask 提供本地服务,每次访问时重跑一次分析脚本,保证看板数据跟上最新的增量抓取结果。这样整个项目从爬虫到展示形成了一个闭环:今天凌晨爬的新数据,中午打开看板就能看到更新后的统计口径。
Flask 版大屏不需要复杂模板,一个路由直接拼 iframe 就好:
from flask import Flask, render_template_string app = Flask(__name__) @app.route("/") def dashboard(): html = """ <div style="display: grid; grid-template-columns: 1fr 1fr;"> <iframe src="/static/salary_dist.html"></iframe> <iframe src="/static/skill_bar.html"></iframe> </div> """ return render_template_string(html) app.run(host="127.0.0.1", port=5000, debug=False)grid-template-columns: 1fr 1fr把两个图表并列排布,iframe直接引用 pyecharts 生成的静态文件。没有用复杂的前端框架,整个看板就是纯 HTML 拼装,新人也能看懂结构。
5. 避坑:招聘数据项目最常见的 5 个翻车现场
5.1 薪资中位数算出来翻倍:正则的贪婪匹配
现象:清洗后统计平均薪资达到 40K+,明显高于招聘网站肉眼看到的水平。
原因:正则\d+在匹配「20-40K·14薪」时,把14薪前面的14也当成了薪资下限。原始正则没有限定只在K前面取数字,导致14被当成一个新的薪资档位。
解决:正则改成从数字开始,强制要求以K/k结尾,且只取第一个匹配段。用re.findall输出所有匹配结果逐一检查,确认只有「20」「40」两个数字被提取。加一条校验规则兜底——salary_max小于salary_min的记录直接置 0 并在日志里打印原始字符串。
5.2 请求频率没控制好:封 IP 之后只能干等
现象:前 200 个请求正常,第 250 个开始持续返回 403,浏览器手动访问也出现验证码,说明 IP 级访问被限制。
原因:请求间隔用了固定time.sleep(1),同一 IP 的请求节奏太规律,被风控识别为脚本行为。
解决:间隔改成random.uniform(1.5, 3.5),每次请求之间随机休息 1.5 到 3.5 秒。同时加请求前延时和失败后的指数退避——这个方案能应对绝大多数反爬策略。如果已经封了,就停一天让限制自动解除,赢了对抗也丢了数据,不划算。
5.3 数据重复率超过 30%:来源 URL 和内容指纹两个约束都要有
现象:洗完数据用source_url查重,重复率不高;但按「公司+岗位+薪资」查,重复率超过 30%。同一公司连续几天发布同一批岗位,URL 不同,内容几乎一样。
原因:单独靠 URL 去重挡不住「同岗位重新上架」的情况,原 URL 和重新上架的 URL 不同,内容却完全相同。
解决:内容指纹去重逻辑里加入publish_date,超过 30 天的同内容记录允许重新入库,新发布的看做新岗位。同时job_posts表保留复合唯一约束(company_name, job_title, salary_raw, publish_date),数据库层面兜底。
5.4 Selenium 无头模式加载不出内容:等待条件写错位置
现象:无头模式打开页面后,find_elements返回空列表,但去掉--headless后正常。
原因:页面内容是滚动懒加载的,初始化时只渲染首屏,后续内容需要滚动触发。无头模式窗口高度默认 800px,首屏内容太少,等待条件presence_of_element_located已经满足,往下找卡片就找不到了。
解决:等待条件改成presence_of_all_elements_located,并且循环执行滚动直到页面高度不再变化。滚动代码加上之后,还要配合WebDriverWait等待特定数量的卡片出现。另一个细节是--window-size必须设置大尺寸,首屏渲染的内容越多越稳。
5.5 词云全是「岗位职责」「任职要求」:停用词表不过关
现象:词云图生成后,占据视觉中心的全是「岗位职责」「任职要求」「具有良好的」这些套话,真正的技术词被挤到边缘。
原因:岗位描述里高频出现的 JD 模板语言词频远高于真实技能词,词云按词频布局,套话自然占据核心位置。
解决:建一个业务停用词表,把「岗位职责」「任职要求」「具备」「优先」「良好」「相关」「以上学历」这类词全部过滤掉,再跑词频统计。停用词表要按招聘场景专门维护,通用中文停用词表覆盖不到「优先」「熟悉」这类招聘特有词。
提示:这五个坑不是全部,但覆盖了从采集到展示的最常见事故点。项目做到后面你会发现,维护成本最大的不是写代码,而是持续调清洗规则和停用词表。
6. 进阶:数据质量自查清单与增量看板自动化
项目跑通之后,我给自己定的验收标准是三句话:数据能自圆其说、图表能回答业务问题、整套流程两周后还能跑。前两条靠分析逻辑,最后一条靠自动化。每天凌晨跑一次爬虫脚本已经是基本功,但很多人的脚本跑两周就开始报错——不是代码坏了,是数据质量下滑导致清洗逻辑崩了。
我习惯每次爬虫任务结束后自动跑一套数据质量自查,把结果写进日志:当天空值率有没有超过 5%、薪资解析失败率有没有超过 3%、重复率有没有超过阈值、新增岗位数是不是接近 0。这四个数任何一个异常,就发一封提醒邮件,等白天再排查。新增岗位数接近 0 这种情况大概率是页面结构变了,HTML 选择器匹配不到内容,这是周期性任务最隐蔽的失败模式,不跑质量自查根本发现不了。
看板侧可以做两个小优化。第一个是把凌晨爬完的数据直接写入一个data_processed.csv,可视化脚本只读取这个 CSV,不直接碰数据库,数据链路变成「爬虫 → 库 → CSV → 图表」,每层职责清晰。第二个是给 KPI 卡片加环比:今天的岗位总量和昨天比涨了还是跌了,用df["crawl_time"].dt.date分组就能算出来。环比数字放进大屏后,看板从「展示现状」升级为「展示变化」,给面试官讲的时候也更有话可说——「我不仅分析了静态分布,还跟踪了趋势」。
最后说一个我自己的习惯:每次改完清洗或分析脚本,先把原始数据备份一份,再跑全流程,最后对比新旧两份图表输出的差异。这个动作让我避开过好几次「改完正则,前两周的数据全部解析失败」这种自己给自己挖的坑。先备份、再修改、后对比,这套流程已经成了我做数据项目的肌肉记忆,希望帮到你。
本文还有配套的精品资源,点击获取