1. 从零开始:为什么选择爬取Pixiv,以及你需要知道的前提

如果你对二次元文化、插画艺术或者仅仅是寻找高质量的壁纸感兴趣,那么Pixiv这个日本最大的插画交流社区,绝对是一个绕不开的宝藏。上面汇聚了全球顶尖的画师和无数令人惊叹的作品。然而,Pixiv官方并没有提供一个非常便捷的批量下载工具,尤其是当你想要收藏某个画师的全部作品,或者根据特定标签(Tag)整理图片时,手动一张张保存就成了一场噩梦。这时候,用Python写一个爬虫,就成了一个自然而然的选择。

但请注意,爬取Pixiv并非简单的“请求-下载”过程。它涉及到复杂的登录验证、反爬机制、以及最重要的——对创作者版权的绝对尊重。在开始之前,我们必须明确几个核心原则:第一,爬取行为应仅限于个人学习、研究和欣赏,严禁用于任何商业用途或未经授权的二次分发。第二,必须严格遵守Pixiv的 robots.txt 协议,控制请求频率,避免对服务器造成过大压力。第三,在代码中应尽量模拟人类浏览行为,并妥善处理可能遇到的各种异常。今天,我就以一个过来人的身份,带你从环境搭建到完整爬虫实现,一步步拆解这个过程中的技术要点、踩过的坑以及一些实用的技巧。

2. 核心工具链选型:为什么是 requests BeautifulSoup selenium 的组合?

面对一个动态内容丰富的现代网站,工具的选择直接决定了爬虫的复杂度、稳定性和可维护性。对于Pixiv,我强烈推荐 requests + BeautifulSoup + selenium 的组合拳,而不是单一依赖某个库。下面我详细解释一下为什么这么选,以及它们各自扮演的角色。

2.1 requests :高效稳定的基础通信模块

requests 库是Python HTTP客户端库的事实标准,它语法简洁,功能强大。我们将用它来处理最核心的HTTP请求,比如获取登录后的页面、提交搜索表单、下载图片二进制数据等。它的高效和易用性是其他库难以比拟的。在爬虫中,我们会用它来维持一个会话(Session),这个会话对象可以自动管理Cookies,这对于需要登录的Pixiv至关重要,避免了手动处理Cookie的繁琐。

2.2 BeautifulSoup :静态HTML解析的利器

Pixiv的很多页面,特别是作品详情页的初始HTML中,其实已经包含了作品ID、标题、作者、描述以及 多张图片的原始链接 等关键信息。这些信息通常被嵌入在 <meta> 标签或特定的 <script> 脚本的JSON数据中。 BeautifulSoup 的作用就是像一把手术刀,精准地从这些静态HTML中提取出我们需要的结构化数据。它比正则表达式更易读、更稳定,尤其是在处理嵌套复杂的HTML标签时优势明显。

2.3 selenium :应对动态渲染与复杂登录的“核武器”

这是整个方案中最关键也最重量级的一环。Pixiv的登录页面以及部分页面交互(如翻页)可能采用了JavaScript动态加载或验证。单纯用 requests 模拟登录流程极其复杂,需要逆向分析登录接口的加密参数。而 selenium 可以驱动一个真实的浏览器(如Chrome),完全模拟用户的操作:打开网页、输入账号密码、点击登录按钮。登录成功后,浏览器中会生成包含登录状态的Cookies,我们可以将这些Cookies提取出来,注入到 requests 的Session中。这样,后续的所有请求都可以用轻量级的 requests 来执行,既绕过了复杂的登录逆向,又保证了后续请求的高效率。简而言之, selenium 负责“攻破”登录堡垒, requests 负责后续的“扫荡”工作。

环境准备实操: 首先,你需要安装这些库。建议使用虚拟环境。

pip install requests beautifulsoup4 selenium

其次,你需要下载与你的Chrome浏览器版本匹配的 ChromeDriver ,并将其所在目录添加到系统PATH,或者直接在代码中指定路径。 selenium 需要通过它来控制Chrome。

3. 攻克第一道关卡:模拟登录并获取有效会话

