谷粒的泥坑

🌿谷粒的生活笔记 —— 在数字世界的泥坑中,播种代码、文字的种子,静待每一份热爱自然生长

Node.js项目中通过Redis提升性能

在Node.js项目中,通过Redis提升性能的方案可以从以下几个方面详细展开,结合连接管理、数据操作优化、缓存策略及高级技巧来实现:


1. 连接管理与复用

  • 使用连接池
    避免频繁创建和关闭连接,通过连接池复用Redis连接,减少TCP握手开销。例如,使用ioredisnode-rediscreatePool方法,限制最大连接数以防止资源耗尽。

    1
    2
    const Redis = require('ioredis');
    const redis = new Redis({ host: '127.0.0.1', port: 6379, maxRetriesPerRequest: 3 });
  • 异步操作与Promise化
    Node.js的异步特性与Redis的异步API(如async/await)结合,避免阻塞事件循环。例如,使用ioredis的Promise接口。


2. 数据操作优化

  • 批量操作与管道技术(Pipelining)
    将多个命令合并为一次网络请求,减少RTT(往返时间)。适用于批量写入或读取场景。

    1
    2
    3
    4
    const pipeline = redis.pipeline();
    pipeline.set('key1', 'value1');
    pipeline.get('key2');
    pipeline.exec().then(results => console.log(results));
  • 合理选择数据结构
    根据业务需求选择高效的数据结构:

    • 哈希表(Hashes):存储对象,避免多个键查询。
    • 有序集合(Sorted Sets):实现排行榜等需要排序的功能。

3. 缓存策略设计

  • 读写穿透(Cache-Aside)
    读取时先查缓存,未命中则查数据库并回填缓存;写入时更新数据库并删除/更新缓存,避免脏数据。

    1
    2
    3
    4
    5
    6
    7
    async function getUser(userId) {
    const cached = await redis.get(`user:${userId}`);
    if (cached) return JSON.parse(cached);
    const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
    await redis.setex(`user:${userId}`, 3600, JSON.stringify(user)); // 设置1小时过期
    return user;
    }
  • 缓存预热与过期策略

    • 预热:启动时加载高频数据到Redis(如热门商品)。
    • TTL设置:根据数据更新频率设置合理的过期时间,平衡一致性与性能。

4. 高级技巧

  • 发布订阅(Pub/Sub)
    实现实时通知或事件驱动架构,减少轮询开销。例如,订单状态更新后通过Pub/Sub通知相关服务。
  • Lua脚本
    将复杂逻辑(如原子操作)封装为Lua脚本在Redis端执行,减少网络交互。
  • 分布式扩展
    使用Redis集群或分片应对高并发,结合ioredis.Cluster配置多节点。

5. 监控与调优

  • 性能指标监控
    通过INFO命令或工具(如Redis CLI)监控内存使用、命中率等,识别瓶颈。
  • 避免大键和慢查询
    使用SCAN替代KEYS遍历键,拆分大哈希表或列表。

示例项目结构

1
2
3
4
5
6
7
8
// 结合Express的缓存中间件示例
app.get('/api/data', async (req, res) => {
const cachedData = await redis.get('api-data');
if (cachedData) return res.json(JSON.parse(cachedData));
const data = await fetchFromDB(); // 数据库查询
await redis.setex('api-data', 60, JSON.stringify(data)); // 缓存60秒
res.json(data);
});

通过上述方案,Node.js项目可显著降低数据库负载、提高响应速度(如Facebook工程师案例中提到的性能提升)。需根据业务场景调整策略,例如高读写比场景侧重缓存,实时性要求高的场景结合Pub/Sub。

Playwright实现录制登录、保存Cookie及自动登录

Playwright实现录制登录、保存Cookie及自动登录的完整指南

Playwright是一个强大的浏览器自动化工具,支持录制用户操作、保存登录状态以及复用Cookie实现自动登录。下面我将详细介绍在Node.js环境下如何使用Playwright实现这些功能。

一、录制登录操作并保存Cookie

1. 使用codegen录制登录过程

