news 2026/10/3 8:57:59

Python招聘数据爬虫分析与可视化:从爬取到看板的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python招聘数据爬虫分析与可视化:从爬取到看板的完整链路

简介:这是一套基于Python的招聘数据爬虫分析与可视化课程设计源码包,面向计算机专业学生及爬虫数据分析初学者,可用于期末大作业、课程设计或毕业项目实践。项目覆盖从招聘网站页面爬取、字段解析清洗、MySQL入库存储到Flask后台服务与多维度可视化展示的完整技术链路,学业评估98分,代码经调试测试可直接部署。资源包共73个文件,约10.48MB,主体为py源码与pyc编译文件,配合sql数据库脚本、xls数据表、图表及前端静态页面,另有pptx演示文稿与README说明文档。演示文稿与说明文档可帮助使用者理解腾讯招聘、51job岗位信息采集、城市坐标转换及数据分析可视化的全流程实现思路。平台目前已有28人学习下载,适合希望快速借鉴完整项目框架、掌握工程化爬虫项目组织方式的开发者。

1. 招聘数据爬虫分析与可视化:一条能把简历调研跑通的完整链路

「Python招聘数据爬虫分析与可视化系统」听起来像课程大作业,实际上它是一个很实用的个人项目:把招聘网站上「岗位名称、公司、城市、薪资、经验要求、技能描述」抓下来,清洗成一张干净规整的二维表,再用图表把城市岗位分布、薪资中位数、技能需求量呈现在浏览器看板里。对求职者,它能快速回答「我这个方向在哪个城市岗位多、钱多」;对正在学 Python 的人,它是一段能串起 python爬虫、数据分析、可视化三块技能的完整代码。下面的方案按最小可运行组合拆开:requests 抓页面,BeautifulSoup 解析,SQLAlchemy + SQLite 落库,pandas 洗数据,pyecharts 出图,最后用 Flask 拼成一个可视化大屏。

2. 爬虫采集层:用 requests 抓招聘网站,用 SQLAlchemy + SQLite 落库

2.1 选型:为什么不是 Scrapy,而是 requests + BeautifulSoup

招聘网站列表页大多数是服务端渲染的 HTML,岗位名称、薪资、公司名直接写在 DOM 里。这类页面用 requests 发一个 GET 请求,再用 BeautifulSoup 解析,十几行代码就能拿到结构化数据。网络爬虫原理就是这样:构造请求、获取响应、解析提取、循环翻页。

那为什么不直接上 Scrapy?Scrapy 的并发和去重确实强,但它有一套自己的 Twisted 调度体系,项目拆成 items、spiders、pipelines,新手拿到源码先被框架本身绊住。这个场景单机、单任务、每天抓几百页,requests 完全够用,出问题也容易定位。同理,除非目标站点是纯 JavaScript 动态渲染、接口又难找,否则不建议一开始就上 python selenium 反爬虫那套重型方案,Selenium 慢、耗内存,放到静态页上是杀鸡用牛刀。

存储层我用 SQLAlchemy + SQLite。SQLite 零配置文件即用,SQLAlchemy 的 ORM 模型让代码可读性高,后面 pandas 分析时用 SQLAlchemy engine 直接读表,非常顺手。网上这套系统的源码喜欢用 MySQL,但给本地学习用的项目配 MySQL 属于给自己找事:装服务、配账号、处理远程连接权限,跑通成本全花在环境上。

2.2 岗位表结构设计:先想清楚要分析什么,再动手建表

很多项目翻车的第一个原因就是字段没设计好,抓完发现「经验要求」和「学历要求」挤在一个字符串里,后期清洗非常痛苦。我建表的原则是:一个字段只放一种语义,能拆的提前拆。