这是整个爬虫能否成功运行的第一步,也是最容易出错的一步。我们的目标是获得一个携带了有效登录凭证(Cookies)的 requests.Session 对象。

3.1 使用Selenium自动化登录流程

我们利用Selenium打开登录页面,自动填充账号密码并点击登录。这里有一个至关重要的细节:Pixiv的登录表单可能会随页面加载动态生成,直接通过ID或Name定位元素可能失败。更稳健的做法是使用XPath或CSS选择器,并显式等待元素加载完成。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

def login_with_selenium(username, password):
    """
    使用Selenium模拟登录Pixiv,并返回获取到的Cookies字典。
    """
    # 初始化Chrome驱动,可配置无头模式(不显示浏览器界面)
    options = webdriver.ChromeOptions()
    # options.add_argument('--headless') # 生产环境可开启,调试时建议关闭
    options.add_argument('--disable-gpu')
    options.add_argument('--no-sandbox')
    # 防止被检测为自动化工具,可添加一些参数
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option('useAutomationExtension', False)

    driver = webdriver.Chrome(options=options)
    driver.get('https://accounts.pixiv.net/login?return_to=https://www.pixiv.net/')

    try:
        # 等待用户名输入框出现
        username_input = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.XPATH, "//input[@autocomplete='username']"))
        )
        username_input.send_keys(username)

        # 等待密码输入框出现
        password_input = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.XPATH, "//input[@autocomplete='current-password']"))
        )
        password_input.send_keys(password)

        # 定位并点击登录按钮
        login_button = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.XPATH, "//button[@type='submit']"))
        )
        login_button.click()

        # 等待登录成功,通常可以等待跳转到首页或某个特定元素出现
        WebDriverWait(driver, 15).until(
            EC.url_contains('www.pixiv.net')
        )
        print("登录成功!")

        # 关键步骤:获取浏览器中的所有Cookies
        cookies = driver.get_cookies()
        # 将Selenium格式的Cookies转换为requests库可用的字典格式
        cookies_dict = {cookie['name']: cookie['value'] for cookie in cookies}

        return cookies_dict

    except Exception as e:
        print(f"登录过程中出现错误: {e}")
        return None
    finally:
        # 获取Cookies后即可关闭浏览器,节省资源
        driver.quit()

# 使用你的Pixiv账号密码
my_cookies = login_with_selenium('your_username', 'your_password')

注意: 将你的真实账号密码硬编码在代码中是极不安全的。在实际项目中,你应该通过环境变量、配置文件或运行时输入的方式来管理这些敏感信息。

3.2 将Cookies注入Requests Session

拿到 cookies_dict 后,我们创建一个 requests.Session ,并将Cookies更新进去。这个Session对象在后续所有请求中都会自动携带这些登录凭证。

import requests

def create_authenticated_session(cookies_dict):
    """
    使用从Selenium获取的Cookies创建一个已认证的requests Session。
    """
    session = requests.Session()
    # 更新session的cookies
    session.cookies.update(cookies_dict)
    # 设置一个合理的User-Agent,模拟真实浏览器
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
        'Referer': 'https://www.pixiv.net/', # 设置Referer,很多图片服务器会校验
    })
    return session

# 创建已登录的会话
if my_cookies:
    authed_session = create_authenticated_session(my_cookies)
    # 测试一下会话是否有效
    test_response = authed_session.get('https://www.pixiv.net/')
    if test_response.status_code == 200 and '您的用户名' in test_response.text: # 检查页面中是否包含登录后的用户信息
        print("Session创建成功,已处于登录状态。")
    else:
        print("Session可能未正确登录,请检查。")

至此,我们拥有了一个可以通行Pixiv大部分页面的“通行证”。接下来的数据抓取都将基于这个 authed_session 进行。

4. 核心数据抓取策略:从作品列表到图片原链

Pixiv上图片的抓取,通常有两种思路:一是抓取特定画师(用户)的所有作品;二是根据搜索关键词(标签)来抓取相关作品。无论哪种,流程都类似:获取作品列表页 -> 解析出每个作品的ID -> 进入作品详情页 -> 解析出图片的真实原始地址。

