1. 项目概述:为什么选择 Pixiv 作为爬虫实践对象?

如果你对二次元文化、插画艺术或者仅仅是海量的精美图片感兴趣,那么 Pixiv(简称 P 站)这个名字你一定不陌生。作为一个全球性的插画艺术家社交与作品发布平台,它汇聚了数百万创作者和数以亿计的作品。对于开发者,尤其是数据采集和 Python 学习者而言,Pixiv 是一个极具挑战性又充满价值的“实战练兵场”。它不像一些简单的静态网页,Pixiv 的防护机制、动态加载逻辑、以及庞大的数据量,都让针对它的爬虫项目变得复杂而有趣。这个项目不仅仅是写几行 requests.get() 那么简单,它涉及到登录态管理、反爬策略应对、异步高效抓取、以及大规模数据的存储与去重,几乎涵盖了现代网络爬虫工程中会遇到的大部分核心问题。通过这个项目,你能学到的远不止“如何下载图片”,而是构建一个健壮、可维护、高效率的数据采集系统的完整思路。

2. 核心需求与目标拆解:我们到底要爬什么?

在动手写一行代码之前,明确目标至关重要。针对 Pixiv 的爬虫,需求可以非常多样,我们需要将其具体化。

2.1 常见爬取目标分析

  1. 按画师(用户)爬取 :这是最直接的需求。给定一个或多个画师的 ID 或主页 URL,爬取他们发布的所有作品(插画、漫画、动图)。这需要处理画师主页的作品列表翻页,并识别出每件作品的唯一标识符(Illust ID)。
  2. 按标签(Tag)爬取 :Pixiv 的标签系统非常强大。我们可以爬取特定标签(如“原神”、“Fate/GrandOrder”、“風景”)下的热门作品或最新作品。这涉及到标签搜索页面的解析和排序规则的处理。
  3. 按收藏(Bookmark)爬取 :爬取自己或某个公开用户收藏夹里的作品。这通常需要有效的登录状态(Cookie),因为非公开收藏夹无法访问。
  4. 排行榜数据爬取 :Pixiv 有日榜、周榜、月榜、原创榜、新人榜等。爬取排行榜是获取高质量作品的捷径,页面结构相对固定。
  5. 作品详情深度爬取 :除了图片本身,作品标题、描述、上传时间、浏览量、点赞数、收藏数、使用的标签、评论等元数据也极具价值,可用于数据分析或推荐系统。

2.2 项目核心目标设定

对于一个综合性的学习项目,我建议设定一个涵盖多方面的目标,例如: “构建一个能够根据画师ID或标签名,自动爬取其下所有作品的原图(或多种尺寸图),并完整保存作品元数据(标题、画师、标签、统计信息)到本地数据库或文件系统的爬虫系统。”

这个目标包含了用户输入、页面遍历、数据解析、多媒体下载、元数据存储等多个环节,是一个完整的 pipeline。

3. 技术栈选型与核心工具解析

工欲善其事,必先利其器。针对 Pixiv 的特点,我们的技术栈需要精心挑选。

3.1 网络请求库: httpx 优于 requests

虽然 requests 库简单易用,但在应对现代复杂网页和需要高性能的场景时, httpx 是更佳选择。

  • 异步支持 : httpx 原生支持异步( async/await ),这对于需要发起大量网络请求(如下载成千上万张图片)的爬虫来说,性能提升是数量级的。Pixiv 的图片服务器支持并发请求,异步能极大缩短总耗时。
  • HTTP/2 支持 : httpx 默认支持 HTTP/2,在某些情况下可以提升连接效率。
  • 兼容性 : httpx 的 API 设计与 requests 高度相似,学习成本低。
# 同步请求(类似requests)
import httpx
client = httpx.Client(headers=your_headers)
response = client.get('https://www.pixiv.net/')
client.close()

# 异步请求(高性能之选)
import asyncio
import httpx
async def fetch(url):
    async with httpx.AsyncClient(headers=your_headers) as client:
        response = await client.get(url)
        return response

3.2 解析库: parsel 或 BeautifulSoup 搭配 json 解析

