js获取后端数据(用Next.js爬取6万+招聘信息,搭建可视化仪表盘!技术党必看)

js获取后端数据(用Next.js爬取6万+招聘信息,搭建可视化仪表盘!技术党必看)
用Next.js爬取6万+招聘信息,搭建可视化仪表盘!技术党必看



一、找不到靠谱招聘数据?他花30小时爬取6万+信息,亲手造答案

做数据分析师、职场研究者,最头疼的就是找不到干净、全面的行业招聘数据——要么付费才能看,要么信息过时模糊,要么只给表面结论,没有底层细节。肯尼亚程序员John Chiwai就遇到了这样的难题,他想搞清楚肯尼亚就业市场的真实模样:哪些领域招聘最火爆?企业真正需要什么经验水平的人才?岗位真的全集中在首都内罗毕吗?

全网翻遍,没有一份能满足需求的数据集,大多数报告要么锁在付费墙后,要么模糊到毫无参考价值。换做普通人,可能早就放弃,但John偏不——他决定自己动手,从爬取数据到搭建仪表盘,全程从零实现,耗时30小时,最终搞定68648条招聘信息,做出了一个能直观呈现肯尼亚就业市场的可视化工具。

这看似是一次简单的技术实践,却解决了无数研究者、求职者的痛点,但背后藏着太多不为人知的坑:爬取时如何避免被网站封禁?结构化数据怎么提取才高效?Next.js搭建仪表盘比Python更有优势?这些问题,不仅是John要解决的,更是每一个想做数据可视化的技术党都会遇到的坎,看完这篇,你既能学到实操方法,也能避开90%的弯路。

二、核心拆解:从爬取数据到部署上线,完整实操步骤全公开

John的整个项目,核心分为四大环节:选择合适的招聘网站、搭建爬虫获取数据、用SQLite存储数据、用Next.js搭建仪表盘并部署,每一步都有明确的操作逻辑,甚至包含具体代码,普通人跟着做也能复刻。

1. 选对招聘网站:MyJobMag为何成为最优解?

John对比了肯尼亚多个主流招聘网站,最终选择了MyJobMag,不是随机选择,而是经过精准筛选,核心原因有4点,这也是我们爬取数据时选对数据源的关键:

一是HTML结构统一可预测,很多招聘网站的列表页和详情页布局不一致,给爬取带来极大麻烦,而MyJobMag的页面结构规整,无需反复调整爬取逻辑;二是招聘信息包含结构化元数据,字段、地点、学历、经验、岗位类型、行业等信息清晰,不是杂乱的纯文本,提取效率极高;三是数据量充足,不仅有当前活跃岗位,还有历史归档页面,能获取更全面的市场快照,而非单一时间点的信息;四是详情页包含application/ld+json结构化数据块,可直接提取关键字段,无需复杂的HTML解析,大幅提升数据质量。

2. 爬虫搭建:TypeScript+Cheerio,30小时爬取6万+数据

John的爬虫基于TypeScript开发,搭配tsx运行(无需编译步骤),用Cheerio做HTML解析(相当于Node.js版jQuery),还借助Cursor辅助搭建,整体流程清晰,还加入了防封禁、去重机制,具体操作如下:

爬虫核心流程

1. 从MyJobMag首页开始,通过爬取网站导航栏,发现所有类别、领域、地点、行业的招聘列表页URL;

2. 针对每个发现的列表页,分页爬取所有内容,直到没有更多页面;

3. 对每个岗位,提取唯一标识slug(URL路径片段,如senior-software-engineer-at-safaricom-123),检查是否已存在于数据库中;

4. 若为新岗位,获取岗位详情页,提取结构化元数据;

5. 将数据插入SQLite数据库。

核心脚本与代码

项目包含两个爬虫脚本,适配不同需求:

scrape.ts:简单的分页爬虫,适合快速运行,获取少量数据用于测试;

scrape-deep.ts:完整的爬虫,可自动发现所有列表页,遍历所有分页,获取全部数据。

关键优化点(避坑重点):

- 防封禁:每两次请求之间添加1秒延迟,避免高频请求被网站拦截,整个爬取过程未出现限流问题;