4.1 解析作品列表页并提取作品ID

以抓取画师作品为例,画师的主页URL格式通常为: https://www.pixiv.net/users/{user_id} 。我们需要从这个页面中找到“作品”列表,并提取出每个作品的链接或ID。

这里有一个 非常重要的技巧 :Pixiv的前端页面大量使用JavaScript渲染,直接请求HTML得到的内容可能不完整。但是,Pixiv实际上提供了一个用于预渲染的初始状态数据,它被放在一个 <script id="meta-preload-data" type="application/json"> 标签中。这个JSON数据包含了页面初始化时所需的所有信息,包括作品列表、用户信息等。这是我们获取数据最可靠、最便捷的途径。

import json
from bs4 import BeautifulSoup

def get_artist_illust_ids(session, user_id, limit=20):
    """
    获取指定画师的作品ID列表。
    :param session: 已认证的requests Session
    :param user_id: 画师的用户ID
    :param limit: 想要获取的作品数量限制
    :return: 作品ID列表
    """
    url = f'https://www.pixiv.net/users/{user_id}'
    try:
        resp = session.get(url)
        resp.raise_for_status() # 检查请求是否成功
        soup = BeautifulSoup(resp.text, 'html.parser')

        # 查找包含预加载数据的script标签
        meta_data_script = soup.find('script', id='meta-preload-data')
        if not meta_data_script:
            print("未找到预加载数据,页面结构可能已变化。")
            return []

        # 提取JSON字符串并解析
        meta_data = json.loads(meta_data_script.string)

        # 这个JSON结构非常庞大,我们需要找到用户作品相关的部分
        # 通常路径是:['illust'][‘作品ID’] 或 ['user'][user_id]['illusts']
        # 需要实际打印出来分析结构。一个常见的结构是:
        illust_ids = []
        # 遍历meta_data,寻找包含作品信息的部分
        # 这里提供一个常见的解析逻辑,但Pixiv可能会调整结构
        for key, value in meta_data.items():
            if 'illust' in key or 'artworks' in key:
                # 假设value是一个字典,键是作品ID
                for illust_id in value.keys():
                    if illust_id.isdigit(): # 确保是数字ID
                        illust_ids.append(illust_id)
                break # 找到第一个包含illust的键就跳出

        # 如果上述方法不行,可以尝试更通用的方法:直接搜索所有包含作品ID的链接
        if not illust_ids:
            all_links = soup.find_all('a', href=True)
            for link in all_links:
                href = link['href']
                # 匹配作品详情页的URL模式,例如 /artworks/123456789
                if '/artworks/' in href:
                    illust_id = href.split('/artworks/')[-1].split('?')[0]
                    if illust_id.isdigit() and illust_id not in illust_ids:
                        illust_ids.append(illust_id)

        # 去重并限制数量
        illust_ids = list(set(illust_ids))[:limit]
        print(f"成功获取到 {len(illust_ids)} 个作品ID。")
        return illust_ids

    except requests.exceptions.RequestException as e:
        print(f"请求画师页面失败: {e}")
        return []
    except json.JSONDecodeError as e:
        print(f"解析JSON数据失败: {e}")
        return []

实操心得: Pixiv的页面结构,特别是 meta-preload-data 的内部路径,可能会随着前端改版而变化。最可靠的方法是,在成功登录后,手动打开画师页面,使用浏览器的开发者工具(F12)查看 Elements 面板,搜索 meta-preload-data ,然后仔细研究其JSON结构。将这段JSON复制到在线JSON格式化工具中分析,找到存放作品ID的确切路径。这是爬虫开发中必不可少的“侦查”环节。

4.2 从作品详情页提取高质量图片原图链接

获取到作品ID(例如 123456789 )后,我们就可以构造作品详情页的URL: https://www.pixiv.net/artworks/{illust_id} 。同样,这个页面也包含了 meta-preload-data ,里面就有我们梦寐以求的原始图片链接。

Pixiv的作品可能有多种形式:单图、多图(漫画)、动图(Ugoira)。单图和多图的静态图片链接通常可以在 illust 对象下的 urls 字段中找到,其中 original 字段对应最高质量的原始图片。