Pixiv 的页面数据大量通过前端 JavaScript 渲染,直接解析 HTML 往往找不到关键数据。观察网络请求会发现,很多数据是以 JSON 格式内嵌在 HTML 的 <script> 标签中,或通过 API 接口返回。

  • parsel : 由 Scrapy 团队开发,兼容 CSS 选择器和 XPath,性能很好。适合快速提取 HTML 中的片段。
  • BeautifulSoup : 老牌解析库,语法灵活,容错性好,适合处理“不干净”的 HTML。
  • 核心技巧 : 优先尝试直接寻找 JSON 数据。在页面源码中搜索 `` 或 pixiv.context 等关键词,经常能直接找到包含作品列表、用户信息的完整 JSON 对象,这比解析 DOM 树要稳定和高效得多。
import json
import re
from parsel import Selector

html = ... # 获取的页面HTML
selector = Selector(text=html)

# 尝试从js变量中提取json
script_text = selector.xpath('//script[contains(text(), "pixiv.context")]/text()').get()
if script_text:
    # 使用正则表达式提取JSON部分
    match = re.search(r'pixiv\.context\s*=\s*(\{.*?\});', script_text, re.DOTALL)
    if match:
        data = json.loads(match.group(1))
        # data 中可能包含了作品数据、用户信息等

3.3 异步任务与队列: asyncio + aiofiles + aiohttp (可选)

对于下载任务,我们需要管理成千上万的协程。

  • asyncio : Python 原生的异步IO框架,用于创建和管理事件循环、协程、任务。
  • aiofiles : 提供异步文件读写操作,避免在保存图片时阻塞事件循环。
  • aiohttp : 一个基于 asyncio 的 HTTP 客户端/服务器框架。虽然 httpx.AsyncClient 已经很强,但在某些极端定制化场景, aiohttp 的底层控制更灵活。本项目用 httpx 通常足够。

3.4 数据存储:SQLite 或 PostgreSQL

我们需要持久化存储作品元数据。

  • SQLite : 轻量级,单文件,无需安装数据库服务器。非常适合个人使用、小型项目或原型开发。Python 标准库 sqlite3 直接支持。
  • PostgreSQL : 功能强大的开源关系数据库。如果需要处理极其庞大的数据量(数百万作品),进行复杂查询或未来扩展,PostgreSQL 是更专业的选择。可以使用 asyncpg 库进行异步操作。

表结构设计示例(SQLite) :

CREATE TABLE IF NOT EXISTS illusts (
    id INTEGER PRIMARY KEY, -- 作品ID,Pixiv唯一标识
    title TEXT, -- 作品标题
    user_id INTEGER, -- 画师ID
    user_name TEXT, -- 画师名
    tags TEXT, -- 标签,可以用JSON字符串或逗号分隔存储
    width INTEGER, -- 图片宽度
    height INTEGER, -- 图片高度
    page_count INTEGER, -- 作品页数(漫画多图)
    view_count INTEGER, -- 浏览数
    like_count INTEGER, -- 点赞数
    bookmark_count INTEGER, -- 收藏数
    create_date TIMESTAMP, -- 创建时间
    upload_timestamp INTEGER, -- 上传时间戳
    image_urls TEXT, -- 原图/多尺寸图URL,JSON格式存储
    local_path TEXT, -- 本地存储路径
    crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 爬取时间
);

3.5 反爬应对核心:请求头(Headers)与 Cookies

这是爬取 Pixiv 最关键也是最复杂的一环。

  1. User-Agent : 必须设置为一个常见的浏览器标识,例如 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... 。伪装成浏览器是基础中的基础。
  2. Referer : Pixiv 对图片请求有严格的 Referer 检查 。直接请求图片链接会被拒绝。必须在请求图片的 headers 中加上 Referer: https://www.pixiv.net/ 或具体作品页的 URL。这是最常见的坑。
  3. Cookies : 要爬取非公开内容(如关注列表、特定用户收藏)或进行大量请求,你需要一个已登录的 Cookie。获取方式是在浏览器中登录 Pixiv 后,从开发者工具(F12)的“网络”或“应用”标签中复制 PHPSESSID 等关键 Cookie 字段。

    重要警告 : 绝对不要在任何公开场合(如 GitHub)提交或分享你的 Pixiv Cookie!这等同于交出你的账号密码,可能导致账号被封禁。应将 Cookie 存储在本地配置文件或环境变量中,并通过 .gitignore 确保其不会被提交。