Playwright提供了codegen命令可以录制用户操作并生成代码:

1
npx playwright codegen --save-storage=auth.json https://example.com/login

执行此命令会:

  1. 打开浏览器和Playwright Inspector窗口
  2. 记录你在登录页面的所有操作
  3. 在关闭浏览器后,将cookie和本地存储信息保存到auth.json文件中

2. 手动编码实现登录并保存状态

如果需要更精细的控制,可以手动编写登录脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { chromium } = require('playwright');

(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();

// 访问登录页面
await page.goto('https://example.com/login');

// 填写登录表单
await page.fill('#username', 'your_username');
await page.fill('#password', 'your_password');
await page.click('#login-button');

// 等待登录完成
await page.waitForNavigation();

// 保存登录状态到文件
await context.storageState({ path: 'auth.json' });

await browser.close();
})();

这种方法可以保存完整的浏览器上下文状态,包括cookies、localStorage和sessionStorage

二、使用保存的Cookie实现自动登录

1. 加载保存的状态文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { chromium } = require('playwright');

(async () => {
const browser = await chromium.launch({ headless: false });

// 加载之前保存的登录状态
const context = await browser.newContext({
storageState: 'auth.json'
});

const page = await context.newPage();

// 直接访问需要登录的页面
await page.goto('https://example.com/dashboard');

// 验证是否已登录
await page.screenshot({ path: 'logged-in.png' });

await browser.close();
})();

通过storageState选项加载保存的状态文件,可以跳过登录步骤直接进入已认证状态

2. 直接操作Cookies

如果需要更精细地控制Cookies,可以手动操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { chromium } = require('playwright');
const fs = require('fs');

(async () => {
// 从文件读取cookies
const cookies = JSON.parse(fs.readFileSync('cookies.json', 'utf-8'));

const browser = await chromium.launch();
const context = await browser.newContext();

// 添加cookies到上下文
await context.addCookies(cookies);

const page = await context.newPage();
await page.goto('https://example.com');

// 其他操作...

await browser.close();
})();

这种方法适合只需要cookies而不需要完整浏览器状态的场景

三、高级技巧与注意事项

1. Cookie的有效期管理

  • 检查cookie的expires属性,确保没有过期
  • 对于会话cookie(没有设置expires),需要在同一浏览器会话中使用

2. 处理动态内容

1
2
// 等待特定元素出现,确认登录成功
await page.waitForSelector('.user-avatar', { timeout: 5000 });

3. 多环境适配

1
2
3
4
5
6
7
// 可以根据不同环境加载不同的状态文件
const env = process.env.NODE_ENV || 'development';
const stateFile = `auth.${env}.json`;

const context = await browser.newContext({
storageState: fs.existsSync(stateFile) ? stateFile : undefined
});

4. 错误处理

1
2
3
4
5
6
try {
await page.goto('https://example.com/dashboard');
} catch (error) {
console.error('可能登录状态已失效:', error);
// 重新登录逻辑...
}

四、实际应用示例

1. 完整的登录状态保存与复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');

const STATE_PATH = path.join(__dirname, 'auth.json');

async function loginAndSaveState() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();

try {
await page.goto('https://example.com/login');

// 登录操作
await page.fill('#username', 'your_username');
await page.fill('#password', 'your_password');
await page.click('#login');

// 等待登录完成
await page.waitForURL('https://example.com/dashboard');

// 保存状态
await context.storageState({ path: STATE_PATH });
console.log('登录状态已保存');

return true;
} catch (error) {
console.error('登录失败:', error);
return false;
} finally {
await browser.close();
}
}

async function useSavedState() {
if (!fs.existsSync(STATE_PATH)) {
console.log('没有找到保存的登录状态,需要先登录');
return;
}

const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
storageState: STATE_PATH
});
const page = await context.newPage();

try {
await page.goto('https://example.com/dashboard');
console.log('成功使用保存的状态登录');

// 执行需要登录的操作...

} catch (error) {
console.error('使用保存的状态失败:', error);
} finally {
await browser.close();
}
}