def get_illust_image_urls(session, illust_id):
    """
    根据作品ID,获取该作品所有图片的原始URL。
    :param session: 已认证的requests Session
    :param illust_id: 作品ID
    :return: 图片原始URL列表
    """
    url = f'https://www.pixiv.net/artworks/{illust_id}'
    image_urls = []
    try:
        resp = session.get(url)
        resp.raise_for_status()
        soup = BeautifulSoup(resp.text, 'html.parser')

        meta_data_script = soup.find('script', id='meta-preload-data')
        if not meta_data_script:
            print(f"作品 {illust_id} 页面未找到预加载数据。")
            return image_urls

        meta_data = json.loads(meta_data_script.string)

        # 关键:定位到当前作品的数据
        # 通常路径是:['illust'][illust_id]
        illust_data = meta_data.get('illust', {}).get(illust_id)
        if not illust_data:
            # 有时可能在更深的层级,尝试通用查找
            for key, value in meta_data.items():
                if isinstance(value, dict) and illust_id in value:
                    illust_data = value[illust_id]
                    break

        if not illust_data:
            print(f"在预加载数据中未找到作品 {illust_id} 的信息。")
            return image_urls

        # 提取图片信息
        # 单幅作品
        if illust_data.get('illustType') == 0:
            urls = illust_data.get('urls')
            if urls:
                original_url = urls.get('original')
                if original_url:
                    image_urls.append(original_url)
        # 多幅作品(漫画)
        elif illust_data.get('illustType') == 1:
            pages = illust_data.get('pages', [])
            for page in pages:
                original_url = page.get('urls', {}).get('original')
                if original_url:
                    image_urls.append(original_url)
        # 动图(Ugoira),这里只获取封面,下载动图需要额外处理zip
        elif illust_data.get('illustType') == 2:
            urls = illust_data.get('urls')
            if urls:
                original_url = urls.get('original') # 动图封面
                if original_url:
                    image_urls.append(original_url)
            print(f"作品 {illust_id} 是动图(Ugoira),本脚本仅获取封面。")

        print(f"作品 {illust_id} 解析出 {len(image_urls)} 张图片。")
        return image_urls

    except Exception as e:
        print(f"解析作品 {illust_id} 图片链接时出错: {e}")
        return image_urls

关于图片链接的格式: 原始图片链接通常类似于 https://i.pximg.net/img-original/img/2024/05/15/00/00/00/123456789_p0.jpg 。注意域名 i.pximg.net ,Pixiv的图片服务器会对请求头进行严格检查,特别是 Referer 必须设置为 https://www.pixiv.net/ ,否则会返回403错误。这就是为什么我们在创建Session时提前设置了 Referer

5. 图片下载与本地存储:稳健性与效率的平衡

拿到图片的原始URL列表后,下载本身是简单的,但需要考虑网络异常、文件存储命名、避免重复下载等问题。

5.1 实现一个健壮的图片下载函数

import os
from urllib.parse import urlparse

def download_image(session, image_url, save_dir='./pixiv_images', referer='https://www.pixiv.net/'):
    """
    下载单张图片到本地。
    :param session: 已认证的requests Session
    :param image_url: 图片的原始URL
    :param save_dir: 保存目录
    :param referer: 请求头中的Referer,必须为Pixiv域名
    """
    # 创建保存目录
    os.makedirs(save_dir, exist_ok=True)

    # 从URL中提取文件名
    parsed_url = urlparse(image_url)
    filename = os.path.basename(parsed_url.path) # 例如 `123456789_p0.jpg`

    # 构建完整的保存路径
    save_path = os.path.join(save_dir, filename)

    # 如果文件已存在,跳过下载(可根据需要修改为覆盖或重命名)
    if os.path.exists(save_path):
        print(f"文件 {filename} 已存在,跳过。")
        return False

    try:
        # 下载图片,必须设置Referer
        headers = {'Referer': referer}
        # 使用stream模式下载大文件
        with session.get(image_url, headers=headers, stream=True, timeout=30) as response:
            response.raise_for_status()
            # 检查Content-Type是否是图片
            content_type = response.headers.get('content-type', '')
            if 'image' not in content_type:
                print(f"警告:{image_url} 返回的内容类型不是图片: {content_type}")
                return False

            # 写入文件
            with open(save_path, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)
        print(f"成功下载: {filename}")
        return True
    except requests.exceptions.Timeout:
        print(f"下载超时: {image_url}")
        return False
    except requests.exceptions.RequestException as e:
        print(f"下载失败 {image_url}: {e}")
        return False
    except IOError as e:
        print(f"文件写入失败 {save_path}: {e}")
        return False