一个基础的请求头示例 :

BASE_HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Accept-Encoding': 'gzip, deflate, br',
    'DNT': '1',
    'Connection': 'keep-alive',
    'Upgrade-Insecure-Requests': '1',
    'Sec-Fetch-Dest': 'document',
    'Sec-Fetch-Mode': 'navigate',
    'Sec-Fetch-Site': 'none',
    'Sec-Fetch-User': '?1',
    'Cache-Control': 'max-age=0',
}
# 在请求具体作品页或API时,需要添加Referer
IMAGE_REFERER = 'https://www.pixiv.net/'

4. 爬虫架构设计与核心流程

一个健壮的爬虫不应该是一堆线性脚本,而应该是一个有清晰模块划分的小型系统。这里我设计一个基于生产者-消费者模式的异步爬虫架构。

4.1 系统模块划分

  1. 调度器(Scheduler) : 负责管理初始任务队列(如画师ID列表、标签列表),并将任务分发给爬取器。它还需要处理去重,避免重复爬取同一作品。
  2. 爬取器(Fetcher/Crawler) : 核心模块。包含网页下载器(使用 httpx )和解析器。负责请求目标页面(画师主页、标签页、作品详情页),并解析出两种数据:一是需要进一步爬取的新任务(如作品详情页链接),二是最终的目标数据(作品元数据和图片URL)。
  3. 数据管道(Item Pipeline) : 负责处理爬取器提取出的结构化数据。主要做两件事:一是将元数据清洗后存入数据库,二是将图片URL放入下载队列。
  4. 下载器(Downloader) : 一个高性能的异步下载模块。从下载队列中消费图片URL,并下载到本地文件系统,同时更新数据库中该作品的本地存储路径。
  5. 配置与状态管理(Config & State) : 管理Cookie、请求头、数据库连接、代理设置(如果需要)、爬取速率限制等全局配置和运行状态。

4.2 核心工作流程

  1. 初始化 : 读取配置,初始化数据库,建立请求客户端(携带Cookie和Headers)。
  2. 种子注入 : 用户提供初始画师ID或标签,调度器将其作为初始任务放入队列。
  3. 页面爬取与解析 :
    • 爬取器从队列取一个任务(如画师主页URL)。
    • 下载页面HTML。
    • 关键步骤 : 解析页面。优先尝试从 <script> 标签中提取JSON格式的预加载数据。如果失败,再降级使用CSS选择器/XPath解析HTML。
    • 从解析出的数据中,提取作品列表。对于每个作品,生成两个子任务:一是作品详情页任务(用于获取元数据和原图URL),二是可能的新画师任务(如果开启了发现模式)。
  4. 元数据提取与存储 :
    • 爬取器请求作品详情页,同样优先解析内嵌的JSON数据。
    • 提取标题、画师、标签、尺寸、统计信息等,并 最重要的是提取原始图片的URL 。Pixiv 的图片URL通常有规律,如 i.pximg.net/img-original/img/2024/05/15/00/00/00/12345678_p0.jpg 。
    • 数据管道将这批元数据组装成一个“Item”,存入数据库(标记为“已发现,未下载”)。
  5. 图片下载 :
    • 数据管道将图片URL放入下载队列。
    • 下载器异步并发地从队列中取URL, 在请求头中务必加上正确的Referer ,然后将图片二进制流保存到本地。文件命名建议包含作品ID和页码(如 12345678_p0.jpg ),并建立合理的目录结构(如按画师ID/日期分类)。
    • 下载完成后,更新数据库中该条记录的 local_path 字段。
  6. 循环与终止 : 重复步骤3-5,直到调度器中的任务队列为空,且所有下载任务完成。可以设置爬取深度、最大作品数等条件来终止爬虫。

5. 关键代码实现与难点突破

让我们深入到几个具体的技术难点,看看代码如何实现。

5.1 登录态维持与Cookie管理

我们不能每次请求都手动复制Cookie。一个常见的做法是将Cookie保存到文件。

import json
import httpx
from pathlib import Path

COOKIE_FILE = Path('./config/cookies.json')

