基于Playwright TypeScript/JavaScript的API调用爬虫成熟方案

本文详细介绍了如何使用 Playwright 和 TypeScript/JavaScript 构建成熟的 API 调用爬虫服务,涵盖基础架构、高级功能、生产级架构设计、性能优化、安全考虑、部署监控等完整解决方案,并提供了可直接使用的代码示例。

1. 基础API爬虫服务架构

1.1 Express + Playwright方案

这是一个基于Node.js和Express的轻量级API爬虫服务实现:

typescript 复制代码
import express from 'express';
import { chromium, Browser, Page } from 'playwright';

const app = express();
app.use(express.json());

let browser: Browser;

// 初始化浏览器实例
async function initBrowser() {
    browser = await chromium.launch({
        headless: true,
        args: ['--no-sandbox']
    });
}

// 爬虫服务核心逻辑
async function scrapePage(url: string, options = {}) {
    const context = await browser.newContext();
    const page = await context.newPage();
    
    try {
        await page.goto(url, { waitUntil: 'networkidle' });
        
        // 可根据需求定制数据提取逻辑
        const data = await page.evaluate(() => {
            return {
                title: document.title,
                content: document.body.innerText,
                links: [...document.querySelectorAll('a')].map(a => a.href)
            };
        });
        
        return { success: true, data };
    } catch (error) {
        return { success: false, error: error.message };
    } finally {
        await page.close();
        await context.close();
    }
}

// API端点
app.post('/api/scrape', async (req, res) => {
    const { url } = req.body;
    if (!url) {
        return res.status(400).json({ error: 'URL is required' });
    }
    
    const result = await scrapePage(url);
    res.json(result);
});

// 启动服务
initBrowser().then(() => {
    app.listen(3000, () => {
        console.log('Scraper API running on http://localhost:3000');
    });
});

// 优雅关闭
process.on('SIGTERM', async () => {
    await browser.close();
    process.exit(0);
});

这个方案提供了以下特性:

  • 基于Express的RESTful API接口
  • Playwright的无头浏览器实例管理
  • 基本错误处理和资源清理
  • 优雅的启动和关闭流程

2. 高级功能实现

2.1 支持动态参数和配置

typescript 复制代码
interface ScrapeOptions {
    waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
    timeout?: number;
    headers?: Record<string, string>;
    screenshot?: boolean;
    pdf?: boolean;
    userAgent?: string;
}

async function scrapeWithOptions(url: string, options: ScrapeOptions = {}) {
    const context = await browser.newContext({
        userAgent: options.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    });
    
    const page = await context.newPage();
    if (options.headers) {
        await page.setExtraHTTPHeaders(options.headers);
    }
    
    try {
        await page.goto(url, {
            waitUntil: options.waitUntil || 'networkidle',
            timeout: options.timeout || 30000
        });
        
        const result: any = {
            title: await page.title(),
            url: page.url()
        };
        
        if (options.screenshot) {
            result.screenshot = await page.screenshot({ fullPage: true });
        }
        
        if (options.pdf) {
            result.pdf = await page.pdf();
        }
        
        return result;
    } finally {
        await page.close();
        await context.close();
    }
}

2.2 拦截网络请求优化性能

typescript 复制代码
async function scrapeWithInterception(url: string) {
    const context = await browser.newContext();
    const page = await context.newPage();
    
    // 拦截不必要的资源请求
    await context.route('**/*.{png,jpg,jpeg,svg,gif,woff,woff2}', route => route.abort());
    
    // 监听API请求
    const apiResponses = [];
    page.on('response', async response => {
        if (response.url().includes('/api/')) {
            apiResponses.push({
                url: response.url(),
                status: response.status(),
                body: await response.json().catch(() => null)
            });
        }
    });
    
    await page.goto(url);
    
    return {
        pageContent: await page.content(),
        apiResponses
    };
}

3. 生产级架构方案

3.1 完整的爬虫服务架构