5.2 组织主流程与添加延迟控制

我们不能一次性发起大量请求,这会被服务器识别为攻击并封禁IP。必须在请求之间加入随机延迟。

import time
import random

def crawl_artist_illustrations(session, user_id, max_illusts=30, download_delay=(1, 3)):
    """
    爬取指定画师的作品并下载图片。
    :param session: 已认证的会话
    :param user_id: 画师ID
    :param max_illusts: 最大爬取作品数
    :param download_delay: 下载每张图片后的延迟范围(秒)
    """
    print(f"开始爬取画师 {user_id} 的作品...")
    # 1. 获取作品ID列表
    illust_ids = get_artist_illust_ids(session, user_id, limit=max_illusts)

    if not illust_ids:
        print("未获取到任何作品ID,爬取终止。")
        return

    total_downloaded = 0
    for idx, illust_id in enumerate(illust_ids):
        print(f"\n处理作品 ({idx+1}/{len(illust_ids)}): ID={illust_id}")
        # 2. 获取该作品的所有图片URL
        image_urls = get_illust_image_urls(session, illust_id)

        if not image_urls:
            print(f"  作品 {illust_id} 未解析到图片链接,跳过。")
            continue

        # 3. 下载该作品的每一张图片
        for img_url in image_urls:
            success = download_image(session, img_url, save_dir=f'./pixiv_images/artist_{user_id}')
            if success:
                total_downloaded += 1
            # 在每张图片下载后添加随机延迟,模拟人类操作
            time.sleep(random.uniform(download_delay[0], download_delay[1]))

    print(f"\n爬取完成!画师 {user_id} 共成功下载 {total_downloaded} 张图片。")

# 使用示例
if __name__ == '__main__':
    # 假设已经通过前面的步骤获得了 authed_session
    # 替换成你想爬取的画师ID
    TARGET_ARTIST_ID = '1234567' # 示例ID,需替换
    crawl_artist_illustrations(authed_session, TARGET_ARTIST_ID, max_illusts=20, download_delay=(1.5, 4))

6. 高级话题与避坑指南:让爬虫更稳健、更高效

在实际操作中,你会遇到各种各样的问题。下面分享几个我踩过坑后总结出的关键点。

6.1 会话(Session)过期与自动重登录

通过Selenium获取的Cookies是有生命周期的,可能几小时或几天后就会失效。一个健壮的爬虫需要能检测登录状态,并在失效时自动重新登录。

