ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

招聘岗位数据爬虫与可视化分析:从Scrapy到pyecharts的完整实现

招聘岗位数据爬虫与可视化分析:从Scrapy到pyecharts的完整实现 简介一套基于Python的招聘岗位数据爬虫与可视化分析完整项目面向正在准备毕业设计或期末大作业的计算机相关专业学生也适合需要实战练习的Python数据分析初级开发者。包内含五十九个文件主要包含9个.py源码、12个.pyc编译文件、SQL数据库脚本、Flask配置与前端页面、图片素材与演示PPT报告压缩包整体约10.34MB其中Python脚本支撑爬虫与后端逻辑SQL用于数据库初始化图片与PPT用于结果展示源码经严格调试可运行层次清晰便于二次改造。项目曾获导师认可的高分评审内置腾讯招聘爬虫与51job数据集可利用依赖清单文件快速搭建环境配合MySQL完成数据存储与统计图表展示代码结构涵盖爬虫采集模块、Flask服务端和数据分析模块。目前已有113人学习整体难度适中能为理解招聘数据分析全流程提供完整参考无论是课程展示还是毕业答辩都具有较强的参考价值。1. 招聘岗位数据爬虫与可视化分析究竟在解决什么问题招聘岗位的数据爬虫与可视化分析是Python技术栈里少有的、能把requests、Scrapy、Pandas、pyecharts全部串起来的综合型项目。它的核心价值不在于“把网页上的职位信息抓下来”而在于把一条完整的链路走通从数据采集的合法性判断、页面结构分析、字段抽取到数据清洗入库再到用可视化图表回答“哪个城市岗位多、薪资分布如何、技术栈要求是什么”这类实际问题。换句话说一个招聘爬虫系统本质上是一个微缩版的数据工程选型、架构、边界、性能一个都不少。这类项目最常见的落地场景是求职市场分析、薪酬调研、岗位技能趋势跟踪。适合的人群有三类一是正在学习爬虫与数据分析的Python开发者需要一份能讲清楚原理的完整项目二是需要做行业报告或市场调研的运营与HR他们要的是可信的数据结论三是准备写毕业设计或技术总结的工程师需要有架构、有代码、有验证的项目骨架。本文以招聘网站的岗位列表页与详情页为对象从数据源分析开始逐步拆解爬虫的工程化设计、字段清洗与入库、可视化指标设计最后落在并发与反爬的边界处理上确保你拿到的是一套可以照着落地的方案。2. 数据字段设计、反爬边界与招聘网站的技术选型2.1 先看数据源再谈爬虫设计招聘类网站与普通内容站的最大区别在于它们几乎都会在前端渲染职位列表时同步加载结构化的 JSON 数据。这意味着爬虫的第一件事不是解析 HTML而是判断数据的载体。常见的招聘网站有四种数据形态数据形态典型特征爬取策略服务端渲染 HTML职位信息直接写在 DOM 中右键查看网页源代码即可看到岗位名称、薪资等字段直接解析 HTML用 XPath 或 CSS 选择器抽取异步加载 JSON 接口列表页只有框架数据由fetch或XMLHttpRequest请求返回网络面板里能看到 JSON直接请求 API 接口注意请求头与参数签名页面内嵌 JSON 数据数据打包在script typeapplication/json或window.__INITIAL_STATE__中用正则或 JSON 解析提取无需二次请求图片或 PDF 形式部分高端岗位详情以图片方式呈现搜索引擎与爬虫均无法直接读取需要 OCR 或放弃该字段我一般的做法是先用浏览器开发者工具打开列表页查看“网络”面板里的 XHR 请求。如果发现职位数据走的是 JSON 接口就优先抓接口如果接口有复杂的签名参数如signature、token再退回解析 HTML。对招聘岗位这种字段结构相对固定的场景接口通常比 HTML 更稳定因为招聘网站的接口很少频繁改版而页面结构却经常调整。2.2 爬虫的合法性与 robots 协议任何招聘数据爬虫的代码设计都应该从检查robots.txt开始。常见招聘网站的 robots 规则差异很大有的完全开放职位列表页有的对搜索接口做了限制。爬虫脚本运行时应该先请求该文件把禁止爬取的路径过滤掉再进入采集逻辑。from urllib.robotparser import RobotFileParser rp RobotFileParser() rp.set_url(https://example.com/robots.txt) rp.read() can_fetch rp.can_fetch(MySpider/1.0, https://example.com/jobs) print(允许爬取职位列表:, can_fetch)这段代码的逻辑是用RobotFileParser读取目标网站的robots.txt然后传入爬虫的 User-Agent 和待抓取的 URL得到布尔值。User-Agent最好定义成有辨识度的字符串比如MySpider/1.0 (mailto:youremail.com)而不是直接写成浏览器 UA这样网站管理员可以在日志里联系到你。can_fetch返回False时应当跳过该路径这是爬虫工程化的底线。2.3 字段模型先定义你需要的字段爬虫代码的维护成本有一半来自字段规则的变化。所以开工前需要把目标字段抽象成一张表。招聘岗位数据一般包含三类字段基础信息、职位要求、公司信息。字段分组字段名示例值类型说明基础信息job_namePython开发工程师string职位名称基础信息salary_min20int最低月薪/千元基础信息salary_max40int最高月薪/千元基础信息city上海string工作城市职位要求experience3-5年string经验要求职位要求education本科string学历要求职位要求skills[Python, Django]list技能标签公司信息company_name某科技公司string公司名称公司信息company_size100-499人string公司规模定义字段表的价值在于后面处理薪资区间、做技能词频统计时能够直接在清洗阶段做结构化转换而不是在采集时匆忙处理。需要留意的一个细节是很多招聘网站的接口会把薪资写成20k-40k或面议这决定了清洗函数的逻辑至少要有三种分支区间、固定值、面议而不是简单地 split 一下。2.4 从静态页面到接口直连的演进路径对于学习项目或毕设场景最稳妥的路径是先跑通静态模板页的爬取再过渡到接口请求。因为静态页面的解析逻辑直观XPath 写错一眼就能定位而接口请求虽然返回干净但往往需要处理headers、cookies、加密参数三层问题。以某个常见的职位搜索页为例接口返回的核心结构如下{ code: 0, data: { list: [ { jobName: Python爬虫工程师, salary: 25k-40k, city: 北京, company: {name: 某数据服务公司, size: 100-499人} } ], totalCount: 2341 } }接口请求的代码比解析 HTML 更简单但有一个隐蔽的坑接口通常校验请求头里的Referer字段少了这个字段会返回 403。常用的处理手段是构造一个请求头字典把User-Agent、Referer、Accept都补齐import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/jobs?citybeijing, Accept: application/json, text/plain, */*, } resp requests.get(https://api.example.com/job/list, headersheaders, timeout10) data resp.json()这段代码里timeout10是一个很重要的参数。招聘网站的接口有时会因负载高而变慢如果没有超时设置爬虫会挂在其中某一条请求上导致整个采集流程停滞。超时的阈值根据实际网络情况调节局域网内抓取可以设到 5 秒公网建议 10 到 15 秒。resp.json()解析失败时还需要额外捕获JSONDecodeError因为部分网站在反爬时会返回一段 HTML 而不是 JSON。3. Scrapy 分布式爬虫框架下的招聘数据采集实现3.1 为什么选择 Scrapy 而不是 requests招聘数据爬虫如果只跑一次用requests加BeautifulSoup完全够用。但当需要抓取的城市多、岗位类型多、翻页页数深时单线程的requests方案在效率上会输得很惨。Scrapy 的优势在于它自带了并发调度、去重、重试、日志和中间件机制这些能力在生产环境里不是可选项而是刚需。Scrapy 的请求调度是基于异步事件循环的这意味着它可以同时维持几十个连接去请求不同页面而不需要像requests那样靠ThreadPoolExecutor手动管理线程池。另一个关键点是 Scrapy 的去重过滤器它会把已经请求过的 URL 指纹存下来避免翻页时重复采集同一份数据。相比之下requests方案里的去重逻辑需要你自己写集合或 Redis 存储。对于“招聘岗位数据爬虫”这个场景我的建议是小批量测试用requests正式采集或毕设展示直接用 Scrapy。后者在代码组织上天然分出了spiders、items、pipelines、middlewares四层结构这份架构本身就是答辩或文档报告里最好的素材。3.2 创建 Scrapy 项目和核心代码实现创建项目的命令非常简单scrapy startproject job_spider cd job_spider scrapy genspider job example.com执行完这条命令后项目的目录结构是job_spider/ ├── scrapy.cfg └── job_spider/ ├── __init__.py ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py └── spiders/ └── job.py在写爬虫代码之前需要先定义 Item 数据结构。Item 的作用相当于数据表结构每个字段都是后面 Pipeline 处理的入口import scrapy class JobItem(scrapy.Item): job_name scrapy.Field() salary_min scrapy.Field() salary_max scrapy.Field() city scrapy.Field() experience scrapy.Field() education scrapy.Field() company_name scrapy.Field() company_size scrapy.Field() skills scrapy.Field()这个 Item 定义的逻辑是为后续的数据清洗与可视化提供统一的数据格式。Field()并不做类型校验它更像一个字典的 Key但定义在这里Postgres 或 MySQL 建表时的字段顺序、类型转换就能一一对应起来少了代码层面的“字段名拼写不一致”的问题。3.3 爬虫主逻辑中的翻页、解析与回调接下来是爬虫的核心逻辑。这个文件要解决三件事第一生成翻页 URL 列表第二解析列表页拿到每个职位的详情页链接或接口数据第三把字段组装成 Item 递给 Pipeline。以接口直连的方案为例import json import scrapy from job_spider.items import JobItem class JobSpider(scrapy.Spider): name job base_url https://api.example.com/job/list?city{city}page{page} def start_requests(self): cities [北京, 上海, 广州, 深圳, 杭州] for city in cities: for page in range(1, 11): url self.base_url.format(citycity, pagepage) yield scrapy.Request(url, callbackself.parse, meta{city: city}) def parse(self, response): data json.loads(response.text) job_list data.get(data, {}).get(list, []) for job in job_list: item JobItem() item[job_name] job.get(jobName) item[city] response.meta[city] salary job.get(salary, 面议) item[salary_min], item[salary_max] self.parse_salary(salary) item[experience] job.get(experience) item[education] job.get(education) item[company_name] job.get(company, {}).get(name) item[company_size] job.get(company, {}).get(size) item[skills] job.get(skills, []) yield item def parse_salary(self, salary): if k in salary: parts salary.lower().replace(k, ).split(-) if len(parts) 2: return int(float(parts[0]) * 1000), int(float(parts[1]) * 1000) return 0, 0这段代码里有两个设计值得说明。一是start_requests里手动生成了城市与页码的笛卡尔积这在需要精确控制采集范围时非常方便不需要依赖网站的分页参数二是parse_salary把25k-40k统一转换成数值型的最低与最高月薪存入 MySQL 或 CSV 后做区间统计时直接用salary_max做中位数排序就可以。需要注意salary.replace(k, )中必须强制转小写否则K在 Python 里替换后没变化会产生非数字字符串导致int()抛异常。3.4 配置 Item Pipeline 完成数据落库Pipeline 是 Scrapy 里的数据出口。我一般会在 Pipeline 里做两层处理第一层清洗比如把空值替换为默认值、去掉首尾空格第二层持久化把 Item 写入 MySQL。下面是一个写入 MySQL 的 Pipeline 示例import pymysql class JobPipeline: def open_spider(self, spider): self.conn pymysql.connect( hostlocalhost, port3306, userroot, password123456, databasejob_db, charsetutf8mb4, ) self.cursor self.conn.cursor() def process_item(self, item, spider): sql INSERT INTO job_position (job_name, salary_min, salary_max, city, experience, education, company_name, company_size, skills) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) skills_str ,.join(item[skills]) if item[skills] else self.cursor.execute(sql, ( item[job_name], item[salary_min], item[salary_max], item[city], item[experience], item[education], item[company_name], item[company_size], skills_str, )) self.conn.commit() return item def close_spider(self, spider): self.cursor.close() self.conn.close()这段代码的逻辑要点是open_spider在爬虫启动时建立数据库连接process_item在每一条数据被抓取后执行插入close_spider在爬虫结束时清理连接。skills是列表类型MySQL 原生不支持数组所以用join方法拼成逗号分隔的字符串如果后续要做技能词频分析在查询时再GROUP_CONCAT或FIND_IN_SET还原。commit()逐条提交在数据量大时性能差可以考虑每 100 条批量提交一次用executemany处理效率更高。3.5 settings.py 里的关键参数与反爬中间件Scrapy 项目能稳定跑多久取决于settings.py里的几项参数。以下是推荐配置和解释参数名推荐值作用说明ROBOTSTXT_OBEYTrue遵循 robots 协议正式环境务必开启CONCURRENT_REQUESTS16并发请求数调大提速但增加封 IP 风险DOWNLOAD_DELAY0.5请求间隔秒数越小越快越容易被识别DEFAULT_REQUEST_HEADERS带 UA 与 Referer 的字典模拟浏览器基础行为DOWNLOADER_MIDDLEWARES配置代理中间件用于 IP 轮换应对频率限制ITEM_PIPELINES{job_spider.pipelines.JobPipeline: 300}注册数据管道数字越小优先级越高这里需要特别提醒的是ROBOTSTXT_OBEY。它是一个开关式的全局设置如果你的爬虫只采集公开的岗位列表页保持True但如果目标网站 robots 里把接口路径全部Disallow爬虫会直接拒绝所有请求。这种情况一般出现在需要登录后才能访问的招聘平台处理方式是改为False并在代码层面自己控制爬取边界而不是盲目依赖这个开关。一个常见的反爬手段是DOWNLOAD_DELAY与CONCURRENT_REQUESTS的配合。多数招聘网站的限流规则是基于单 IP 在单位时间内的请求次数比如 1 秒超过 5 次就临时封禁。这时把并发降到 8、DELAY设为 1 秒比换代理 IP 更稳定——因为代理 IP 的可用性和速度往往是更大的不确定性来源。4. 招聘数据清洗与可视化分析前的标准化处理4.1 原始数据的脏数据问题写入数据库的数据并不等于可以分析的数据。招聘岗位数据的典型脏数据集中在薪资、城市、经验、技能四类字段上。薪资可能是面议、1.5-2.5万/月、25-40万/年三种格式城市可能是北京-海淀区或北京·望京经验可能是在校/应届或经验不限。这些格式如果不处理salary_min在排序时会出现字符型与数值型混排导致统计结果完全错误。处理方案是写一个独立的清洗脚本在可视化前对数据库表执行一次原地更新而不是在每次可视化查询时重复清洗逻辑。这样做的效率最高也方便人工检查中间结果。4.2 清洗代码薪资归一化、经验字段映射与技能拆解下面的脚本演示如何把 MySQL 中的job_position表进行标准化处理import re import pymysql import pandas as pd conn pymysql.connect(hostlocalhost, userroot, password123456, databasejob_db, charsetutf8mb4) df pd.read_sql(SELECT id, salary_text, experience, skills FROM job_position, conn) def normalize_salary(text): text str(text) if 万/月 in text: nums re.findall(r[\d.], text) return [int(float(n) * 10000) for n in nums] if k in text.lower(): nums re.findall(r[\d.], text.lower().replace(k, )) return [int(float(n) * 1000) for n in nums] if 元/天 in text: nums re.findall(r[\d.], text) return [int(n) * 22 * 12 for n in nums] return [0, 0] df[salary_min] df[salary_text].apply(lambda x: normalize_salary(x)[0]) df[salary_max] df[salary_text].apply(lambda x: normalize_salary(x)[1]) exp_map { 在校/应届: 1, 1年以内: 1, 1-3年: 2, 3-5年: 3, 5-10年: 4, 经验不限: 0, } df[experience_level] df[experience].map(exp_map).fillna(0) for idx, row in df.iterrows(): conn.cursor().execute( UPDATE job_position SET salary_min%s, salary_max%s, experience_level%s WHERE id%s, (row[salary_min], row[salary_max], row[experience_level], row[id]), ) conn.commit() conn.close()这段代码的关键在于normalize_salary函数的分支判断。25-40万/年这种格式需要先除以 12 再乘 10000我在上面用万/月和k的方式匹配如果你抓取的网站包含万/年格式需要在最后加一个elif 万/年 in text分支并除以 12。re.findall负责把1.5-2.5中的两个数字都提取出来注意float转换必须放在int()之前1.5会被截断为3千而不是期望的15000这是新手最容易踩的坑。4.3 基于 DataFrame 的统计口径设计清洗完成后就需要设计分析指标。招聘数据可视化不追求复杂模型核心关注四个统计口径分析维度指标SQL / Pandas 实现方式城市分布岗位数量占比GROUP BY city ORDER BY count DESC薪资水平城市平均薪资 / 中位数GROUP BY city 后取 salary_max 均值经验要求各经验层次岗位数量映射后的experience_level分组技能需求Top20 技能词频将skills字段拆成多行再GROUP BY统计口径的差异会直接影响可视化呈现同一份数据用均值和中位数绘图结论可能完全不同。招聘薪资的分布通常是右偏的少数高薪岗位拉高均值所以我在可视化时更推荐用箱线图展示薪资分布而不是只画一个平均值的柱状图——后者会把数据失真地“平滑化”写报告时也经不起质疑。import pandas as pd df pd.read_sql(SELECT city, salary_max, experience_level FROM job_position, conn) city_salary df.groupby(city)[salary_max].agg([mean, median, count]) city_salary city_salary.sort_values(mean, ascendingFalse).head(10)这段代码直接输出各城市的岗位数量、平均薪资、中位数薪资三个指标一起对比时你才能判断“这个城市岗位多是因为大厂多还是因为中小公司投递活跃”。可视化分析的目标不是做出漂亮的图而是让看图的人能够从图里看出结论、提出追问。4.4 Excel 交叉透视城市与经验层级的矩阵统计除了库内统计招聘岗位数据还经常需要导出成交叉表。交叉表的价值在于能够一眼看出“北京的资深岗位比例是否明显高于其他城市”这是单纯条形图做不到的。import pandas as pd from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:123456localhost:3306/job_db?charsetutf8mb4) df pd.read_sql(SELECT city, experience_level, salary_max FROM job_position, engine) pivot pd.pivot_table(df, indexcity, columnsexperience_level, valuessalary_max, aggfunccount, fill_value0) pivot.to_excel(city_experience_pivot.xlsx)pivot_table的aggfunccount统计的是满足条件的记录数它会忽略NaN值并用 0 填充。交叉表导出后可以放进 PPT 文档报告里作为附页这是招聘数据可视化项目中性价比最高的输出物之一。需要注意的是create_engine里的密码如果包含特殊字符要使用urllib.parse.quote_plus编码否则连接数据库时会报Access denied。5. 招聘岗位数据的可视化分析与三维联动图表设计5.1 pyecharts 在地理分布与薪资散点图上的应用招聘数据的可视化选型上我建议直接用pyecharts而不是matplotlib。原因有三一是 pyecharts 基于 ECharts交互效果远好于静态图适合放进 PPT二是地理图表地图不需要额外处理底图数据内置的中国地图组件直接用三是代码风格接近链式调用写起来比matplotlib的面向对象 API 更顺手。安装方式pip install pyecharts pandas pymysql对于城市薪资分布散点图是最适合展示城市吸引力的图表。横轴为城市岗位数量、纵轴为平均薪资、点的大小为招聘数量这样的气泡图能够在同一张图中同时回答“哪里机会多”“哪里薪资高”“哪座城市两者兼具”三个问题。5.2 城市-薪资气泡图代码实现import pymysql import pandas as pd from pyecharts.charts import Scatter, Bar, Pie from pyecharts import options as opts conn pymysql.connect(hostlocalhost, userroot, password123456, databasejob_db, charsetutf8mb4) df pd.read_sql( SELECT city, AVG(salary_max) AS avg_salary, COUNT(*) AS job_count FROM job_position GROUP BY city ORDER BY job_count DESC LIMIT 20, conn, ) scatter ( Scatter() .add_xaxis(df[city].tolist()) .add_yaxis( 平均薪资, df[[avg_salary, job_count]].values.tolist(), symbol_sizelambda val: max(10, min(60, val[1] / 5)), ) .set_global_opts( title_optsopts.TitleOpts(title各城市招聘岗位平均薪资与数量分布), xaxis_optsopts.AxisOpts(name城市, axislabel_opts{rotate: 45}), yaxis_optsopts.AxisOpts(name平均薪资元/月), tooltip_optsopts.TooltipOpts( formatter{b}: {c[0]}元/月, {c[1]}个岗位 ), ) ) scatter.render(city_salary_bubble.html)这段代码里最值得展开说明的是symbol_size参数。pyecharts 的 Scatter 图默认symbol_size是统一大小但如果把数据以[平均薪资, 岗位数]的二元列表传入就需要通过symbol_size函数动态计算气泡大小val[1] / 5是为了缩放避免岗位数过多的城市把图撑爆。tooltip_formatter里的{c[0]}和{c[1]}分别对应传入列表中的第一个和第二个值显示效果是“上海: 28000元/月, 350个岗位”。5.3 技能关键词词频统计与柱状图展示招聘网站详情页里的技能标签通常是逗号分隔的字符串需要先拆散再统计。下面是计算 Top20 技能词频并输出横向柱状图的代码from collections import Counter import pymysql import pandas as pd from pyecharts.charts import Bar conn pymysql.connect(hostlocalhost, userroot, password123456, databasejob_db, charsetutf8mb4) df pd.read_sql(SELECT skills FROM job_position WHERE skills ! , conn) skill_counter Counter() for row in df[skills].str.split(,): for skill in row: skill skill.strip() if skill: skill_counter[skill] 1 top_skills skill_counter.most_common(20) skills, counts zip(*top_skills) bar ( Bar() .add_xaxis(list(skills)) .add_yaxis(岗位需求数, list(counts)) .reversal_axis() .set_series_opts(label_optsopts.LabelOpts(positionright)) .set_global_opts( title_optsopts.TitleOpts(title招聘岗位 Top20 技能需求排名), yaxis_optsopts.AxisOpts(name技能名称), xaxis_optsopts.AxisOpts(name出现次数), ) ) bar.render(top_skills.html)reversal_axis()是 pyecharts 里非常实用的方法作用是把坐标系翻转横向条形图在技能名称过长时视觉上更舒适。str.split(,)会把Python, Django, Flask按逗号拆成三元列表再逐个放入 Counter 对象进行计数。注意抓到的 skills 字段如果是以中文逗号“”分隔这里还需要先做一次replace(, ,)的预处理这是招聘网站数据里最容易出现的编码差异问题。5.4 多图串联成可交互报告页面的实践单一图表的信息量有限招聘数据分析通常会将地图、柱状图、饼图、表格拼装成一个 HTML Dashboard。pyecharts 提供了Tab或Page组件来组合多张图表。我建议用Page(layoutPage.SimplePageLayout)纵向排列这样在 PPT 文档报告里可以直接嵌入整张长截图。from pyecharts.charts import Page, Pie pie ( Pie() .add(, [(经验不限, 120), (1-3年, 340), (3-5年, 260), (5-10年, 80)]) .set_global_opts(title_optsopts.TitleOpts(title经验要求分布)) ) page Page(layoutPage.SimplePageLayout) page.add(scatter, bar, pie) page.render(job_analysis_report.html)Page组件的add方法支持多图共存渲染后是一份可以上下滚动的单文件报告。整份报告包含城市气泡图、技能排名柱状图、经验要求饼图三个核心视角。这份 HTML 文件后续可以直接套进 PPT 的 Web 浏览器插件里演示不需要额外启动本地服务。如果图表数据量较大首次加载会有几百毫秒的白屏需要控制在图表数量不超过 5 张为宜。6. 从可视化页面反推爬虫设计结构改进可视化分析做完后接下来回到源头检查一遍爬虫的工程化程度。很多人做完图表就收工了但招聘岗位爬虫与可视化系统真正值钱的地方在于能否增量更新、能否自动重试、能否应对反爬。这一章节的核心技巧是从图表中反馈的异常反推爬虫设计——比如若非北京岗位数据明显偏少说明抓取请求被目标接口的部分参数限制。这些判断标准如下可视化发现可能原因爬虫改进方向城市字段出现大量NaN列表页的城市字段只在详情页返回增加一次详情页请求或从 URL 参数中提取城市薪资集中在 0-5k 区间salary_text清洗不完整扩展正则匹配万/年、元/天格式技能标签几乎没有重复skills字段在接口中未拼接完整检查接口返回的skillName是否嵌套在子对象某一页的数据全部缺失翻页地址使用了pageIndex但实际参数名为pageNo爬虫翻页参数命名与实际请求参数不一致一个实用的改进手段是给爬虫增加一个增量爬取逻辑。招聘网站每天都有新岗位发布全量爬取既耗时又会触发反爬。常见的做法是记录每条数据的job_id或job_url指纹在process_item中查重只有新记录才插入数据库def process_item(self, item, spider): sql_check SELECT id FROM job_position WHERE job_url %s LIMIT 1 sql_insert INSERT INTO job_position (job_name, salary_min, salary_max, city, job_url) VALUES (%s, %s, %s, %s, %s) self.cursor.execute(sql_check, (item[job_url],)) exists self.cursor.fetchone() if not exists: self.cursor.execute(sql_insert, ( item[job_name], item[salary_min], item[salary_max], item[city], item[job_url], )) self.conn.commit() return item这段代码的思路是先按job_url查一次表如果已经存在则跳过插入。job_url是爬虫抓取时从详情页链接中解析到的唯一标识它比岗位名称更可靠——因为同一家公司的同一个职位名称可能在多个渠道发布多条。增量逻辑避免了反复抓取同一份数据导致的图表统计虚高让图表中的岗位数更接近真实需求。最后还要提到一个容易被忽略的点可视化页面本身也可以作为爬虫代码的回归测试工具。你只需隔一段时间重新抓取少量数据并刷新图表如果城市分布、技能 Top 榜出现明显异常波动大概率不是市场行情发生了变化而是页面结构或接口字段被改动过。这个“以图测爬”的思路比每次人工看爬虫日志更直观也更适合放进文档报告作为运维实践的一部分。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表