// 使用示例
(async () => {
const useSaved = await useSavedState();
if (!useSaved) {
await loginAndSaveState();
await useSavedState();
}
})();

五、常见问题解决

  1. 登录状态无效

    • 检查cookie是否过期
    • 确认网站没有额外的安全措施(如IP绑定)
    • 可能需要更新保存的状态文件
  2. 跨域问题

    • 确保cookie的domainpath设置正确
    • 对于跨域场景,可能需要手动设置cookie属性
  3. 页面元素定位问题

    • 使用Playwright的定位策略(如getByText, getByRole等)
    • 考虑使用page.pause()进行调试

通过以上方法,你可以轻松实现Playwright的录制登录、保存Cookie以及自动登录功能,大大提高自动化测试和爬虫开发的效率。

Node.js项目中实现数据缓存进而提升服务器性能的方案整理

在Node.js项目中实现数据缓存是提升服务器性能的有效手段,以下是几种主流的缓存方案及其适用场景,结合了多个来源的最佳实践:


1. 内存缓存(快速但易失性)

  • 适用场景:高频访问的临时数据(如会话信息、计算结果),单服务器环境。

  • 推荐工具

    • node-cache:简单易用,支持过期时间设置。
      1
      2
      3
      4
      const NodeCache = require('node-cache');
      const cache = new NodeCache({ stdTTL: 60 }); // 60秒过期
      cache.set('key', 'value');
      const value = cache.get('key');
    • lru-cache:基于LRU算法(最近最少使用),适合限制内存占用的场景。
      1
      2
      const LRU = require('lru-cache');
      const cache = new LRU({ max: 500, maxAge: 1000 * 60 }); // 最多500条,1分钟过期
  • 优点:极快(内存读写),无网络开销。

  • 缺点:进程重启后数据丢失,多实例间无法共享。


2. 分布式缓存(多服务器共享)

  • 适用场景:多实例部署或需要持久化的缓存(如用户数据、热点查询)。

  • 推荐工具

    • Redis:高性能键值存储,支持复杂数据结构和持久化。
      1
      2
      3
      4
      const redis = require('redis');
      const client = redis.createClient();
      client.setex('key', 3600, 'value'); // 缓存1小时
      client.get('key', (err, reply) => console.log(reply));
    • Memcached:轻量级,适合简单键值缓存。
      1
      2
      3
      const Memcached = require('memcached');
      const memcached = new Memcached('localhost:11211');
      memcached.set('key', 'value', 300, (err) => {}); // 缓存5分钟
  • 优点:跨进程/服务器共享,高可用性(如Redis集群)。

  • 缺点:需额外维护缓存服务,网络延迟略高。


3. HTTP缓存(减少重复请求)

  • 适用场景:API响应或静态资源(如HTML、CSS文件)。
  • 实现方式:通过Cache-Control等响应头控制客户端或CDN缓存。
    1
    2
    3
    4
    5
    6
    const express = require('express');
    const app = express();
    app.get('/data', (req, res) => {
    res.set('Cache-Control', 'public, max-age=300'); // 缓存5分钟
    res.json({ data: 'cached' });
    });
  • 优点:减少服务器负载,提升客户端体验。
  • 缺点:仅适用于可公开缓存的数据。

4. 文件缓存(持久化但较慢)

  • 适用场景:低频变更的静态数据(如配置文件、模板)。
  • 示例
    1
    2
    3
    4
    5
    6
    const fs = require('fs');
    const cachePath = './cache.json';
    // 写入缓存
    fs.writeFileSync(cachePath, JSON.stringify({ key: 'value' }));
    // 读取缓存
    const data = JSON.parse(fs.readFileSync(cachePath));
  • 优点:数据持久化。
  • 缺点:I/O性能较低,不适合高频读写。

5. 数据库查询缓存

  • 适用场景:复杂查询结果缓存(如MySQL查询缓存或ORM层缓存)。
  • 工具:ORM(如Sequelize)内置缓存或手动结合Redis。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 使用Redis缓存数据库查询
    async function getCachedUser(id) {
    const key = `user:${id}`;
    const cached = await client.get(key);
    if (cached) return JSON.parse(cached);
    const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
    client.setex(key, 3600, JSON.stringify(user)); // 缓存1小时
    return user;
    }