def load_cookies():
    """从文件加载cookies"""
    if COOKIE_FILE.exists():
        with open(COOKIE_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    return {}

def save_cookies(cookies):
    """保存cookies到文件"""
    COOKIE_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(COOKIE_FILE, 'w', encoding='utf-8') as f:
        json.dump(cookies, f, ensure_ascii=False, indent=2)

def create_client():
    """创建携带cookies的httpx客户端"""
    cookies = load_cookies()
    # 为Pixiv设置特定的cookies,通常需要`PHPSESSID`和`device_token`
    client = httpx.AsyncClient(
        headers=BASE_HEADERS,
        cookies=cookies,
        timeout=httpx.Timeout(30.0),
        follow_redirects=True,
        http2=True, # 启用HTTP/2
    )
    return client

# 使用示例
async def test_login(client):
    """测试cookies是否有效,访问个人页面"""
    try:
        resp = await client.get('https://www.pixiv.net/ajax/user/self')
        if resp.status_code == 200 and resp.json().get('error') is False:
            print("登录态有效")
            return True
        else:
            print("登录态可能已失效")
            # 这里可以触发重新登录流程(需要模拟登录,难度极高,通常建议手动更新cookie)
            return False
    except Exception as e:
        print(f"网络错误: {e}")
        return False

5.2 解析作品详情页并获取原图URL

这是爬虫的数据提取核心。我们利用内嵌的JSON数据。

import re
import json

async def fetch_illust_detail(client, illust_id):
    """根据作品ID获取详情和原图URL"""
    url = f'https://www.pixiv.net/artworks/{illust_id}'
    headers = {**BASE_HEADERS, 'Referer': 'https://www.pixiv.net/'}
    
    try:
        resp = await client.get(url, headers=headers)
        resp.raise_for_status()
        html = resp.text
        
        # 方法1: 从预加载的JSON数据中提取(最可靠)
        pattern = r'\"illust\":(\{.*?\}),\"user\"'
        # 更健壮的模式,匹配整个`preload`数据
        pattern_full = r'window\.__INITIAL_STATE__\s*=\s*(\{.*?\})\s*;'
        match = re.search(pattern_full, html, re.DOTALL)
        
        if match:
            try:
                data = json.loads(match.group(1))
                # 导航到作品数据,路径可能类似: data.illust.illustDetail.illustId
                # 实际路径需要根据页面结构分析,这里是一个示例
                illust_data = data.get('illust', {}).get(illust_id, {})
                if not illust_data:
                    # 尝试其他路径
                    illust_data = data.get('illustDetail', {}).get('illust', {})
                
                if illust_data:
                    title = illust_data.get('title')
                    user_id = illust_data.get('userId')
                    user_name = illust_data.get('userName')
                    tags = [tag['tag'] for tag in illust_data.get('tags', {}).get('tags', [])]
                    page_count = illust_data.get('pageCount', 1)
                    
                    # 构建原图URL!这是关键
                    urls = []
                    for page in range(page_count):
                        # 单页作品
                        if page_count == 1:
                            original_url = illust_data.get('urls', {}).get('original')
                        # 多页作品(漫画)
                        else:
                            original_url = illust_data.get('urls', {}).get('original')
                            # 多页时,original_url可能是一个模板,需要替换页码
                            if original_url and '_p0' in original_url:
                                original_url = original_url.replace('_p0', f'_p{page}')
                            # 或者从`body`中的`pages`列表获取
                            pages = illust_data.get('body', {}).get('pages', [])
                            if pages and page < len(pages):
                                original_url = pages[page].get('urls', {}).get('original')
                        
                        if original_url:
                            urls.append(original_url)
                    
                    return {
                        'id': illust_id,
                        'title': title,
                        'user_id': user_id,
                        'user_name': user_name,
                        'tags': tags,
                        'page_count': page_count,
                        'image_urls': urls,
                        # ... 其他元数据
                    }
            except json.JSONDecodeError as e:
                print(f"解析JSON失败: {e}")
        
        # 方法2: 降级到HTML解析(备用方案)
        # 使用parsel或BeautifulSoup解析HTML,但信息可能不全,且原图URL可能被隐藏或需要额外请求API
        # 这里省略...
        
        return None
        
    except httpx.RequestError as e:
        print(f"请求作品 {illust_id} 详情失败: {e}")
        return None

5.3 异步图片下载与存储

实现一个支持并发控制、错误重试的下载器。

import asyncio
import aiofiles
from pathlib import Path
import hashlib

class AsyncImageDownloader:
    def __init__(self, client, save_dir='./downloads', max_concurrent=5, retries=3):
        self.client = client
        self.save_dir = Path(save_dir)
        self.semaphore = asyncio.Semaphore(max_concurrent) # 控制并发数
        self.retries = retries
        self.save_dir.mkdir(parents=True, exist_ok=True)
    
    async def download_one(self, image_url, referer, filename=None):
        """下载单张图片"""
        async with self.semaphore: # 限制并发
            for attempt in range(self.retries):
                try:
                    headers = {'Referer': referer} # Referer至关重要!
                    resp = await self.client.get(image_url, headers=headers, follow_redirects=True)
                    resp.raise_for_status()
                    
                    image_data = resp.content
                    # 简单校验文件类型
                    if not resp.content:
                        raise ValueError("下载内容为空")
                    
                    # 生成文件名
                    if not filename:
                        # 从URL提取或使用哈希
                        filename = image_url.split('/')[-1]
                        # 避免非法字符
                        filename = "".join(c for c in filename if c.isalnum() or c in '._-').rstrip()
                    
                    save_path = self.save_dir / filename
                    # 异步写入文件
                    async with aiofiles.open(save_path, 'wb') as f:
                        await f.write(image_data)
                    
                    print(f"下载成功: {filename} -> {save_path}")
                    return save_path
                    
                except (httpx.RequestError, IOError) as e:
                    wait = 2 ** attempt # 指数退避
                    print(f"下载失败 (尝试 {attempt+1}/{self.retries}): {image_url}, 错误: {e}, {wait}秒后重试")
                    await asyncio.sleep(wait)
            print(f"下载最终失败: {image_url}")
            return None
    
    async def download_many(self, download_tasks):
        """并发下载多个任务,每个任务是一个(url, referer, filename)元组"""
        tasks = [self.download_one(url, ref, name) for url, ref, name in download_tasks]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        # 处理结果
        successful = [r for r in results if isinstance(r, Path)]
        failed = [tasks[i] for i, r in enumerate(results) if isinstance(r, Exception)]
        return successful, failed

6. 反爬策略深度分析与应对方案

Pixiv 作为大型平台,拥有成熟的反爬机制。硬闯只会导致 IP 被封或账号受限。我们必须采取“友好”的策略。

6.1 主要反爬手段识别

  1. 请求头校验 : 缺失 User-Agent 、 Referer (尤其是图片请求)会被直接拒绝。
  2. 频率限制 : 短时间内发起过多请求,会触发速率限制,返回 429 Too Many Requests 或直接暂时封禁 IP。
  3. 行为模式检测 : 如果请求模式过于规律(如固定间隔毫秒级请求),可能被识别为机器人。
  4. Cookie 与 Session 验证 : 对于需要登录的页面,无效或过期的 Cookie 会导致返回错误或跳转登录页。
  5. JavaScript 挑战 : 某些操作(如查看多图)可能需要执行前端 JS 才能获取真实数据,不过 Pixiv 的核心数据目前大多仍在初始 HTML 的 JSON 中。

6.2 实战应对策略

  1. 完备的请求头 : 如前所述,模拟真实浏览器,特别是 Referer 。
  2. 请求速率控制 : 这是最重要的策略。不要在代码里用无间隔的循环发起请求。
    • 固定延迟 : 在每个请求之间加入 await asyncio.sleep(random.uniform(1, 3)) 。简单但有效。
    • 更智能的控制 : 使用令牌桶(Token Bucket)或漏桶(Leaky Bucket)算法。可以定义一个全局的速率限制器。
    import time
    class RateLimiter:
        def __init__(self, calls_per_second=0.5):
            self.calls_per_second = calls_per_second
            self.min_interval = 1.0 / calls_per_second
            self.last_call = 0
    
        async def acquire(self):
            elapsed = time.time() - self.last_call
            wait_for = max(0, self.min_interval - elapsed)
            if wait_for > 0:
                await asyncio.sleep(wait_for)
            self.last_call = time.time()
    
    # 在发起请求前调用
    limiter = RateLimiter(0.3) # 每秒不超过0.3个请求(约3秒一个)
    await limiter.acquire()
    response = await client.get(url)
    
  3. 随机化与人性化 : 在延迟时间中加入随机因子,模拟人类操作的不可预测性。可以随机切换 User-Agent 池(但注意 Cookie 可能绑定特定客户端)。
  4. 使用代理IP池 : 对于大规模爬取,这是必备方案。当某个 IP 被限制时,自动切换到下一个。可以使用付费代理服务或自建代理。在 httpx.AsyncClient 中通过 proxies 参数设置。

    注意 : 免费代理大多不稳定、速度慢,且可能存在安全风险。生产环境建议使用可靠的付费代理。

  5. 处理异常状态码 :
    • 429 Too Many Requests : 立即停止,等待更长的时间(如10分钟)再重试,并考虑降低请求频率或更换代理。
    • 403 Forbidden : 检查 Cookie 是否失效,Referer 是否正确,或 IP 是否被永久封禁。
    • 5xx 服务器错误 : 可能是 Pixiv 服务器问题,等待后重试。

6.3 道德与法律边界

  • 遵守 robots.txt : 查看 https://www.pixiv.net/robots.txt 。虽然它可能允许某些爬虫路径,但大规模爬取通常不被欢迎。
  • 尊重版权 : Pixiv 上的作品版权归创作者所有。爬取的数据 仅限个人学习、研究、欣赏之用 。绝对禁止用于任何商业用途、重新分发、或训练AI模型而未获授权。
  • 不要给服务器造成负担 : 控制速率,避免 DDoS 式的请求。你的爬虫行为不应该影响正常用户的访问体验。
  • 查看用户协议 : 明确 Pixiv 的服务条款中关于数据抓取的规定。

7. 项目优化与高级技巧

当基础功能实现后,可以考虑以下优化点来提升爬虫的健壮性、效率和可维护性。

7.1 断点续爬与状态持久化

爬虫可能因为网络问题、程序崩溃或主动中断而停止。重新开始不应该从头爬起。

  • 思路 : 将调度器中的任务队列、正在处理的任务、已完成的任务状态定期保存到磁盘(如 SQLite 或 JSON 文件)。
  • 实现 : 使用 atexit 注册退出函数,或在收到中断信号(如 SIGINT )时保存状态。启动时先加载状态,恢复队列。
  • 数据库去重 : 在存入作品数据时,使用 INSERT OR IGNORE 或 ON CONFLICT DO NOTHING 语句,避免重复插入。

7.2 更精细的错误处理与重试机制

为不同类型的错误设计不同的重试策略。

  • 网络错误 (超时、连接断开): 立即重试,最多3-5次,配合指数退避。
  • 服务器错误 (5xx): 等待一段时间后重试。
  • 客户端错误 (4xx,如403、404): 403可能需要更新Cookie,404说明资源不存在,记录日志并跳过,无需重试。
  • 速率限制 (429): 等待较长时间(如5-10分钟),并显著降低全局请求频率。

可以引入 tenacity 库来优雅地实现重试逻辑。

7.3 分布式爬虫雏形

如果数据量极大,单机爬取太慢,可以考虑分布式。

  • 中心化任务队列 : 使用 Redis 或 RabbitMQ 作为任务队列。多个爬虫节点从同一个队列消费任务。
  • 去重 : 使用 Redis 的 Set 或 Bloom Filter 实现全局去重。
  • 结果汇总 : 各个节点将爬取到的数据推送到一个中心数据库或文件存储。
  • 注意 : 分布式会引入复杂度,如节点通信、状态同步、负载均衡等,需根据实际需求权衡。

7.4 监控与日志

一个成熟的爬虫需要有清晰的运行日志和简单的监控。

  • 结构化日志 : 使用 logging 模块,配置不同的级别(INFO, WARNING, ERROR),并输出到文件和控制台。记录爬取进度、错误信息、请求状态等。
  • 进度可视化 : 可以定期打印进度条,或使用 tqdm 库。记录已爬取作品数、成功率、平均速度等。
  • 告警 : 对于连续错误、成功率骤降等情况,可以集成邮件或即时通讯工具告警(但这对于个人项目可能过重)。

8. 常见问题排查与实战心得

这里分享一些我实际爬取过程中踩过的坑和解决方案。

8.1 问题速查表

问题现象 可能原因 排查步骤与解决方案
请求返回403 Forbidden 1. Referer 请求头缺失或错误。
2. Cookie 失效或未设置。
3. IP 被目标网站封禁。
1. 检查请求头,确保图片请求的 Referer 是 https://www.pixiv.net/ 。
2. 访问个人中心页面测试 Cookie 是否有效。
3. 尝试更换网络环境或使用代理。
图片下载下来是0字节或损坏文件 1. 请求图片时未通过反盗链(Referer)。
2. 服务器返回了错误页面(如验证码)。
1. 确保图片下载请求的 headers 中包含 Referer 。
2. 打印响应状态码和内容类型 ( Content-Type ),确认是否是 image/jpeg 等。
解析不到作品数据,JSON提取失败 1. 页面结构已更新。
2. 正则表达式模式不匹配。
3. 需要登录才能查看(如R-18作品)。
1. 手动打开目标页面,检查源代码,搜索 illust 或 作品ID ,找到新的数据存放模式。
2. 使用更宽泛的正则或直接使用 json.loads 解析整个 <script> 内容后遍历查找。
3. 确认使用的 Cookie 有效且账号有查看权限。
爬虫运行缓慢 1. 网络延迟。
2. 没有使用异步并发。
3. 请求频率过高触发限流,导致等待或重试。
1. 使用异步 ( asyncio + httpx.AsyncClient )。
2. 适当增加并发数(如 Semaphore(10) )。
3. 必须添加请求间隔 ,建议主请求间隔2-5秒,图片下载可稍快但也要控制。
数据库插入重复记录 程序中断后重启,从上次的页面重新解析,导致重复任务。 1. 实现任务去重队列(内存Set或Redis)。
2. 数据库表设置唯一索引(如 illust_id ),使用 INSERT OR IGNORE 。
遇到验证码 请求行为被识别为异常,触发人机验证。 1. 立即大幅降低请求频率 ,模拟更人类化的操作(随机延迟、滚动页面等)。
2. 更换IP地址(使用代理)。
3. 考虑暂停爬虫一段时间(数小时或一天)。

8.2 实操心得与建议

  1. 从小范围测试开始 : 不要一开始就设定爬取几万张图的目标。先针对一个画师或一个标签,跑通整个流程:登录、解析页面、拿到元数据、下载一张图片。确保每个环节都无误。
  2. 勤于查看网络请求 : 浏览器开发者工具的“网络”(Network)选项卡是你最好的老师。观察页面加载时发了哪些请求,哪个请求返回了你需要的数据(通常是XHR或Fetch请求),直接模仿这个请求往往事半功倍。
  3. 数据比图片更重要 : 元数据(标签、作者、时间)的结构化存储,为后续的分析、筛选、搜索提供了可能。设计一个好的数据库 schema 会让你后期省很多事。
  4. 尊重与克制 : 设置合理的速率限制(比如每秒不超过0.2个请求到主站)。尽量在网站流量较低的时段(例如当地时间的深夜)运行爬虫。你的目标是获取数据,而不是攻击网站。
  5. 代码要模块化 : 将网络请求、解析、存储、下载分别写成独立的函数或类。这样不仅代码清晰,而且当某个环节需要修改(比如解析规则变了)时,影响范围最小。
  6. 做好异常处理 : 网络爬虫运行在复杂的环境中,异常是常态。你的代码必须能妥善处理网络超时、连接重置、数据格式意外变化等情况,并记录日志,而不是直接崩溃。
  7. 版本化管理与配置分离 : 使用 Git 管理代码。将 Cookie、数据库路径、下载目录等配置信息放在单独的配置文件(如 config.yaml 或 .env 文件)中,不要硬编码在脚本里,并通过 .gitignore 忽略它们。

构建一个完整的 Pixiv 爬虫是一个系统工程,它考验的不仅是 Python 语法,更是对 HTTP 协议、前端知识、反爬策略、异步编程、数据存储和工程化思维的全面理解。当你最终看到爬虫稳定运行,将喜爱的作品有序地保存到本地时,那种成就感是无可替代的。希望这份详细的指南能为你扫清障碍,祝你爬虫愉快!记住,能力越大,责任越大,请务必在法律和道德的框架内合理使用技术。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