技术探索与实践:一个全职妈妈程序员的前端爬虫历险记

写码的阿川
2025-12-18 14:43
阅读 1517

凌晨两点,小宝终于睡了。我轻手轻脚地合上婴儿监控器,打开 MacBook,咖啡已经凉了第三杯——这大概就是当代“代码人生”的真实写照吧。

我是那种边带娃边写代码的全职妈妈,白天在公司当“人形需求翻译机”,晚上回家当“人类幼崽饲养员”。在这家公司干了三年多,从 Vue2 一路卷到 Vue3,从手动部署到 CI/CD 流水线都快背下来了。团队氛围其实不错,但最近总觉得有点倦怠——可能是因为每次技术分享会我都只能在线听(毕竟要哄睡),也可能是因为上周产品经理又提了个“就改一行代码”的需求,结果改出三个线上 Bug。

说到底,我想换个环境了。于是开始偷偷研究些新东西,尤其是那些能让我简历看起来“哇塞”的技能点。正好上个月,公司有个内部数据聚合项目,需要从几个公开网站抓点结构化数据做分析。后端同事甩锅说“这不归我们管”,运维一脸冷漠:“别搞太多请求,不然 IP 封了别找我。”最后锅落到了我这个前端头上——行吧,谁让我是那个“什么都会一点”的人呢?

爬虫?前端也能干!

说实话,听到“爬虫”两个字,我第一反应是:这不是 Python 的地盘吗?Requests + BeautifulSoup + Scrapy,三件套走天下。但转念一想,既然我是前端,为啥不用 JavaScript 搞?一来熟悉语法,二来如果能用浏览器自动化,还能绕过一些反爬机制(比如动态渲染的内容),三来……万一跳槽面试官问“你会不会爬虫”,我就能理直气壮地说:“当然,我用 Puppeteer 写过!”

于是,我的技术选型之旅开始了。目标很明确:用 JS 实现一个稳定、可维护、不被封 IP 的前端爬虫方案

方案一:纯 Node.js + Axios + Cheerio(“老派静态派”)

最朴素的想法:直接用 Node.js 发 HTTP 请求,拿到 HTML 后用 Cheerio(类 jQuery 的服务端 DOM 解析库)提取数据。代码简单到令人发指:

const axios = require('axios');
const cheerio = require('cheerio');

async function scrape(url) {
  const { data } = await axios.get(url);
  const $ = cheerio.load(data);
  return $('.product-title').text();
}

优点?快、轻量、资源消耗低。适合抓取静态页面,比如新闻列表、商品目录这种 SSR 渲染的网站。

但问题也来了。去年双11期间,我们试过用这种方式抓某电商的促销页,结果返回的全是 <div id="app"></div> —— 因为人家早用上了 SPA(单页应用),数据都是通过 XHR 动态加载的。这时候 Cheerio 就傻眼了:DOM 树里根本没内容啊!

而且,很多网站现在都有基础反爬策略:User-Agent 检测、请求频率限制、甚至要求 Cookie 或 Token。你要是不做点伪装,分分钟被 403。

方案二:Puppeteer(“重型武器”)

既然静态不行,那就上浏览器自动化!Puppeteer 是 Google 官方的 Node 库,可以控制无头 Chrome(Headless Chrome),完全模拟真实用户行为。

const puppeteer = require('puppeteer');

async function scrapeWithPuppeteer(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle2' });
  const title = await page.$eval('.product-title', el => el.textContent);
  await browser.close();
  return title;
}

这套方案牛在哪?它能执行 JS、等待异步数据加载、处理 Cookie、甚至自动登录(虽然我不推荐这么干)。更重要的是,它发出的请求和真实用户几乎没区别,反爬系统很难识别。

但代价也很明显:慢、吃内存、难部署

上周五晚上,我兴致勃勃把 Puppeteer 脚本跑起来,结果本地还好,一上测试服务器,运维立马冲过来:“你这玩意儿占了 800MB 内存!还启动了 Chromium 进程?!” 更惨的是,某次抓取过程中页面跳转超时,浏览器实例没关,直接导致服务器 OOM(Out of Memory)——第二天晨会,我被点名“差点让监控告警炸了”。

方案三:Playwright(“Puppeteer 的叛逆弟弟”)

就在我不知所措时,隔壁组一个刚入职的小哥安利了 Playwright。他说:“Puppeteer 只支持 Chromium,但 Playwright 能同时跑 Chromium、Firefox 和 WebKit,API 更现代,还自带自动等待机制。”

我半信半疑试了下:

const { chromium } = require('playwright');

async function scrapeWithPlaywright(url) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(url);
  const title = await page.textContent('.product-title');
  await browser.close();
  return title;
}

确实香!Playwright 的 page.textContent() 不需要像 Puppeteer 那样写 $eval,语义更清晰。而且它的“自动等待”真的省心——比如你点击一个按钮后要等弹窗出现,它会智能判断元素是否 ready,不用手动加 setTimeoutwaitForSelector

性能上,两者差不多,但 Playwright 的跨浏览器支持对我这种想“一次编写,多处验证”的人太友好了。不过,内存占用问题依然存在,只是换了个壳子。


选型对比:不是越新越好,而是看场景

折腾了一周,我做了个简单对比表,贴出来给大家参考(也方便我自己以后翻):