实现思路: 在每次发起关键请求(如获取作品列表)前,可以先访问一个需要登录才能查看的页面(如个人设置页 https://www.pixiv.net/setting_user.php ),检查返回的页面内容或状态码。如果发现未登录(例如被重定向到登录页,或页面中不包含用户信息),则触发重新登录流程,更新Session的Cookies。

def check_login_status(session):
    """检查当前session是否仍处于登录状态"""
    test_url = 'https://www.pixiv.net/setting_user.php'
    resp = session.get(test_url, allow_redirects=False) # 禁止重定向,方便判断
    # 如果未登录,访问此页面通常会返回302重定向到登录页
    if resp.status_code == 302 or 'login' in resp.headers.get('Location', ''):
        return False
    # 或者检查页面内容是否包含登录后的元素
    if resp.status_code == 200 and '您的用户名' in resp.text:
        return True
    return False

# 在主循环中集成状态检查
def robust_crawl(session, user_id, username, password):
    if not check_login_status(session):
        print("检测到登录状态失效,尝试重新登录...")
        new_cookies = login_with_selenium(username, password)
        if new_cookies:
            session.cookies.clear()
            session.cookies.update(new_cookies)
            print("重新登录成功!")
        else:
            print("重新登录失败,程序退出。")
            return
    # 继续执行爬取任务...
    crawl_artist_illustrations(session, user_id)

6.2 应对IP封锁与请求频率控制

即使设置了延迟,长时间、大批量的爬取仍然可能触发Pixiv的风控机制,导致IP被暂时封锁。除了将 download_delay 设置得更大(例如3-10秒),还可以考虑以下策略:

  1. 使用代理IP池 :这是应对IP封锁最有效的方法。你可以订阅一些代理IP服务,然后在 requests.get session.get 中通过 proxies 参数轮换使用不同的IP。
    proxies = {
        'http': 'http://your_proxy_ip:port',
        'https': 'http://your_proxy_ip:port',
    }
    response = session.get(url, proxies=proxies, timeout=10)
    
  2. 伪装请求头 :除了 User-Agent Referer ,还可以随机添加一些其他常见的请求头,如 Accept-Language , Accept-Encoding 等,让请求看起来更“自然”。
  3. 分散爬取任务 :不要一次性爬取一个画师的所有作品(可能成百上千)。可以分多次进行,每次爬取一部分,中间间隔数小时甚至一天。

6.3 数据去重与增量爬取

为了避免重复下载,我们在 download_image 函数中做了简单的文件存在性检查。但在实际项目中,你可能需要更精细的管理,比如维护一个本地数据库(如SQLite),记录已下载的作品ID、图片URL、下载时间、画师ID等元数据。每次爬取前先查询数据库,只下载新的作品。这不仅能节省时间和流量,也能在爬虫中断后从中断点恢复。

6.4 错误处理与日志记录

一个工业级的爬虫必须有完善的错误处理和日志记录。我们上面的代码使用了 try...except 来捕获异常,但还不够。建议使用Python的 logging 模块,将不同级别的信息(DEBUG, INFO, WARNING, ERROR)输出到文件和控制台。对于网络请求错误、解析错误、文件IO错误等,都应该被捕获并记录,而不是让程序直接崩溃。同时,可以为每个失败的任务设置重试机制(例如,重试3次)。

7. 伦理、法律与最佳实践:做一个负责任的爬虫使用者

最后,也是最重要的一部分,我们必须再次强调爬虫使用的边界。

  1. 尊重 robots.txt :访问 https://www.pixiv.net/robots.txt 。虽然Pixiv的 robots.txt 可能没有明确禁止所有爬虫,但我们必须遵守其中规定的爬取延迟(Crawl-delay),并避免爬取明确禁止的路径。
  2. 控制爬取速度 :这是对网站服务器的基本尊重。将请求间隔设置得足够长,我的经验是,在非高峰时段,单线程爬虫的间隔最好不低于3-5秒。绝对避免开启多线程疯狂请求。
  3. 仅用于个人用途 :你爬取的数据(图片)版权归原作者所有。严禁用于任何商业目的,包括但不限于售卖、未经授权的二次创作、训练AI模型等。最好在下载的图片文件中保留原作品的ID、标题等信息。
  4. 关注画师的设置 :有些画师会在个人简介或作品描述中注明“禁止转载”或“禁止保存”。虽然技术上仍可爬取,但从道德层面,应尊重画师的意愿。
  5. 不要给服务器带来负担 :如果你的爬虫导致目标网站响应变慢或影响其他用户,请立即停止。

编写爬虫是一项强大的技能,但能力越大,责任越大。通过今天分享的这套从登录、解析到下载的完整方案,以及其中融入的诸多细节和避坑经验,希望你能不仅实现Pixiv图片的自动化收藏,更能理解其背后的技术逻辑和伦理考量,安全、合规、高效地运用这项技术。在实际操作中,最考验人的往往不是代码本身,而是对目标网站反爬策略的耐心分析和应对,以及对可能遇到的各种边界情况的周全处理。多测试,多观察,你的爬虫会越来越稳健。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