选择建议

  • 单机临时数据node-cachelru-cache
  • 多实例/持久化:Redis(支持数据结构)或Memcached(简单键值)。
  • 静态资源:HTTP缓存 + CDN。
  • 敏感数据:避免缓存或加密存储。

注意事项

  • 设置合理的过期时间,避免脏数据。
  • 监控缓存命中率(如Redis的INFO命令)。
  • 处理缓存穿透(布隆过滤器)和雪崩(随机过期时间)。

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

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

Playwright作为微软开发的现代化浏览器自动化工具,特别适合用于构建API调用的爬虫服务。以下是使用TypeScript/JavaScript实现的成熟方案:

1. 基础API爬虫服务架构

1.1 Express + Playwright方案

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 支持动态参数和配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 拦截网络请求优化性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 完整的爬虫服务架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 浏览器服务管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 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 爬虫服务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 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 使用集群提高并发能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 结合消息队列实现分布式爬取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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部署方案

1
2
3
4
5
6
7
8
9
10
11
12
13
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进行进程管理

1
2
3
pm2 start dist/index.js --name "playwright-api" -i max
pm2 save
pm2 startup

5.3 健康检查与监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 添加健康检查端点
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认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 请求限流

1
2
3
4
5
6
7
8
9
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 单元测试示例

1
2
3
4
5
6
7
8
9
10
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 调试技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 启用调试模式
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实现Cookie免登录的完整方案

Playwright实现Cookie免登录的完整方案

Playwright提供了多种方式来实现手动登录后保存Cookie,后续访问时直接使用保存的Cookie免登录的功能。以下是几种成熟可靠的实现方法:

1. 使用storageState保存和加载登录状态

这是Playwright官方推荐的方式,可以保存完整的浏览器上下文状态,包括cookies和localStorage。

保存登录状态的实现代码(Node.js版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { chromium } = require('playwright');

(async () => {
// 启动浏览器
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();

// 执行登录操作
await page.goto('https://example.com/login');
await page.fill('#username', 'your_username');
await page.fill('#password', 'your_password');
await page.click('#login-button');

// 等待登录完成
await page.waitForNavigation();

// 保存状态到文件
await context.storageState({ path: 'auth/state.json' });

// 关闭浏览器
await browser.close();
})();

使用保存的状态免登录访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { chromium } = require('playwright');

(async () => {
const browser = await chromium.launch();

// 加载之前保存的状态
const context = await browser.newContext({
storageState: 'auth/state.json'
});

const page = await context.newPage();

// 直接访问需要登录的页面
await page.goto('https://example.com/dashboard');
console.log(await page.title());

await browser.close();
})();

这种方法的主要优点是:

  • 保存完整的浏览器状态,包括cookies和localStorage
  • 不需要手动处理cookie的解析和存储
  • 适用于大多数基于cookie或localStorage的认证系统

2. 手动保存和加载Cookies

如果需要更精细地控制cookie,可以手动保存和加载cookie。

保存Cookies的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { chromium } = require('playwright');
const fs = require('fs');

(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

// 登录过程...
await page.goto('https://example.com/login');
// ...填写登录表单并提交

// 获取cookies
const cookies = await context.cookies();

// 保存到文件
fs.writeFileSync('cookies.json', JSON.stringify(cookies));

await browser.close();
})();

使用保存的Cookies免登录访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { chromium } = require('playwright');
const fs = require('fs');

(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();

// 加载cookies
const cookies = JSON.parse(fs.readFileSync('cookies.json'));
await context.addCookies(cookies);

const page = await context.newPage();
await page.goto('https://example.com/dashboard');

await browser.close();
})();

3. 使用持久化用户数据目录

对于需要长期保持登录状态的场景,可以使用浏览器的持久化用户数据目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { chromium } = require('playwright');