- JSON-LD提取技巧:优先解析详情页的[xss_clean]块,若@type为JobPosting,直接读取datePosted、employmentType等关键字段,只有当数据缺失时,才 fallback到HTML解析,大幅提升数据质量;

- 去重机制:通过slug字段判断岗位是否已存在,可多次运行爬虫,中断后可恢复,不会产生重复数据。

爬虫核心代码(简化版,可直接复用):

// 引入依赖import cheerio from 'cheerio';import axios from 'axios';import Database from 'better-sqlite3';// 初始化数据库const db = new Database('./data/jobs.db');db.prepare(`  CREATE TABLE IF NOT EXISTS jobs (    id INTEGER PRIMARY KEY AUTOINCREMENT,    slug TEXT UNIQUE,    url TEXT,    title TEXT,    company TEXT,    field TEXT,    location TEXT,    experience TEXT,    qualification TEXT,    job_type TEXT,    industry TEXT,    description TEXT,    posted_date TEXT,    deadline TEXT,    scraped_at DATETIME DEFAULT CURRENT_TIMESTAMP  )`).run();// 延迟函数,防封禁const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));// 爬取列表页async function scrapeListingPage(url: string) {  try {    await delay(1000); // 1秒延迟    const response = await axios.get(url);    const $ = cheerio.load(response.data);        // 提取所有岗位slug    const jobSlugs = $('a.job-title-link').map((_, el) => {      const href = $(el).attr('href');      return href ? href.split('/').pop() : null;    }).get().filter(Boolean) as string[];        // 处理每个岗位    for (const slug of jobSlugs) {      const jobExists = db.prepare('SELECT 1 FROM jobs WHERE slug = ?').get(slug);      if (!jobExists) {        await scrapeJobDetail(slug);      }    }        // 分页处理    const nextPage = $('a.next-page').attr('href');    if (nextPage) {      await scrapeListingPage(`https://www.myjobmag.co.ke${nextPage}`);    }  } catch (error) {    console.error('爬取列表页失败:', error);  }}// 爬取岗位详情页async function scrapeJobDetail(slug: string) {  try {    await delay(1000);    const url = `https://www.myjobmag.co.ke/job/${slug}`;    const response = await axios.get(url);    const $ = cheerio.load(response.data);        // 优先解析JSON-LD    let jobData = {};    const script = $('script[type="application/ld+json"]').text();    if (script) {      const json = JSON.parse(script);      if (json['@type'] === 'JobPosting') {        jobData = {          slug,          url,          title: json.title || '',          company: json.hiringOrganization?.name || '',          location: json.jobLocation?.address?.addressLocality || '',          experience: json.experienceRequirements?.description || '',          job_type: json.employmentType || '',          posted_date: json.datePosted || '',          deadline: json.validThrough || '',          description: json.description || ''        };      }    }        // 若JSON-LD缺失, fallback到HTML解析    if (!Object.keys(jobData).length) {      jobData = {        slug,        url,        title: $('h1.job-title').text().trim() || '',        company: $('div.company-name').text().trim() || '',        location: $('div.job-location').text().trim() || '',        experience: $('div.job-experience').text().trim() || '',        job_type: $('div.job-type').text().trim() || '',        description: $('div.job-description').text().trim() || ''      };    }        // 插入数据库    db.prepare(`      INSERT INTO jobs (slug, url, title, company, location, experience, job_type, description, posted_date, deadline)      VALUES (@slug, @url, @title, @company, @location, @experience, @job_type, @description, @posted_date, @deadline)    `).run(jobData);        console.log(`成功插入岗位:${jobData.title}`);  } catch (error) {    console.error(`爬取岗位${slug}失败:`, error);  }}// 启动爬虫scrapeListingPage('https://www.myjobmag.co.ke/');

3. 数据库选择:SQLite,轻量高效的最优选择

John选择SQLite存储所有爬取数据,而非MySQL、PostgreSQL等数据库,核心原因是适配项目需求:整个项目的数据量为68648条,单表结构(约10列),SQLite作为单文件数据库,无需配置,零依赖,且足够支撑这类读密集型的分析查询,操作简单高效。