# models.py from sqlalchemy import create_engine, Column, Integer, String, Text from sqlalchemy.orm import declarative_base, sessionmaker DB_URI = "sqlite:///../data/jobs.db" engine = create_engine(DB_URI, echo=False) Base = declarative_base() class JobPosting(Base): __tablename__ = "job_posting" id = Column(Integer, primary_key=True, autoincrement=True) url = Column(String(500), unique=True, index=True) # 岗位详情页URL,唯一键,防重复 title = Column(String(200)) # 岗位名称 company = Column(String(200)) # 公司名称 city = Column(String(50)) # 城市 salary = Column(String(100)) # 原始薪资字符串,保留现场 experience = Column(String(100)) # 经验要求文案 education = Column(String(100)) # 学历要求文案 description = Column(Text) # 岗位描述正文,技能词统计的材料 published_at = Column(String(50)) # 发布时间 crawl_time = Column(String(50)) # 抓取时间,增量抓取时用 Base.metadata.create_all(engine) Session = sessionmaker(bind=engine)

逻辑说明:url字段加unique=True是整张表最重要的设计,同一个岗位详情页的 URL 稳定不变,拿它当唯一键,重复抓取时数据库层面就能挡住。salary字段存原始字符串而不是直接拆好的数值,这是个「后悔药」设计——解析规则写错了,原始数据还在,改完规则重跑清洗即可。description存岗位描述全文,技能词统计全靠它,不能省。

参数说明:echo=False关闭 SQLAlchemy 的 SQL 日志,否则控制台会被刷屏;String(500)留给 URL,短了会报数据超长错误。

2.3 列表页采集:requests 请求与 BeautifulSoup 解析

定义好表结构,写采集函数。核心是三步:组装 headers 发请求、用 BeautifulSoup 找职位卡片、把每个卡片字段填进 ORM 模型。

# crawler/spider.py import time import random import requests from bs4 import BeautifulSoup from models import Session, JobPosting BASE_URL = "https://example.com/jobs?page={}" # 替换为目标站列表页模板 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_list_page(page: int): """抓取列表页,返回 BeautifulSoup 对象;失败时返回 None""" url = BASE_URL.format(page) try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code != 200: print(f"page {page} status_code: {resp.status_code}") return None resp.encoding = "utf-8" return BeautifulSoup(resp.text, "html.parser") except requests.RequestException as e: print(f"page {page} error: {e}") return None def parse_list_page(soup): """从职位卡片中提取字段,返回字典列表""" items = [] # 以下选择器按目标站实际HTML结构调整,当前写法是通用占位 for card in soup.select("li.job-card"): link = card.select_one("a.job-title") if not link: continue items.append({ "url": link.get("href"), "title": link.get_text(strip=True), "company": card.select_one(".company-name").get_text(strip=True), "city": card.select_one(".job-city").get_text(strip=True), "salary": card.select_one(".salary").get_text(strip=True), "experience": card.select_one(".exp").get_text(strip=True), }) return items def save_jobs(items): """入库前用URL去重;已存在则跳过""" session = Session() saved = 0 for it in items: if not it["url"]: continue exists = session.query(JobPosting).filter_by(url=it["url"]).first() if exists: continue session.add(JobPosting(**it)) saved += 1 session.commit() session.close() return saved

逻辑说明:parse_list_page里的选择器是占位写法,不同招聘网站 HTML 差异极大,拿到这套源码后第一件事就是把li.job-card、.job-title、.company-name改成目标站实际类名。save_jobs先按 URL 查一次再插入,配合表的唯一索引,重复抓同一页不会产生重复数据。

参数说明:timeout=10是请求超时兜底,网络抖动时不至于让整个脚本卡死;resp.encoding = "utf-8"是手动指定解码,部分站点的响应头没写 charset 或写错,不指定的话中文会变乱码。.get_text(strip=True)会去掉标签内文字首尾空白,比直接.text干净。

2.4 翻页循环与限速:把「能抓一页」变成「能抓全站」

单页解析没问题后,写循环。招聘站点普遍有反爬,限速是必须的。我一般让每次请求间隔 0.5 到 1.5 秒随机浮动,固定间隔容易被识别成脚本,随机间隔能明显降低触发验证码的概率。

# crawler/run_spider.py import time import random from spider import fetch_list_page, parse_list_page, save_jobs def crawl(max_pages=30): for page in range(1, max_pages + 1): soup = fetch_list_page(page) if soup is None: continue items = parse_list_page(soup) if not items: print(f"page {page}: no items, maybe blocked") break saved = save_jobs(items) print(f"page {page}: parsed {len(items)}, new {saved}") time.sleep(random.uniform(0.5, 1.5)) # 随机延时,降低请求特征 if __name__ == "__main__": crawl(30)