(async () => {
// 首次运行 - 登录并保存数据
const browser = await chromium.launchPersistentContext('./user-data-dir', {
headless: false
});

const page = await browser.newPage();
await page.goto('https://example.com/login');
// ...登录操作

// 后续运行 - 直接使用已登录状态
const browser2 = await chromium.launchPersistentContext('./user-data-dir');
const page2 = await browser2.newPage();
await page2.goto('https://example.com/dashboard');
})();

动态渲染方案(Selenium/Playwright)的详细介绍

以下是关于动态渲染方案(Selenium/Playwright)的详细介绍,以及如何利用手动登录的Cookie跳过模拟登录流程的完整说明:


🔧 一、动态渲染方案:Selenium vs Playwright

1. 核心功能与适用场景

  • Selenium

    • 原理:通过浏览器驱动(如ChromeDriver)控制真实浏览器,模拟用户操作(点击、输入、滚动等)。
    • 优点
      • 兼容性广:支持老旧浏览器(如IE)及多操作系统。
      • 生态成熟:社区资源丰富,长期维护稳定。
    • 缺点
      • 速度慢:基于HTTP协议通信,渲染等待需手动实现(如WebDriverWait)。
      • 资源消耗高:浏览器实例占用内存大,高并发时易崩溃。
  • Playwright

    • 原理:通过WebSocket协议直接控制Chromium/Firefox/WebKit内核,内置浏览器二进制无需额外驱动。
    • 优点
      • 速度快:异步支持,无头模式请求耗时仅50-150ms(Selenium需200-500ms)。
      • 智能等待click()等操作自动等待元素就绪,减少Flaky问题。
      • 高级功能:内置网络拦截、设备模拟(如iPhone 13)、多上下文隔离Cookie。
    • 缺点
      • 对老旧浏览器支持有限,Python生态较新(文档可能不全)。

2. 性能与反爬能力对比

指标 Selenium Playwright
成功率(100次请求) 82% 95%
平均耗时 慢3.7倍 快(异步优化)
反爬绕过 依赖代理IP+UA轮换 内置Stealth模式隐藏自动化特征
典型用例 需兼容IE的测试 高并发爬取、复杂SPA页面

💡 建议选型:优先用Playwright处理动态渲染;若需兼容旧浏览器或复杂交互(如滑块验证),配合Selenium突破。


🍪 二、手动登录Cookie跳过模拟登录流程

1. 核心原理

网站登录后,服务器返回Cookie(如session_id)作为身份凭证。后续请求携带此Cookie即可直接访问受限数据,无需重复登录。

2. 操作步骤

  1. 获取手动登录后的Cookie

    • 浏览器开发者工具(F12 → Network → 刷新页面 → 复制Request Headers中的Cookie值)。
    • 插件导出:使用EditThisCookie等插件导出JSON格式Cookie。
  2. 在代码中注入Cookie

    • Selenium示例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      from selenium import webdriver
      driver = webdriver.Chrome()
      driver.get("https://example.com") # 先访问域名以设置Cookie作用域
      # 添加Cookie字典(需包含name/value/domain/path)
      driver.add_cookie({
      "name": "session_id",
      "value": "手动复制的Cookie值",
      "domain": "example.com",
      "path": "/"
      })
      driver.refresh() # 刷新页面生效
      driver.get("https://example.com/protected_data") # 直接访问受保护页面

    • Playwright示例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      from playwright.sync_api import sync_playwright
      with sync_playwright() as p:
      browser = p.chromium.launch()
      context = browser.new_context()
      # 批量添加Cookie
      context.add_cookies([{
      "name": "session_id",
      "value": "手动复制的Cookie值",
      "domain": "example.com",
      "path": "/"
      }])
      page = context.new_page()
      page.goto("https://example.com/protected_data") # 无需登录直接访问
      print(page.content())

3. 关键注意事项

  • Cookie过期:手动获取的Cookie可能失效(通常几小时~几天),需定期更新。
  • 作用域限制:Cookie需匹配目标页面的域名(domain)和路径(path),否则无效。
  • 安全校验:部分网站绑定Cookie与IP或User-Agent,需保持一致。
  • 动态Token:若网站使用JWT等Token认证,需从LocalStorage提取而非Cookie。