维度 Axios + Cheerio Puppeteer Playwright
适用场景 静态 HTML 页面 动态渲染 SPA / 需要 JS 执行 同左,且需跨浏览器测试
速度 ⚡️ 极快(毫秒级) 🐢 慢(秒级) 🐢 慢(秒级)
内存占用 💧 极低(<50MB) 🔥 高(500MB+) 🔥 高(500MB+)
反爬绕过能力 ❌ 弱(需手动伪造 UA/Cookie) ✅ 强(真实浏览器行为) ✅ 强
维护成本 💰 低 💸 高(需处理浏览器生命周期) 💸 中高
部署难度 🟢 简单(纯 Node) 🔴 复杂(需安装 Chromium) 🔴 复杂(需安装多浏览器)
调试体验 🤖 需打印 HTML 分析 👁️ 可截图、录屏、DevTools 👁️ 同左,且支持 Trace 查看

结论很清晰:如果是抓取新闻、博客、政府公开数据这类静态内容,Cheerio 足够;如果是抓电商、社交平台、需要登录或 JS 渲染的页面,只能上 Puppeteer 或 Playwright

最终,我选择了 Playwright——不是因为它最新,而是因为我们的目标网站用了 React + 动态懒加载,而且偶尔会有验证码(虽然我们尽量避开触发点)。另外,Playwright 的 TypeScript 支持更好,类型提示让我这个经常半夜写代码、眼神模糊的老母亲少犯低级错误。


实战踩坑:那些让我想砸电脑的瞬间

光有选型不够,实战才是魔鬼。以下是我在项目中踩过的几个大坑,血泪经验,务必收藏:

坑 1:IP 被封,请求全挂

第一次批量跑脚本,100 个 URL,跑了 20 个就全 403。查日志发现是同一个出口 IP 请求太频繁。

解决方案:加代理池 + 随机延迟。

const proxies = ['http://proxy1:8080', 'http://proxy2:8080'];
const randomProxy = proxies[Math.floor(Math.random() * proxies.length)];

const browser = await chromium.launch({
  proxy: { server: randomProxy }
});

// 再加上随机延迟
await page.waitForTimeout(1000 + Math.random() * 2000);

注:公司内网不能随便用外部代理,所以最后我们申请了专用爬虫 IP,并和运维约定了 QPS 上限。

坑 2:页面加载不全,数据为空

有些页面依赖多个 XHR 接口,网络稍慢就拿不到完整数据。一开始用 waitUntil: 'load',结果经常空。

解决方案:用 networkidle2(等待 2 秒内无网络请求)或监听特定事件。

await page.goto(url, { waitUntil: 'networkidle2' });

// 或者更精准地等某个 API 返回
await page.waitForResponse(response =>
  response.url().includes('/api/products') && response.status() === 200
);

坑 3:Headless 模式被识别

某些网站会检测 navigator.webdriver 属性,一旦发现是自动化工具,直接拒绝服务。

解决方案:启动时注入脚本,删除特征字段。

await page.addInitScript(() => {
  Object.defineProperty(navigator, 'webdriver', {
    get: () => undefined,
  });
});

还可以伪造 User-Agent、屏幕分辨率等:

await page.setViewportSize({ width: 1920, height: 1080 });
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...');

坑 4:资源泄露,服务器爆炸

忘了 browser.close(),导致 Chromium 进程堆积。本地没事,上服务器直接崩。

解决方案:用 try-finally 确保释放,或者封装成上下文管理器。

let browser;
try {
  browser = await chromium.launch();
  // ... do work
} finally {
  if (browser) await browser.close();
}

效果与反思:值不值得折腾?

最终,这个爬虫每天凌晨 3 点跑一次,抓取 200+ 商品信息,准确率 98%,失败的任务自动重试两次。数据存进数据库后,BI 团队做分析,老板在周会上夸“前端这次立大功了”——虽然我知道他根本分不清前端后端 😅。

但对我个人而言,更大的收获是:技术探索不是为了炫技,而是解决问题的同时,给自己留条后路

我现在简历上可以写:“熟练使用 Playwright 实现复杂动态页面数据采集,具备反反爬实践经验”。跳槽面试时,这绝对比“精通 Vue 组件封装”更有记忆点。

当然,我也清醒得很:工作中还是得用稳定的技术栈。没人会因为你的爬虫用了最新框架就给你加工资,但如果你能用它解决业务痛点,那价值就出来了。


写在最后:代码人生,不止于代码

带娃之后,我越来越觉得,写代码和养孩子其实很像——都要耐心、都要容错、都要在混乱中找规律。有时候 Bug 修到崩溃,回头一看,小宝正冲我笑,突然就觉得:算了,重启一下,明天再战。

技术探索的意义,或许不在于掌握多少工具,而在于保持对世界的好奇心。哪怕每天只有凌晨两小时,哪怕被产品经理折磨得想转行卖煎饼,只要还能写出一段让自己 proud 的代码,这“代码人生”就没白过。

对了,下周我要去参加一个线下技术沙龙,主题是“前端工程化新趋势”。虽然得请婆婆帮忙带娃,但我想去看看——毕竟,世界很大,代码很酷,而我,不想只被困在尿布和需求文档之间。

共勉,各位打工人 & 育儿战士。

评论 0

最热最新
暂无评论
写码的阿川Lv.1
0
影响力
0
文章
0
粉丝