逻辑说明:if not items判断很关键,很多站点反爬不是返回 403,而是返回一个内容为空的正常页面。如果连续两页解析不到职位卡片,说明触发了风控,继续翻页只会加重封禁,直接break保留现场数据,等冷却后再从当前页续抓。

参数说明:max_pages=30是单轮上限,具体值看目标站总岗位数和你的耐性,建议首次跑 5 页验证链路,再放量。延时random.uniform(0.5, 1.5)表示每次等待 0.5 到 1.5 秒之间的随机值——单位是秒。

提示:不要对任何招聘网站发起高频、高并发抓取。这套代码的量级控制在单机、低频率、个人学习与调研范围内;抓下来的数据只做统计分析,不要二次发布或用于商业用途。

3. 数据清洗层:把薪资、城市、技能词从乱文本里抠出来

3.1 用 pandas 把 SQLite 读成分析表

爬虫落库之后,数据是「能看但没法算」的状态:薪资是15-25K·14薪这种字符串,城市是广州-天河区这种带区划的写法,技能全堆在description长文本里。数据分析的第一步是把表读成 DataFrame。

# analysis/load_data.py import pandas as pd from models import engine df = pd.read_sql("SELECT * FROM job_posting", engine) print(df.shape) print(df.head(3))

pd.read_sql直接接收 SQLAlchemy 的 engine,不需要自己拼连接串。df.shape先看行数列数,df.head(3)看前三条长什么样。拿到手先检查空值数量,df.isnull().sum()对每一列统计缺失值,后续清洗才有方向。这一步不需要写入任何中间文件,内存里操作最方便。

3.2 薪资字符串拆分:统一成「月薪中位数(千/月)」

薪资字段是整张表里最需要小心的。常见格式有15-25K、1-1.5万/月、30-50K·14薪、面议、200-300/天。如果不做单位归一,后面一算平均值全是错的。我的做法是写一个净化函数,把字符串转成「月薪下限、上限、中位数」三个数值字段,单位统一为千/月。

# analysis/clean_salary.py import re import pandas as pd def parse_salary(s): """把薪资字符串转成 (下限K, 上限K, 中位数K);无法解析返回 (None, None, None)""" if not isinstance(s, str) or "面议" in s: return None, None, None # 提取数值区间,兼容 "15-25K"、"1-1.5万"、"30-50K·14薪" nums = re.findall(r"(\d+\.?\d*)", s) if len(nums) < 2: return None, None, None low, high = float(nums[0]), float(nums[1]) # 单位归一:万/月 -> 千/月,乘以10 if "万" in s: low, high = low * 10, high * 10 # 如果写的是年薪或日薪,这里需要单独处理,建议先过滤再解析 if "年" in s or "天" in s: return None, None, None mid = round((low + high) / 2, 1) return low, high, mid df["salary_low"], df["salary_high"], df["salary_mid"] = zip( *df["salary"].map(parse_salary) ) print(df[["salary", "salary_low", "salary_high", "salary_mid"]].head(5))

逻辑说明:先判断单位再转数值,万出现时乘 10 统一成 K。出现「年薪」「日薪」的样本直接置空,宁可少统计也不能把口径不同的数据混进月薪均值。zip(*df["salary"].map(parse_salary))把函数返回的三列展开赋给三个新字段。

参数说明:正则\d+\.?\d*能匹配15和1.5这类整数与小数;面议在解析前就被过滤。实际项目里还会遇到薪资面议、5-8千/月这种「千」字格式,需要再加一层单位判断,规则如下:出现千不缩放,出现万乘 10,什么都不写默认 K。

3.3 城市字段归并:解决「广州」「广州-天河区」「广州(番禺)」并存

招聘网站的 city 字段看似规整,实际上同一个城市有多种写法:广州、广州-天河区、广州(番禺)、广东-广州。做城市维度分析前必须归并。

# analysis/clean_city.py import re def normalize_city(city): """把带区划的城市字段归并成地级市名称""" if not isinstance(city, str): return city city = city.strip() # 去掉括号内容,如 (番禺)、-天河区 等后缀 city = re.sub(r"[((].*?[))]", "", city) city = re.sub(r"[--].*$", "", city) return city df["city_clean"] = df["city"].map(normalize_city) print(df["city_clean"].value_counts().head(10))

