返回所有文章
2152 9 分钟阅读访问统计加载中

Pixiv爬虫/下载/代理,无需服务器即可一键部署!

爬虫serverlesspixiv好玩

项目简介

把去年用python写的pixiv爬虫项目用AI和serverless架构重写了一下,支持定时任务,支持下载,支持代理访问。

✨ 核心特性

  • 无服务器架构:基于 Vercel + Cloudflare Workers,零运维成本
  • 智能爬取:支持热度过滤,自动发现优质内容
  • 图片代理:解决跨域问题,提供高速图片访问
  • 数据存储:集成 Supabase 数据库,支持复杂查询
  • 批量下载:支持图片批量下载到 Cloudflare R2
  • 定时任务:自动爬取排行榜,无需人工干预

️ 系统架构

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Vercel API    │    │ Cloudflare Cron │    │   Supabase DB   │
│   (主服务)       │◄──►│   (定时任务)     │◄──►│   (数据存储)     │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  图片代理服务    │    │   爬虫调度器     │    │   数据分析API   │
│ (跨域解决方案)   │    │  (任务分发)      │    │  (统计查询)      │
└─────────────────┘    └─────────────────┘    └─────────────────┘

核心功能实现

1. 智能爬虫引擎

我们的爬虫不仅能获取基础信息,还能智能分析内容质量:

/**
 * Pixiv 爬虫核心类
 * 支持智能推荐和热度计算
 */
export class PixivCrawler {
  private headers: any;
  private logManager: any;
  private taskId: string;

  constructor(pid: string, headers: any, logManager: any, taskId: string) {
    this.headers = headers;
    this.logManager = logManager;
    this.taskId = taskId;
  }

  /**
   * 获取作品详细信息并计算热度
   * @param pid 作品ID
   * @returns 作品信息和热度分数
   */
  async getIllustDetail(pid: string) {
    try {
      const url = `https://www.pixiv.net/ajax/illust/${pid}`;
      const response = await fetch(url, { headers: this.headers });
      const data = await response.json();

      if (data.error) {
        throw new Error(`API错误: ${data.message}`);
      }

      const illust = data.body;

      // 计算热度分数
      const popularity = this.calculatePopularity(
        illust.likeCount,
        illust.bookmarkCount,
        illust.viewCount
      );

      return {
        pid: illust.id,
        title: illust.title,
        tags: illust.tags.tags.map((tag: any) => tag.tag),
        likeCount: illust.likeCount,
        bookmarkCount: illust.bookmarkCount,
        viewCount: illust.viewCount,
        popularity,
        createDate: illust.createDate
      };
    } catch (error) {
      this.logManager.addLog(`获取作品 ${pid} 失败: ${error.message}`, 'error', this.taskId);
      throw error;
    }
  }

  /**
   * 热度计算算法
   * 综合考虑点赞、收藏、浏览量
   */
  private calculatePopularity(likes: number, bookmarks: number, views: number): number {
    if (views === 0) return 0;

    const likeRate = likes / views;
    const bookmarkRate = bookmarks / views;

    // 加权计算热度分数
    return (likeRate * 0.3 + bookmarkRate * 0.7) * Math.log10(views + 1);
  }
}

2. 图片代理服务

解决跨域问题,提供高速图片访问:

/**
 * Pixiv 图片代理服务
 * 支持多尺寸智能选择和自动降级
 */
export class PixivProxy {
  private headers: any;

  constructor() {
    this.headers = {
      'Referer': 'https://www.pixiv.net/',
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    };
  }

  /**
   * 代理访问 Pixiv 图片
   * @param pid 图片ID
   * @param size 期望尺寸
   * @returns 图片数据流
   */
  async proxyImage(pid: string, size: string = 'regular') {
    // 尺寸优先级:thumb_mini -> small -> regular -> original
    const sizeOptions = ['thumb_mini', 'small', 'regular', 'original'];
    const startIndex = sizeOptions.indexOf(size);

    if (startIndex === -1) {
      throw new Error(`不支持的图片尺寸: ${size}`);
    }

    // 按优先级尝试获取图片
    for (let i = startIndex; i < sizeOptions.length; i++) {
      try {
        const currentSize = sizeOptions[i];
        const imageUrl = await this.getImageUrl(pid, currentSize);

        if (imageUrl) {
          const imageResponse = await fetch(imageUrl, {
            headers: this.headers
          });

          if (imageResponse.ok) {
            return {
              data: imageResponse.body,
              contentType: imageResponse.headers.get('content-type'),
              size: currentSize
            };
          }
        }
      } catch (error) {
        console.log(`尺寸 ${sizeOptions[i]} 获取失败,尝试下一个尺寸`);
        continue;
      }
    }

    throw new Error(`无法获取图片 ${pid} 的任何尺寸版本`);
  }