三、实战优化:结合动态渲染与Cookie方案

1. 高效架构设计

1
2
3
4
5
6
7
8
graph TD
A[用户手动登录] -->|获取Cookie| B(存储至数据库/文件)
B --> C{爬虫启动}
C -->|注入Cookie| D[Playwright渲染页面]
C -->|Token过期| E[Selenium处理重新登录]
D --> F[提取动态数据]
E -->|更新Cookie| B
F --> G[数据持久化]
  • 优势:避免每次爬取都模拟登录,减少验证码触发概率,提升速度。

2. 代码示例(Playwright + Cookie复用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import asyncio
from playwright.async_api import async_playwright

async def crawl_protected_data(cookie_value):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
await context.add_cookies([{
"name": "session",
"value": cookie_value,
"domain": "target-site.com",
"path": "/"
}])
page = await context.new_page()
await page.goto("https://target-site.com/dynamic-content")
# 等待异步加载
await page.wait_for_selector(".data-table", timeout=10000)
data = await page.evaluate("""() => {
return document.querySelector('.data-table').innerHTML;
}""")
print(data)
await browser.close()

# 从文件读取手动登录的Cookie
with open("cookie.txt") as f:
cookie = f.read().strip()
asyncio.run(crawl_protected_data(cookie))

🛡️ 四、反爬策略与稳定性保障

  1. IP轮换:每5~10次请求更换代理IP(如亿牛云服务),避免被封禁。
  2. 行为模拟
    • Playwright使用slow_mo参数模拟真人操作间隔。
    • Selenium通过ActionChains模拟鼠标轨迹。
  3. Cookie动态刷新
    1
    2
    3
    4
    5
    6
    7
    8
    # 监测到Cookie过期时自动重新登录
    if "login_page" in page.content():
    page.fill("#username", "user")
    page.fill("#password", "pass")
    page.click("#submit")
    new_cookies = await page.context.cookies()
    update_cookie_db(new_cookies) # 存储新Cookie


💎 总结

  • 动态渲染选型:优先Playwright(高效异步+智能等待),兼容性需求选Selenium。
  • Cookie跳过登录:适用于会话保持,但需注意作用域匹配与过期刷新。
  • 混合架构:手动Cookie初始化 + Playwright渲染 + Selenium处理验证码,兼顾效率与稳定性。

实测数据表明,该方案在亚航等反爬严格场景下,成功率可达95%以上。建议首次部署时用无头模式调试,稳定后转为Headless运行。

nextjs项目在 npm run build的时候报错了,报错信息如下 Types of property 'searchParams' are incompatible

nextjs项目在 npm run build的时候报错了,报错信息如下 Type error: Type ‘{ searchParams: { paymentIntentId?: string | undefined; orderId?: string | undefined; }; }’ does not satisfy the constraint ‘PageProps’. Types of property ‘searchParams’ are incompatible. Type ‘{ paymentIntentId?: string | undefined; orderId?: string | undefined; }’ is missing the following properties from type ‘Promise‘: then, catch, finally, [Symbol.toStringTag]

基于 Next.js 和 Tailwind CSS 实现左右容器滚动联动效果

很多的电商项目中,会有容器动态联动的效果,而我的项目是用到的nextjs和tailwindcss,所以就写了这个示例,其中这个联动的效果蛮好的。

以下是一个基于 Next.js 和 Tailwind CSS 实现左右容器滚动联动效果的完整示例代码,使用 position: sticky 实现低高度容器先固定、高高度容器滚动到底部后联动的效果:

WordPress自动化定时备份系统

网站的备份是一个很重要的工作,可以通过以下方案实现WordPress自动化备份系统:

一、脚本核心功能设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash
# 定义备份目录和文件名
BACKUP_DIR="/opt/wordpress_backups"
DB_FILE="${BACKUP_DIR}/db_$(date +%Y%m%d).sql"
WEB_DIR="/var/www/html"
HASH_FILE="${BACKUP_DIR}/last_hash.txt"

# 创建备份目录
mkdir -p $BACKUP_DIR

# 导出数据库并计算哈希
mysqldump -uDB_USER -pDB_PASSWORD DB_NAME > $DB_FILE
CURRENT_HASH=$(sha256sum $DB_FILE | awk '{print $1}')

# 检查数据库变化
if [ -f $HASH_FILE ] && [ "$CURRENT_HASH" == "$(cat $HASH_FILE)" ]; then
echo "数据库未变化,跳过备份"
rm $DB_FILE
exit 0
fi

# 备份网站文件
tar -czf "${BACKUP_DIR}/web_$(date +%Y%m%d).tar.gz" $WEB_DIR

# 记录新哈希
echo $CURRENT_HASH > $HASH_FILE

# 提交到Git仓库
cd $BACKUP_DIR
git add .
git commit -m "自动备份 $(date +%Y%m%d)"
git push origin main

二、关键实现细节

  1. 数据库变更检测
    通过SHA256哈希算法比对当日与上次备份的数据库内容差异,避免重复备份未变化数据。相比时间戳检测,哈希校验能精准识别内容变更。

  2. 备份范围控制
    • 数据库:使用mysqldump导出完整SQL文件

• 网站代码:打包WordPress安装目录(通常为/var/www/html)

  1. Git仓库管理
    建议创建专用Git仓库,配置SSH密钥实现免密推送。可添加.gitignore文件保留最近7天备份:
1
2
3
4
*.sql
*.tar.gz
!db_$(date +\%Y\%m\%d -d "-7 days").sql
!web_$(date +\%Y\%m\%d -d "-7 days").tar.gz

三、自动化部署步骤

  1. 环境准备
1
2
3
4
5
# 创建备份目录并初始化Git
sudo mkdir -p /opt/wordpress_backups
sudo chown -R $USER:$USER /opt/wordpress_backups
cd /opt/wordpress_backups && git init
git remote add origin GIT_REPO_URL
  1. 定时任务配置
    通过crontab设置每日4点执行:
1
echo "0 4 * * * /bin/bash /path/to/backup_script.sh" | crontab -
  1. 权限配置
    /etc/sudoers添加:
1
www-data ALL=(ALL) NOPASSWD: /usr/bin/mysqldump

四、增强方案建议

  1. 备份加密
    使用GPG加密敏感数据:
1
gpg --batch --yes --passphrase "YOUR_PASSPHRASE" -c $DB_FILE
  1. 异常通知
    添加邮件通知功能:
1
2
3
if [ $? -ne 0 ]; then
echo "备份失败" | mail -s "WordPress备份警报" admin@example.com
fi
  1. 云存储冗余
    可结合云厂商CLI工具实现多地存储:
1
2
# 腾讯云COS示例
coscmd upload "${BACKUP_DIR}/web_*.tar.gz" /

五、注意事项

  1. 确保MySQL用户具有SELECT, LOCK TABLES权限
  2. Git仓库建议使用LFS管理大文件
  3. 定期测试备份恢复流程(建议每月一次)
  4. 备份目录权限设置为700防止未授权访问

该方案结合了数据库哈希校验、Git版本控制等技术,在保证备份可靠性的同时实现最小化存储消耗。可根据实际服务器配置调整备份策略,例如使用--hex-blob参数处理二进制字段,或添加--skip-lock-tables参数避免锁表影响网站运行。

crontab的用法整理

crontab 是 Linux/Unix 系统中用于设置定时任务的工具,可以让系统在指定时间自动执行脚本或命令。以下是 详细用法指南,包括基础语法、配置方法、调试技巧和实际示例。


一、基本语法

  1. crontab 文件格式
    每个定时任务占一行,格式如下:
1
2
3
4
5
6
7
* * * * * command_to_execute
│ │ │ │ │
│ │ │ │ └── 星期几 (0-6, 0=周日)
│ │ │ └──── 月份 (1-12)
│ │ └────── 日 (1-31)
│ └──────── 小时 (0-23)
└────────── 分钟 (0-59)
  1. 特殊符号
    符号 说明 示例
    * 任意值(每分钟/每小时等) * * * * * → 每分钟执行
    , 指定多个时间点 0,15,30 * * * * → 每小时的 0、15、30 分执行
    - 时间范围 0 9-17 * * * → 每天 9 点到 17 点整点执行
    / 间隔时间 */5 * * * * → 每 5 分钟执行

二、常用操作

  1. 编辑 crontab
1
crontab -e  # 编辑当前用户的定时任务

(首次使用会提示选择编辑器,推荐 nanovim

  1. 查看 crontab
1
crontab -l  # 列出当前用户的定时任务
  1. 删除 crontab
1
crontab -r  # 删除所有定时任务(谨慎使用!)
  1. 指定用户
1
crontab -u username -e  # 编辑指定用户的定时任务(需 root 权限)

三、实际示例

  1. 每天凌晨 4 点执行备份脚本
1
0 4 * * * /bin/bash /path/to/backup_script.sh
  1. 每小时的第 5 分钟执行
1
5 * * * * /usr/bin/python3 /path/to/script.py
  1. 每周一 8:30 发送邮件
1
30 8 * * 1 /usr/sbin/sendmail user@example.com < /tmp/report.txt
  1. 每 10 分钟检查服务
1
*/10 * * * * /usr/bin/systemctl check nginx
  1. 每月 1 号凌晨 3 点清理日志
1
0 3 1 * * /bin/rm -rf /var/log/nginx/*.log

四、环境变量问题
cron 默认环境与终端不同,可能导致脚本执行失败。解决方法:

  1. 指定完整路径
1
0 * * * * /usr/bin/curl https://example.com/api/ping
  1. 在 crontab 中加载环境
1
2
3
4
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

0 * * * * source ~/.bashrc && /path/to/script.sh
  1. 在脚本内设置环境
1
2
3
#!/bin/bash
export PATH=/usr/local/bin:$PATH
# 后续命令...

五、日志与调试

  1. 查看 cron 执行日志
1
2
tail -f /var/log/syslog | grep cron  # Ubuntu/Debian
tail -f /var/log/cron # CentOS/RHEL
  1. 重定向输出到文件
1
0 * * * * /path/to/script.sh >> /var/log/cron.log 2>&1

>> 追加日志

2>&1 将错误输出和标准输出合并

  1. 测试 cron 任务
1
2
3
4
5
# 手动运行脚本,确保无报错
/bin/bash /path/to/script.sh

# 强制立即执行一次(仅适用于某些系统)
crontab -l | grep -v "^#" | cut -f 6- -d " " | while read cmd; do eval $cmd; done

六、注意事项

  1. 脚本权限
    1
    chmod +x /path/to/script.sh  # 确保可执行
  2. 避免频繁任务
    * * * * *(每分钟)可能对系统造成负担,建议至少间隔 5 分钟(*/5 * * * *)。
  3. 依赖问题
    如果脚本依赖 GUI 或 DISPLAY,需额外配置:
    1
    * * * * * export DISPLAY=:0 && /path/to/gui_script.sh
  4. 容器环境
    在 Docker 中建议使用 supervisord 或直接通过 CMD 运行脚本。

七、高级用法

  1. 随机延迟(避免任务集中执行)
1
2
# 每天 3 点随机延迟 0-300 秒执行
0 3 * * * sleep $((RANDOM\%300)) && /path/to/script.sh
  1. 锁机制(防止重复执行)
1
*/10 * * * * flock -n /tmp/backup.lock -c "/path/to/backup.sh"
  1. 邮件通知
1
2
MAILTO="admin@example.com"
0 * * * * /path/to/monitor.sh

(任务输出会发送到指定邮箱)


总结

操作 命令
编辑定时任务 crontab -e
查看定时任务 crontab -l
删除所有任务 crontab -r
指定用户 crontab -u username -e
日志查看 tail -f /var/log/cron

掌握这些技巧后,你可以轻松实现 自动化备份、日志轮转、服务监控 等任务!