逻辑说明:两个正则分别处理括号后缀和横线后缀,[((].*?[))]用非贪婪匹配去掉括号里的区划,[--].*$从横线开始截断。value_counts()是最直观的核对方式,归并后如果还有广州天河区这种无标点粘连,再按字典手动映射。

参数说明:re.sub的替换结果是新字符串,原字段保留不动,这样万一归并规则写错还能回到原始数据重来。城市归并是典型的「看起来简单、实际坑多」环节,跑完一定要肉眼抽查几十条,别信正则一次到位。

3.4 技能关键词统计:Java、Javascript、Python 不能混

技能需求量是招聘分析里最有价值的部分,统计方法却不复杂:准备一份技能关键词表,逐个去description里匹配。真正的坑在于大小写和词边界——javascript里面包含java,简单用"java" in text会把所有 Javascript 岗位都算进 Java 里。

# analysis/skill_stats.py import re from collections import Counter SKILLS = ["Python", "Java", "Javascript", "Vue", "React", "Spring", "Docker", "Kubernetes", "Redis", "MySQL", "Hadoop", "Spark", "Flink", "Linux"] def count_skills(descriptions): """统计技能词在岗位描述中出现的次数,英文词按边界匹配""" counter = Counter() for desc in descriptions: if not isinstance(desc, str): continue text = desc.lower() for skill in SKILLS: key = skill.lower() if key == "java": # 用正则词边界匹配,避免命中 javascript/java 的子串 if re.search(r"\bjava\b", text): counter[skill] += 1 else: if key in text: counter[skill] += 1 return counter skill_counter = count_skills(df["description"].tolist()) for skill, count in skill_counter.most_common(10): print(f"{skill}: {count}")

逻辑说明:\bjava\b里的\b是单词边界,能把java从javascript里区分出来。中文里没有空格分词,所以 Vue、React 这类英文词用简单in判断即可;如果描述里出现Java开发工程师,\bjava\b匹配java后面的空格或标点,能正常命中。

参数说明:关键词表SKILLS按目标岗位方向自行扩展,比如分析前端岗位就加Vue、React、Webpack,分析大数据就加Hadoop、Spark、Flink。Counter.most_common(10)输出频次前十,这就是后面可视化图表的原料。想在清洗阶段就用上分词工具的话,可以引入 jieba 做中文分词再统计,但词表匹配在岗位描述这种半结构化文本上更快、结果更可控。

4. 可视化层:用 pyecharts + Flask 把统计结果拼成招聘大屏

4.1 pyecharts 2.x 的链式写法:底层就是 ECharts

pyecharts 生成的图表本质上是一套 ECharts 配置,Python 负责算数据、拼 JSON,浏览器负责渲染。网上很多老源码用的是 pyecharts 0.5.x 的写法,Bar("标题")这种实例化方式在 2.x 里已经废了,新版本统一用链式写法,数据配置和样式配置分开,代码结构更清晰。

岗位分析看板通常放四张图:城市岗位数量柱状图、城市平均薪资横向柱状图、技能需求 Top 10 条形图、学历要求饼图。下面先用 pyecharts 生成前两张图。

4.2 从 DataFrame 到图表:城市岗位分布与薪资对比

# visual/charts.py from pyecharts.charts import Bar, Pie from pyecharts import options as opts def city_job_bar(city_counts): """城市岗位数量柱状图""" cities = city_counts.index.tolist() counts = city_counts.values.tolist() bar = ( Bar(opts.InitOpts(width="800px", height="400px")) .add_xaxis(cities) .add_yaxis("岗位数量", counts, category_gap="40%") .set_global_opts( title_opts=opts.TitleOpts(title="城市岗位数量 Top20"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=45)), yaxis_opts=opts.AxisOpts(name="岗位数"), ) ) return bar def city_salary_bar(city_salary): """城市平均薪资横向柱状图""" cities = city_salary.index.tolist() salaries = city_salary.values.tolist() bar = ( Bar(opts.InitOpts(width="800px", height="500px")) .add_xaxis(cities) .add_yaxis("平均月薪(K)", salaries) .reversal_axis() # 横向排列 .set_global_opts( title_opts=opts.TitleOpts(title="城市平均月薪 Top20"), xaxis_opts=opts.AxisOpts(name="薪资(K/月)"), ) ) return bar