  /**
   * 获取指定尺寸的图片URL
   */
  private async getImageUrl(pid: string, size: string): Promise<string | null> {
    try {
      const response = await fetch(`https://www.pixiv.net/ajax/illust/${pid}`, {
        headers: this.headers
      });

      const data = await response.json();
      const urls = data.body?.urls;

      return urls?.[size] || null;
    } catch (error) {
      return null;
    }
  }
}

3. API 接口设计

提供完整的 RESTful API:

/**
 * 主 API 处理器
 * 支持多种操作:爬取、下载、代理、统计
 */
export default async function handler(req: VercelRequest, res: VercelResponse) {
  // 设置 CORS 头
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return;
  }

  const { action, pid, size } = req.query;

  try {
    switch (action) {
      case 'proxy-image':
        // 图片代理访问
        if (!pid) {
          res.status(400).json({ error: '缺少 pid 参数' });
          return;
        }

        const proxy = new PixivProxy();
        const imageResult = await proxy.proxyImage(pid as string, size as string);

        res.setHeader('Content-Type', imageResult.contentType);
        res.setHeader('Cache-Control', 'public, max-age=86400'); // 缓存1天

        return imageResult.data.pipe(res);

      case 'get-pic':
        // 获取图片信息
        const crawler = new PixivCrawler(pid as string, getPixivHeaders(), logManager, 'api_request');
        const illustInfo = await crawler.getIllustDetail(pid as string);

        res.status(200).json({
          success: true,
          data: illustInfo
        });
        break;

      case 'stats':
        // 获取统计信息
        const supabase = new SupabaseService();
        const stats = await supabase.getStats();

        res.status(200).json({
          success: true,
          stats
        });
        break;

      default:
        // 返回 Web 界面
        const htmlContent = getWebInterface();
        res.setHeader('Content-Type', 'text/html');
        res.status(200).send(htmlContent);
    }
  } catch (error) {
    res.status(500).json({
      error: '服务器内部错误',
      message: error.message
    });
  }
}

4. 定时任务调度

使用 Cloudflare Cron Worker 实现自动化:

/**
 * Cloudflare Cron Worker
 * 定时执行爬取任务
 */
export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    console.log('定时任务开始执行:', new Date().toISOString());

    try {
      // 每日排行榜爬取
      if (shouldRunDailyRanking(event.cron)) {
        await triggerRankingCrawl('daily', env);
      }

      // 每周排行榜爬取
      if (shouldRunWeeklyRanking(event.cron)) {
        await triggerRankingCrawl('weekly', env);
      }

      // 清理过期日志
      if (shouldCleanLogs(event.cron)) {
        await cleanExpiredLogs(env);
      }

    } catch (error) {
      console.error('定时任务执行失败:', error);
    }
  }
};

/**
 * 触发排行榜爬取
 */