复制代码
playwright-api/
├── src/
│   ├── config/               # 配置管理
│   │   └── browser.ts        # 浏览器配置
│   ├── controllers/          # API控制器
│   │   └── scrape.controller.ts
│   ├── services/             # 业务逻辑
│   │   ├── browser.service.ts # 浏览器管理
│   │   └── scrape.service.ts  # 爬虫逻辑
│   ├── routes/               # API路由
│   │   └── scrape.route.ts
│   ├── middlewares/          # 中间件
│   │   └── error.middleware.ts
│   └── index.ts              # 应用入口
├── test/                     # 测试
├── package.json
├── tsconfig.json
└── .env                      # 环境变量

3.2 浏览器服务管理

typescript 复制代码
// src/services/browser.service.ts
import { chromium, Browser, BrowserContext } from 'playwright';

class BrowserService {
    private browser: Browser | null = null;
    private contexts: BrowserContext[] = [];
    
    async launch() {
        if (this.browser) return;
        
        this.browser = await chromium.launch({
            headless: true,
            args: ['--no-sandbox']
        });
    }
    
    async newContext() {
        if (!this.browser) await this.launch();
        
        const context = await this.browser!.newContext();
        this.contexts.push(context);
        return context;
    }
    
    async close() {
        for (const context of this.contexts) {
            await context.close();
        }
        this.contexts = [];
        
        if (this.browser) {
            await this.browser.close();
            this.browser = null;
        }
    }
}

export const browserService = new BrowserService();

3.3 爬虫服务实现

typescript 复制代码
// src/services/scrape.service.ts
import { browserService } from './browser.service';
import { Page } from 'playwright';

export class ScrapeService {
    async scrape(url: string, options: any = {}) {
        const context = await browserService.newContext();
        const page = await context.newPage();
        
        try {
            await page.goto(url, {
                waitUntil: options.waitUntil || 'networkidle',
                timeout: options.timeout || 30000
            });
            
            // 自定义数据提取逻辑
            const data = await this.extractData(page, options);
            return { success: true, data };
        } catch (error) {
            return { success: false, error: error.message };
        } finally {
            await page.close();
        }
    }
    
    private async extractData(page: Page, options: any) {
        // 实现具体的数据提取逻辑
        return {
            title: await page.title(),
            content: await page.content(),
            // 其他自定义数据
        };
    }
}

4. 性能优化与扩展

4.1 使用集群提高并发能力

typescript 复制代码
import { Cluster } from 'playwright-cluster';

async function runCluster() {
    const cluster = await Cluster.launch({
        concurrency: Cluster.CONCURRENCY_CONTEXT,
        maxConcurrency: 4, // 根据CPU核心数调整
        playwrightOptions: {
            headless: true
        }
    });
    
    // 任务队列处理
    await cluster.task(async ({ page, data: url }) => {
        await page.goto(url);
        return await page.evaluate(() => document.title);
    });
    
    // 添加任务
    cluster.queue('https://example.com');
    cluster.queue('https://example.org');
    
    // 获取结果
    cluster.on('taskend', (result) => {
        console.log(`Title: ${result}`);
    });
    
    await cluster.idle();
    await cluster.close();
}

4.2 结合消息队列实现分布式爬取

typescript 复制代码
import { Consumer } from 'sqs-consumer';
import AWS from 'aws-sdk';
import { scrapeService } from './services/scrape.service';

const app = Consumer.create({
    queueUrl: process.env.SQS_QUEUE_URL,
    handleMessage: async (message) => {
        const { url, options } = JSON.parse(message.Body!);
        const result = await scrapeService.scrape(url, options);
        
        // 处理结果,如存储到数据库或发送到另一个队列
        console.log(result);
    },
    sqs: new AWS.SQS()
});

app.on('error', (err) => {
    console.error(err.message);
});

app.on('processing_error', (err) => {
    console.error(err.message);
});

app.start();

5. 部署与监控

5.1 Docker部署方案

dockerfile 复制代码
FROM node:16

WORKDIR /app
COPY package*.json ./
RUN npm install

COPY . .

# 安装Playwright依赖
RUN npx playwright install
RUN npx playwright install-deps

CMD ["node", "dist/index.js"]

5.2 使用PM2进行进程管理

bash 复制代码
pm2 start dist/index.js --name "playwright-api" -i max
pm2 save
pm2 startup

5.3 健康检查与监控

typescript 复制代码
// 添加健康检查端点
app.get('/health', (req, res) => {
    res.json({
        status: 'UP',
        browser: browserService.isRunning(),
        timestamp: new Date().toISOString()
    });
});