逻辑说明:city_counts来自第 3 章的df["city_clean"].value_counts(),DataFrame 的 index 是城市名、values 是对应数量;city_salary来自df.groupby("city_clean")["salary_mid"].median().sort_values()。两张图对同一个数据中心做不同分组聚合,体现的是清洗结果,不需要额外计算。

参数说明:opts.InitOpts(width="800px", height="400px")控制画布尺寸;rotate=45让 X 轴城市名倾斜 45 度,否则城市名互相遮挡;reversal_axis()把柱状图转成横向,城市名放左侧更易读。pyecharts 2.x 里category_gap控制柱子间距,数值越大柱越细。

4.3 可视化大屏:Flask 本地服务 + 多图表拼接

图表对象生成后,怎么把它们拼成一个可浏览的页面?我一般用 Flask 起本地服务,在路由里把每张图渲染成 HTML 片段,再嵌入一个拼好的大屏模板。pyecharts 2.x 中,图表对象调用.render_embed()就能输出完整的<div>和<script>片段,直接塞进 Jinja2 模板即可。

# visual/app.py from flask import Flask, render_template from charts import city_job_bar, city_salary_bar app = Flask(__name__) @app.route("/") def dashboard(): # 实际数据从第3章清洗后的 df 聚合而来,这里省略重复计算 city_counts = get_city_counts() city_salary = get_city_salary() job_bar = city_job_bar(city_counts) salary_bar = city_salary_bar(city_salary) return render_template( "dashboard.html", job_bar=job_bar.render_embed(), salary_bar=salary_bar.render_embed(), ) if __name__ == "__main__": app.run(host="127.0.0.1", port=8000, debug=False)
<!-- templates/dashboard.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>招聘数据分析看板</title> <style> .chart-container { display: flex; flex-wrap: wrap; gap: 16px; padding: 16px; } .chart-box { border: 1px solid #eee; border-radius: 8px; padding: 8px; } </style> </head> <body> <h2 style="padding-left:16px;">招聘数据可视化大屏</h2> <div class="chart-container"> <div class="chart-box">{{ job_bar | safe }}</div> <div class="chart-box">{{ salary_bar | safe }}</div> </div> </body> </html>

逻辑说明:render_template把图表 HTML 片段传进模板,| safe是 Jinja2 的过滤器,告诉模板引擎这段内容是可信 HTML,不要转义。这个页面只放了两张图,实际项目可以按同样方式继续加 Pie、Map、WordCloud,栅格布局由 CSSflex-wrap控制。

参数说明:app.run(host="127.0.0.1", port=8000, debug=False)里debug=False是运行模式,本地调试可以开 True,但开 True 时 Flask 会加载重载器,多进程下日志会很吵。render_embed()相比chart.render("chart.html")的优点是不落文件,模板渲染时动态生成,适合数据每天更新的场景。

5. 招聘数据爬虫项目的 5 个高频翻车点:现象、原因与解法

5.1 网上拿到的源码跑不起来,九成是版本问题

现象:pip install -r requirements.txt之后运行,报ModuleNotFoundError: No module named 'pyecharts',或者提示Bar初始化参数不对、chart.render()找不到文件。

原因:Python 版本不同,依赖库版本不同。老源码多半基于 pyecharts 0.5.x,new版本的 API 变化很大;还有的源码要求的flask、sqlalchemy版本和你环境里的不一致。很多人第一反应是怀疑代码有问题,实际上环境不匹配才是黑匣子。

解决:先python --version确认解释器版本,再pip list看关键库版本。建议建虚拟环境,按源码附带的requirements.txt原样安装,不要用全局环境跑;如果源码没带依赖清单,就按报错栈逐个排查,通常是pyecharts的写法不兼容。老代码改新 API 的工作量不小,看你手里的源码质量决定是改还是重写。

5.2 列表页浏览器能打开,爬虫却拿不到数据

现象:requests 请求返回 200,但解析结果为空,或者页面内容里全是登录框、验证码组件;有的站点返回 403 Forbidden。

原因:浏览器请求带了完整的请求头,尤其是User-Agent和Cookie,而脚本只带了 UA,站点风控直接拒绝。更隐蔽的情况是「页面照常返回,但岗位列表是通过接口异步加载的」,静态 HTML 里根本没有职位卡片,BeautifulSoup 自然捞不到。

解决:先打印响应前 500 个字符,肉眼判断返回的是不是正常页面。如果是登录页,打开浏览器手动登录一次,把Cookie复制进HEADERS;如果是异步加载,按 F12 找数据接口,直接抓 JSON 比解析 HTML 更稳。遇到反爬加塞 Selenium 不是首选,先试试「带 Cookie + 随机延时 + 低频」这套组合。

5.3 平均薪资算出离谱数字,几万和几十万混在一起

现象:按城市算平均月薪,出来一个三千,另一个三十万,图表完全没法看。

原因:15-25K·14薪里的14薪被正则当成第二个数字取走了;1-1.5万/月没做单位转换,按 K 计算直接差 10 倍;200-300/天的日薪被当成月薪参与均值。

解决:清洗函数里加三条规则——第一,14薪这类数字必须排除,正则改为只取「数字 + 单位」紧挨着的片段;第二,单位万出现时整体乘 10;第三,天、年、小时出现的直接置空,不参与月薪统计。调完规则后打印salary_mid.describe(),看min、max、median是否符合常识。薪资清洗没有捷径,每个异常格式都来自真实数据,遇到一条补一条规则。

5.4 控制台打印正常,CSV 导出后中文全乱码

现象:pandas 输出到控制台是正常中文,df.to_csv("data.csv")后用 Excel 打开全乱码,Notepad 打开正常。

原因:to_csv默认用 UTF-8 编码写文件,Excel 打开 CSV 时默认按 GBK 解码,两边对不上。控制台不乱码是因为终端用的就是 UTF-8,掩盖了文件编码问题。

解决:导出时指定encoding="utf-8-sig"。utf-8-sig会在文件头部写入 BOM 标记,Excel 识别到 BOM 后自动按 UTF-8 解码。同样的问题也出现在 requests 抓取时,resp.encoding不手动指定就可能用错字符集,抓回来就是乱码,这一步在爬虫里就要处理好。

df.to_csv("data/export.csv", index=False, encoding="utf-8-sig")

5.5 重复跑两次爬虫,图表数量翻倍

现象:第一次抓完 1000 条,第二次再跑变成 2000 条,技能词统计和城市分布全部失真。

原因:没有去重逻辑。程序重跑时同一个岗位详情页被重复入库,ORM 里也没做存在性检查。后面图表都是基于count()做的聚合,重复数据直接影响所有统计结果。

解决:两层防护。第一层在表结构上,url字段设unique=True;第二层在save_jobs里先query.filter_by(url=...)判断再插入。满数据库的去重本身也有成本,岗位量到几十万级别后,可以换redis集合做 URL 指纹去重,抓之前先SADD判断是否已存在,内存操作比查 SQLite 快一个数量级。

6. 给系统加个增量抓取:让数据每天自动更新

现在整套链路能跑通,但每次手动执行run_spider.py、clean_data.py、app.py三步很烦,而且全量重抓会重复消耗目标站资源。常见做法是把抓取改成增量模式:每次只抓「前几页里上次没见过的新岗位」,配合定时任务每天自动执行一次。

增量判断不需要额外字段,URL 唯一键就够了。抓取前先查一下最新一条crawl_time,或者直接每次抓前 5 页,URL 已在库里的跳过,新的入库。新岗位更新间隔一般不会太短,每天抓一次前几页足够跟上节奏:

# scheduler.py from apscheduler.schedulers.blocking import BlockingScheduler from crawler.run_spider import crawl from analysis.clean_salary import run_clean from visual.charts import refresh_charts def daily_job(): crawl(max_pages=10) # 增量抓取 run_clean() # 重新清洗 refresh_charts() # 重新生成图表 if __name__ == "__main__": scheduler = BlockingScheduler(timezone="Asia/Shanghai") scheduler.add_job(daily_job, "cron", hour=2, minute=30) print("scheduler started, daily job at 02:30") scheduler.start()

逻辑说明:APScheduler的cron触发器指定每天凌晨 2 点 30 分执行,这个时段目标站负载低,反爬也相对不敏感。crawl(max_pages=10)只抓前 10 页,已存在的 URL 自动跳过,相当于每天只采集新增岗位。清理和图表刷新放在同一个任务里,数据更新后看板自动跟上。

验证方法很简单:第一次跑完记录df.shape[0],第二天同一时间看这个数字,如果比前一天大,说明增量生效;如果连续几天不变,不是没新岗位就是被反爬拦截了,去查看日志里的no items, maybe blocked。岗位量到了百万级,再去引入 redis 做 URL 去重,抓取前先判断成员是否存在,比查数据库更省。

我自己做这类项目有个习惯:清洗函数永远比爬虫函数先写。爬虫跑出来的数据是脏的,清洗规则写清楚后,爬虫的字段设计才有依据;反过来先抓了一堆数据再想怎么洗,规则改了还要回头重抓,那才是真折腾。把这套链路跑通之后,换数据源、换分析维度、换图表样式都是顺手的事,骨架没变,变的是解析规则和展示形式。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 8:57:21

基于机器学习的IDS系统Python源码:决策树从训练到Flask部署全解析

简介&#xff1a;一份基于机器学习的入侵检测系统&#xff08;IDS&#xff09;完整Python项目源码包&#xff0c;面向计算机相关专业毕业生、课程设计或期末大作业需求者&#xff0c;以及希望上手实战机器学习项目的学习者。代码经导师指导并评审通过&#xff0c;得分99分&…

作者头像 李华
网站建设 2026/10/3 8:56:49

Python全栈股票分析系统:可验证、可审计、可上线的金融工程实践

简介&#xff1a;这是一套基于Python构建的全栈股票分析系统源码&#xff0c;面向金融数据分析初学者、量化研究爱好者及Python进阶开发者&#xff0c;解决股票数据获取难、分析流程割裂、可视化能力弱等实际问题。资源共172个文件&#xff0c;涵盖29个核心Python模块&#xff…

作者头像 李华
网站建设 2026/10/3 8:56:15

ARIMA-SSA-LSTM时序预测三重滤镜原理与落地实践

简介&#xff1a;本资源是一套面向高校计算机、电子信息与数学专业学生的Python时间序列预测实战方案&#xff0c;聚焦ARIMA-SSA-LSTM混合建模方法&#xff0c;解决非平稳、含噪声时序数据的高精度预测问题&#xff0c;适用于课程设计、期末大作业及毕业设计等实践场景。压缩包…

作者头像 李华
网站建设 2026/10/3 8:55:12

共享单车调度源码实战:BP神经网络需求预测与蚁群算法路径优化

简介&#xff1a;这份资源是一套基于神经网络与蚁群算法实现的共享单车调度系统源码&#xff0c;面向计算机、人工智能、数据科学等相关专业的在校学生与从业人员&#xff0c;可用于毕业设计、课程设计、大作业或竞赛初期项目立项。项目围绕最优单车调度路径展开&#xff0c;涵…

作者头像 李华
网站建设 2026/10/3 8:54:09

数据管道中的数据集扩充:从算子到调度,提升模型真实场景表现

简介&#xff1a;这份资源面向机器学习与深度学习方向的开发者及学习者&#xff0c;聚焦图像数据集扩充这一常见需求&#xff0c;提供一份可直接参考的Python数据处理脚本。当训练数据有限时&#xff0c;通过旋转、翻转、裁剪、缩放、平移、颜色抖动、噪声注入等变换人为增加样…

作者头像 李华
网站建设 2026/10/3 8:52:27

Python二手房数据分析源码:从CSV清洗到可视化全流程

简介&#xff1a;这份资源是面向高校学生与Python初学者的一套二手房数据分析完整项目&#xff0c;可直接用于毕业设计、期末大作业或课程设计场景。项目以Python为核心&#xff0c;围绕二手房数据的采集、清洗与可视化分析展开&#xff0c;包含从原始数据到清洗后数据的多版本…

作者头像 李华