开发过程中,John使用better-sqlite3(Node.js同步SQLite驱动),同步API简洁,无需处理async/await的复杂逻辑,大幅提升开发效率。数据库表结构包含15个字段:id、slug、url、title、company、field、location、experience、qualification、job_type、industry、description、posted_date、deadline、scraped_at,覆盖岗位所有关键信息。

值得注意的是,部分历史归档岗位没有posted_date字段(JSON-LD中缺失),John没有删除这些数据——它们依然能用于领域、地点、公司的分析,只是在趋势图表中排除,既保证了数据完整性,又避免了误导性。

4. 仪表盘搭建:放弃Python,选择Next.js的核心原因

提到数据可视化仪表盘,大多数人第一反应是用Python(pandas、matplotlib、Streamlit),但John却反常地选择了Next.js,这不是盲目跟风,而是经过深思熟虑的选择,核心优势有3点:

一是样式灵活性高:John使用Tailwind CSS,能完全自定义仪表盘的UI,从卡片布局、颜色搭配,到响应式侧边栏、排版,都能按照自己的想法实现,而Streamlit的UI受框架限制,难以做出个性化设计;

二是部署简单:Next.js可直接部署到Vercel,只需将代码推送到GitHub,关联仓库即可完成部署,无需配置容器、服务器,也不用操心Python版本兼容问题,省去了大量部署成本;

三是App Router优势:Next.js 15的App Router模式,让每个页面都能作为服务器组件,直接运行SQL查询,无需单独搭建API层,数据获取和渲染在同一位置,简化了开发流程。

仪表盘核心结构与实现

仪表盘共8个页面:仪表盘首页、领域分析、公司分析、地点分析、行业分析、洞察、趋势、所有岗位,每个页面的实现逻辑一致:

1. 服务器组件通过Promise.all并行运行SQL查询,获取数据;

2. 将查询结果作为props传递给客户端组件;

3. 客户端组件处理交互、图表渲染、搜索和分页功能;

4. 图表使用Recharts实现,包含柱状图、饼图、折线图,用ResponsiveContainer保证响应式适配,同时用ChartErrorBoundary包裹图表组件,避免单个图表失败导致整个页面崩溃。

开发中的关键bug与解决:

仪表盘的“热门经验等级”KPI,初期始终显示“0–1年”,与实际数据不符。排查后发现,问题出在逻辑错误——将experienceBuckets数组按经验范围排序(0–1年、2–3年……),却直接取第一个元素作为热门等级,而非按数量排序。最终用reduce方法修复,找到数量最多的经验等级:

// 修复前(错误)const topExperience = experienceBuckets[0];// 修复后(正确)const topExperience = experienceBuckets.reduce((max, b) => b.count > max.count ? b : max, experienceBuckets[0]);

5. 部署踩坑:从SQLite到Turso,解决服务器兼容问题

John最初计划将SQLite文件与Next.js应用一起部署到Vercel,但很快遇到两个致命问题:一是better-sqlite3是C++原生插件,会根据本地Node.js环境编译,而Vercel的服务器less函数运行在Linux容器中,架构不同,导致构建失败;二是Vercel的serverless函数没有可写磁盘权限,文件系统是临时的,无法稳定读取.db文件。

最终,John选择Turso(托管SQLite平台)解决问题,Turso基于libSQL(SQLite分支),通过HTTP暴露服务,提供Node.js客户端,完全适配serverless环境,且SQL语法与SQLite一致,迁移成本极低。

数据库迁移核心代码变更:

旧版本(better-sqlite3):

import Database from "better-sqlite3";const db = new Database("./data/jobs.db");const result = db.prepare("SELECT …").all({ param: value });

新版本(Turso @libsql/client):

import { createClient } from "@libsql/client";const client = createClient({   url: process.env.TURSO_DATABASE_URL!,   authToken: process.env.TURSO_AUTH_TOKEN });const result = await client.execute({   sql: "SELECT …",   args: { param: value } });

迁移注意事项:一是参数语法变更,better-sqlite3用@param,Turso用:param,所有参数化查询需批量修改;二是所有数据获取函数需改为async/await,并行查询用Promise.all包裹;三是调整项目结构,将爬虫脚本移至scripts目录,清理next.config.ts中与better-sqlite3相关的配置。

三、辩证分析:Next.js vs Python,哪种方案更适合你?