// 添加Prometheus指标
import client from 'prom-client';
const collectDefaultMetrics = client.collectDefaultMetrics;
collectDefaultMetrics({ timeout: 5000 });

app.get('/metrics', async (req, res) => {
    res.set('Content-Type', client.register.contentType);
    res.end(await client.register.metrics());
});

6. 安全考虑

6.1 API认证

typescript 复制代码
import passport from 'passport';
import { BasicStrategy } from 'passport-http';

passport.use(new BasicStrategy((username, password, done) => {
    if (username === process.env.API_USER && password === process.env.API_PASS) {
        return done(null, { user: 'api' });
    }
    return done(null, false);
}));

// 保护爬虫端点
app.post('/api/scrape', 
    passport.authenticate('basic', { session: false }),
    scrapeController.scrape
);

6.2 请求限流

typescript 复制代码
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 100, // 每个IP限制100个请求
    message: 'Too many requests from this IP, please try again later'
});

app.use(limiter);

7. 测试与调试

7.1 单元测试示例

typescript 复制代码
import { test, expect } from '@playwright/test';
import { scrapeService } from '../src/services/scrape.service';

test.describe('ScrapeService', () => {
    test('should return page title', async () => {
        const result = await scrapeService.scrape('https://example.com');
        expect(result.success).toBe(true);
        expect(result.data.title).toContain('Example');
    });
});

7.2 调试技巧

typescript 复制代码
// 启用调试模式
const browser = await chromium.launch({
    headless: false,
    devtools: true
});

// 监听控制台输出
page.on('console', msg => {
    console.log('Browser console:', msg.text());
});

// 捕获网络请求
page.on('request', request => console.log('>>', request.method(), request.url()));
page.on('response', response => console.log('<<', response.status(), response.url()));

总结

以上方案提供了基于Playwright TypeScript/JavaScript实现API调用爬虫的完整路径,从基础实现到生产级架构,涵盖了:

  1. 基础API服务:Express与Playwright的简单集成
  2. 高级功能:请求拦截、动态参数支持、多种输出格式
  3. 生产架构:模块化设计、错误处理、资源管理
  4. 性能扩展:集群支持、消息队列集成
  5. 部署运维:Docker容器化、进程管理、监控
  6. 安全保障:API认证、请求限流
  7. 测试调试:单元测试、调试技巧

这些方案可以根据实际需求进行组合和调整,构建出适合不同场景的爬虫API服务。对于需要更高性能或更复杂业务逻辑的场景,可以考虑进一步引入分布式任务队列、缓存机制等高级架构。

原文博客链接:基于Playwright TypeScript/JavaScript的API调用爬虫成熟方案

相关推荐
颜酱7 小时前
图结构完全解析:从基础概念到遍历实现
javascript·后端·算法
喵手8 小时前
Python爬虫实战:旅游数据采集实战 - 携程&去哪儿酒店机票价格监控完整方案(附CSV导出 + SQLite持久化存储)!
爬虫·python·爬虫实战·零基础python爬虫教学·采集结果csv导出·旅游数据采集·携程/去哪儿酒店机票价格监控
小迷糊的学习记录8 小时前
Vuex 与 pinia
前端·javascript·vue.js
发现一只大呆瓜8 小时前
前端性能优化:图片懒加载的三种手写方案
前端·javascript·面试
不爱吃糖的程序媛8 小时前
Flutter 与 OpenHarmony 通信:Flutter Channel 使用指南
前端·javascript·flutter
利刃大大8 小时前
【Vue】Element-Plus快速入门 && Form && Card && Table && Tree && Dialog && Menu
前端·javascript·vue.js·element-plus
NEXT069 小时前
AI 应用工程化实战:使用 LangChain.js 编排 DeepSeek 复杂工作流
前端·javascript·langchain
光影少年9 小时前
react的hooks防抖和节流是怎样做的
前端·javascript·react.js
小毛驴85010 小时前
Vue 路由示例
前端·javascript·vue.js
后端小肥肠10 小时前
别再盲目抽卡了!Seedance 2.0 成本太高?教你用 Claude Code 100% 出片
人工智能·aigc·agent