async function triggerRankingCrawl(type: 'daily' | 'weekly' | 'monthly', env: Env) {
  const endpoint = `${env.MAIN_SERVICE_URL}/api/?action=${type}`;

  try {
    const response = await fetch(endpoint, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${env.API_TOKEN}`
      }
    });

    if (response.ok) {
      console.log(`${type} 排行榜爬取任务已触发`);
    } else {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
  } catch (error) {
    console.error(`触发 ${type} 排行榜爬取失败:`, error);
  }
}

3分钟快速部署

步骤1:克隆项目

git clone https://github.com/your-username/serverless_pixiv_crawler.git
cd serverless_pixiv_crawler
npm install

步骤2:配置环境变量

复制 .env.example 为 .env:

# Supabase 数据库配置
SUPABASE_URL=your_supabase_url_here
SUPABASE_ANON_KEY=your_supabase_anon_key_here

# Pixiv 配置
PIXIV_COOKIE=your_pixiv_cookie_here

# Cloudflare R2 配置(可选)
CLOUDFLARE_ACCOUNT_ID=your_account_id
CLOUDFLARE_ACCESS_KEY_ID=your_access_key
CLOUDFLARE_SECRET_ACCESS_KEY=your_secret_key
CLOUDFLARE_BUCKET_NAME=your_bucket_name

步骤3:部署到 Vercel

# 安装 Vercel CLI
npm i -g vercel

# 登录并部署
vercel login
vercel --prod

步骤4:部署定时任务(可选)

cd cron_worker
npm install

# 配置 Cloudflare Workers
npx wrangler login
npx wrangler deploy

功能演示

1. Web 管理界面

部署完成后,访问你的 Vercel 域名,你将看到一个现代化的管理界面:

  • 实时统计:显示爬取数量、成功率等关键指标
  • 任务管理:启动、停止、监控爬取任务
  • 日志查看:实时查看系统运行日志
  • 数据搜索:快速查找和筛选爬取的内容

2. API 接口使用

// 获取图片信息
fetch('https://your-domain.vercel.app/api/?action=get-pic&pid=123456')
  .then(res => res.json())
  .then(data => console.log(data));

// 代理访问图片
const imageUrl = 'https://your-domain.vercel.app/api/?action=proxy-image&pid=123456&size=regular';
document.getElementById('image').src = imageUrl;

// 启动爬取任务
fetch('https://your-domain.vercel.app/api/', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    pid: '123456',
    targetNum: 1000,
    popularityThreshold: 0.22
  })
});

3. 数据分析

系统自动收集和分析数据,提供丰富的统计信息:

-- 热门标签统计
SELECT tag, COUNT(*) as count
FROM illustrations
CROSS JOIN LATERAL unnest(tags) as tag
GROUP BY tag
ORDER BY count DESC
LIMIT 20;

-- 热度分布分析
SELECT
  CASE
    WHEN popularity >= 0.8 THEN '超高热度'
    WHEN popularity >= 0.5 THEN '高热度'
    WHEN popularity >= 0.2 THEN '中等热度'
    ELSE '低热度'
  END as level,
  COUNT(*) as count
FROM illustrations
GROUP BY level;

高级特性

1. 智能推荐算法

系统内置智能推荐算法,能够: - 内容发现:基于已有数据发现相似优质内容 - 热度预测:预测内容未来的热度趋势 - 风格分析:识别和分类不同的艺术风格

2. 防封机制

  • 请求头轮换:模拟真实浏览器行为
  • ⏱️ 智能延迟:动态调整请求间隔
  • 错误重试:智能处理网络异常

3. 数据质量保证

  • 自动去重:避免重复数据
  • 内容验证:确保数据完整性
  • 质量评分:为每个内容计算质量分数

成本分析

这个项目的最大优势是完全免费

服务免费额度足够支撑
Vercel100GB 带宽/月中小型项目
Supabase500MB 数据库50万条记录
Cloudflare Workers10万请求/天大部分使用场景
Cloudflare R210GB 存储数万张图片

故障排除

常见问题

  1. 部署失败
  2. 检查环境变量配置
  3. 确认 Supabase 连接正常
  4. 爬取失败
  5. 验证 Pixiv Cookie 有效性
  6. 检查网络连接状态
  7. 图片无法显示
  8. 确认代理服务正常运行
  9. 检查跨域配置

性能优化建议

  • 启用缓存:合理设置 CDN 缓存策略
  • 监控指标:定期检查系统性能指标
  • 定期清理:清理过期数据和日志

总结

这个 Serverless Pixiv 爬虫项目展示了现代化 Web 开发的强大能力:

  • 零运维成本:完全基于云服务,无需管理服务器
  • 高可扩展性:自动伸缩,应对流量波动
  • 功能完整:爬取、存储、分析、展示一体化
  • 部署简单:3分钟即可完成部署

无论你是想学习 Serverless 架构,还是需要一个实用的数据收集工具,这个项目都是一个很好的起点。

相关资源


免责声明:本项目仅供学习和研究使用,请遵守相关网站的服务条款,合理控制爬取频率。

如果这篇文章对你有帮助,请给项目点个 ⭐ Star!

v2-a211000410c25bbc31bbac49312d1020_1440w.png

喜欢这篇文章?

如果这篇文章帮到了你,可以请我喝杯咖啡,支持我继续写下去。

评论