John放弃Python、选择Next.js搭建仪表盘,确实解决了他的核心需求——个性化UI和简单部署,但这并不意味着Next.js是万能的,两种方案各有优劣,盲目跟风只会踩坑,我们从3个维度辩证分析,帮你找到适合自己的选择。

首先,从开发效率来看,Python更占优势。对于纯数据分析师,Python的pandas、Streamlit生态成熟,几行代码就能实现数据读取、分析和可视化,无需关注UI细节,适合快速出成果;而Next.js需要前端+后端知识,既要写SQL查询,也要做UI开发,开发周期更长,适合有前端基础、追求UI个性化的开发者。

其次,从部署成本来看,Next.js更简单。Streamlit部署需要配置服务器、处理Python环境依赖,甚至需要容器化,对非开发人员不够友好;而Next.js+Vercel的组合,几乎是“一键部署”,无需关注底层环境,适合小白或想快速上线的项目。

最后,从功能适配来看,两者各有侧重。Python适合复杂的数据建模、统计分析,能轻松处理百万级以上数据的聚合计算,适合做深度数据分析;Next.js适合需要个性化UI、多页面交互、简单数据展示的场景,比如John的招聘仪表盘,核心是“展示”而非“深度分析”,Next.js的优势更明显。

这里的关键的不是“哪个更好”,而是“哪个更适配你的需求”:如果你是数据分析师,只想快速实现数据可视化,Python是首选;如果你有前端基础,追求UI质感,需要简单部署,Next.js更适合你。John的选择,是基于自身需求的最优解,而非绝对的“标准答案”。

四、现实意义:这个项目,到底能帮到谁?

John的项目看似是一次个人技术实践,但背后的价值远超“个人练习”,它精准解决了三类人群的核心痛点,同时为技术爱好者提供了可复刻的实践案例,这也是这个项目最有意义的地方。

对于求职者而言,这个仪表盘能直观呈现肯尼亚就业市场的真实需求——哪些领域岗位最多、企业需要什么经验、哪些地区有更多机会,能帮求职者精准定位职业方向,避免盲目投递;对于企业HR而言,能了解行业招聘趋势,优化招聘策略,比如根据经验需求分布,调整岗位任职要求;对于研究者而言,提供了一份免费、全面的招聘数据集,无需再为付费报告或模糊数据发愁,可用于深入分析就业市场规律。

对于技术爱好者而言,这个项目是一个完整的全栈实践案例:从爬虫开发(TypeScript+Cheerio)、数据库操作(SQLite+Turso),到前端搭建(Next.js+Tailwind CSS)、部署上线(Vercel),覆盖了全栈开发的核心环节,且包含大量避坑技巧,比单纯的教程更有参考价值。

更重要的是,John的项目证明了“需求驱动开发”的价值——遇到问题,不依赖现有资源,而是通过技术手段自己解决,这种思路,比任何技术技巧都更值得学习。无论是职场人还是技术学习者,这种“主动解决问题”的能力,都是核心竞争力。

js获取后端数据(用Next.js爬取6万+招聘信息,搭建可视化仪表盘!技术党必看)

五、互动话题:你会怎么选?评论区聊聊你的看法

看完John的项目,相信很多技术党都有自己的思考——有人会认同他选择Next.js的思路,有人会觉得用Python更高效,也有人会吐槽“爬取数据耗时30小时,不如找现成资源”。

不妨在评论区聊聊你的看法:如果是你,要做一个招聘数据仪表盘,会选择Python还是Next.js?为什么?你觉得John的项目还有哪些可以优化的地方?比如数据更新、功能补充、性能优化等。

另外,如果你也做过类似的数据爬取或可视化项目,欢迎分享你的踩坑经历,帮更多技术小白避坑;如果没有相关经验,也可以留言说说你最想学习这个项目中的哪个环节,我们一起交流探讨。

项目核心技术栈汇总

框架:Next.js 15(App Router)

语言:TypeScript

样式:Tailwind CSS v4

图表:Recharts

数据库:Turso(libSQL / SQLite)

爬取:Node.js + Cheerio

部署:Vercel

代码辅助:Cursor + Claude Code

[xss_clean]

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

相关阅读