1. 项目概述:为什么Pixiv爬虫是个“技术活”?
如果你是个画师,或者是个二次元爱好者,那你对Pixiv(俗称P站)肯定不陌生。这个全球最大的插画交流网站,简直就是个视觉宝库,每天都有海量的高质量作品更新。但问题来了,网站本身的功能,比如收藏夹、搜索,有时候并不能完全满足我们“囤积”美好事物的需求。你想批量下载某个画师的所有作品?想按特定标签、收藏数、日期范围来归档图片?或者想为自己的AI绘画模型收集特定风格的训练集?这时候,一个自己写的Pixiv爬虫就成了刚需。
但Pixiv爬虫,远不是简单的requests.get()加正则表达式就能搞定的。它涉及到复杂的登录验证、反爬虫机制(比如著名的PHPSESSID和device_token)、动态加载的API接口,以及图片资源本身的多种规格和防盗链策略。这不像爬一个静态新闻网站,更像是在和网站的安全工程师“斗智斗勇”。网上那些“一行代码爬全网”的教程,在Pixiv面前基本都会碰壁。所以,这个项目不只是写个脚本,更是一次对现代Web反爬策略的实战演练,能让你深刻理解会话管理、请求模拟和数据处理。
接下来,我会以一个完整的、可运行的爬虫项目为蓝本,拆解从环境准备、登录破解、数据抓取到本地存储的每一个环节。我会重点解释“为什么”要这么做,并分享我在实际开发中踩过的坑和总结的技巧。无论你是Python爬虫新手想挑战高难度副本,还是有一定经验想完善自己的工具库,这篇内容都能给你提供一条清晰的路径。
2. 核心思路与架构设计:模拟真人,而非攻击
在动手写代码之前,我们必须明确一个核心原则:一个稳健的爬虫应该尽可能地模拟真实用户浏览器的行为,而不是试图用高频请求去“攻击”服务器。对于Pixiv这种级别的网站,粗暴的爬取不仅容易被封IP和账号,也违背了基本的网络礼仪。我们的设计思路是“低调、精准、尊重”。
2.1 技术栈选型与理由
为什么是这些工具?我们来逐一分析:
- Python + Requests + BeautifulSoup4 (bs4):这是爬虫的黄金基础组合。Python语法简洁,生态丰富。
Requests库处理HTTP请求简单高效,比原生urllib友好太多。BeautifulSoup用于解析HTML,虽然Pixiv主要数据来自API,但登录页面和部分元信息仍需HTML解析。 - Requests-HTML (可选但推荐):这个库是
Requests作者开发的,它内置了一个简易的浏览器引擎,可以执行JavaScript。虽然Pixiv的核心内容不依赖JS渲染,但有些页面元素或早期的反爬检查可能会用到。用它作为Requests的补充或替代,能应对更复杂的情况。 - 浏览器开发者工具 (Chrome DevTools):这是最重要的“非代码”工具。我们需要用它来:
- Network (网络) 面板:监听页面加载过程中的所有HTTP请求,找到真正返回图片和数据(通常是JSON格式)的API接口。这是爬虫的“眼睛”。
- Application (应用) 面板:查看和管理Cookies、LocalStorage。Pixiv的登录状态主要靠Cookies维持,这里是我们获取关键认证信息的地方。
- JSON 模块 (Python内置):Pixiv的API返回的数据基本都是JSON格式,Python的
json模块可以轻松地将它们转换为字典或列表进行操作。 - 文件操作 (os, hashlib):用于创建本地目录、保存图片、生成唯一的文件名(比如用MD5哈希值防止重复)。
注意:不建议初学者一上来就用
Selenium或Playwright这类浏览器自动化工具。它们虽然能模拟几乎所有用户操作,但资源消耗大、速度慢,容易被识别为自动化脚本。我们的目标是找到效率更高的API接口直接通信。
2.2 爬虫工作流程设计
整个爬虫的运作可以抽象为以下几个步骤,我画了一个简单的流程图来帮助理解:
flowchart TD A[开始: 初始化会话<br>设置请求头] --> B[核心挑战: 模拟登录<br>获取有效Cookies] B --> C{登录方式选择} C -- 推荐/稳定 --> D[方式一: Cookie直连<br>从浏览器手动复制] C -- 自动/编程 --> E[方式二: 密码登录<br>处理验证码与Token] D --> F[登录成功<br>建立认证会话] E --> F F --> G[构造搜索/用户请求] G --> H[解析API返回的JSON数据] H --> I[提取作品ID列表] I --> J[遍历ID, 请求作品详情页] J --> K[解析详情页, 获取原图链接] K --> L[下载图片至本地<br>按规则命名归档] L --> M{是否继续下一页?} M -- 是 --> G M -- 否 --> N[结束]这个流程的核心在于登录和API请求。登录是拿到“门票”,而找到正确的API并构造合法的请求,是拿到“货物”的关键。
3. 实战第一步:环境准备与登录破解
登录是横在Pixiv爬虫面前的第一道,也是最高的门槛。Pixiv的登录流程几经改版,反爬措施严密。这里我提供两种经过验证的策略,第一种更稳定简单,第二种更自动化但复杂。
3.1 基础环境搭建
首先,确保你的Python环境(建议3.8以上)已经就绪,然后安装必要的库:
pip install requests requests-html beautifulsoup4创建一个新的Python文件,比如pixiv_crawler.py,开始导入模块并建立一个会话对象:
import requests import json import os import time from bs4 import BeautifulSoup import hashlib # 建立一个会话 (Session), 它可以自动管理Cookies, 保持登录状态 session = requests.Session() # 设置通用的请求头, 模拟浏览器 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', 'Referer': 'https://www.pixiv.net/', # Referer对于图片防盗链至关重要 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', } session.headers.update(headers)User-Agent告诉服务器我们是什么浏览器,Referer告诉服务器我们是从哪个页面跳转过来的,很多图片服务器会校验这个字段,如果不对会返回403错误。
3.2 登录策略一:Cookie直连(推荐给新手和稳定使用)
这是目前最稳定、最省事的方法。原理是:你手动用浏览器正常登录一次Pixiv,然后把浏览器里的Cookie复制出来,交给爬虫程序使用。这样爬虫就“继承”了你的登录状态。
操作步骤:
- 用Chrome或Edge浏览器正常访问
https://www.pixiv.net/并登录你的账号。 - 按
F12打开开发者工具,切换到Application(应用) 标签。 - 在左侧
Storage下找到Cookies, 点击https://www.pixiv.net。 - 在右侧列表中,找到名为
PHPSESSID的Cookie, 双击其Value值,复制那一长串字符。 - 在你的代码中,将这个值直接设置到会话的Cookie里。
# 将从浏览器复制的 PHPSESSID 值粘贴在这里 phpsessid_value = "你复制的那一串很长的字符" # 设置Cookie cookies = { 'PHPSESSID': phpsessid_value, } session.cookies.update(cookies) # 验证登录是否成功:访问个人主页, 如果返回的HTML中包含你的用户名, 说明成功 test_url = 'https://www.pixiv.net/' response = session.get(test_url) if '你的用户名' in response.text: # 请替换为你的Pixiv昵称 print("登录成功!") else: print("登录失败, 请检查PHPSESSID是否正确或已过期。")实操心得:
PHPSESSID是有有效期的(通常是浏览器会话期间或一段时间)。如果长时间不用,可能需要重新登录复制。这种方式避免了处理密码、验证码等复杂问题,非常适合个人小规模、间歇性的爬取需求。
3.3 登录策略二:模拟密码登录(应对Cookie失效)
如果你想实现全自动登录,或者Cookie方式失效了,就需要模拟完整的登录流程。这个过程非常复杂,因为Pixiv会使用device_token、post_key等动态令牌,并可能触发验证码。
核心步骤解析:
- 获取登录页面, 提取关键Token:首先GET登录页面,从HTML中解析出一个名为
post_key的隐藏输入框的值。这个值是每次登录请求必须携带的。 - 构造登录请求:POST请求到登录接口,需要携带用户名、密码(通常是加密的)、
post_key、device_token等参数。其中device_token可以是一个固定的随机字符串,首次登录后服务器会绑定它。 - 处理验证码:如果登录失败或触发风控,返回的信息可能会要求输入验证码。你需要解析出验证码图片URL,下载并人工识别(或调用OCR服务),然后将验证码填入再次请求。
- 保存Cookies:登录成功后,服务器返回的响应头里会设置新的Cookies,我们的
session会自动保存。之后就可以用这个会话来访问需要登录的页面了。
由于此方法代码冗长且受Pixiv前端改动影响大,这里不展开完整代码,但其核心请求如下所示:
login_url = 'https://accounts.pixiv.net/login?lang=zh' login_api_url = 'https://accounts.pixiv.net/api/login?lang=zh' # 1. 获取登录页和post_key login_page_resp = session.get(login_url) soup = BeautifulSoup(login_page_resp.text, 'html.parser') post_key_input = soup.find('input', {'name': 'post_key'}) post_key = post_key_input['value'] if post_key_input else '' # 2. 构造登录数据 (密码加密是前端完成的, 这里简化展示, 实际需要逆向JS) login_data = { 'pixiv_id': '你的邮箱', 'password': '你的密码', # 注意: 真实密码可能需要经过前端特定算法加密 'post_key': post_key, 'return_to': 'https://www.pixiv.net/', 'device_token': 'your_generated_device_token', # 可自行生成一个UUID 'source': 'pc', } # 3. 发送登录请求 resp = session.post(login_api_url, data=login_data) result = resp.json() if result.get('error'): print(f'登录失败: {result.get("message")}') # 可能需要处理验证码 else: print('登录成功!')重要警告:模拟登录涉及账号安全,且Pixiv的加密逻辑可能随时变更。强烈不建议在公开脚本中硬编码你的账号密码。对于自动化需求,更安全的做法是定期手动更新Cookie。此方法仅作原理了解。
4. 核心抓取逻辑:找到真正的数据接口
登录成功后,我们就要开始找数据了。以“搜索特定标签的作品”为例,我们不会去解析搜索结果页的HTML,因为那是给人类看的,效率低且结构易变。我们要找的是背后为前端提供数据的API。
4.1 发现并分析API
- 打开浏览器,登录Pixiv,在搜索框输入一个标签,比如“オリジナル”(原创)。
- 按
F12->Network(网络) 面板, 勾选Preserve log(保留日志)。 - 刷新页面或点击搜索。在Network面板里,你会看到大量请求。筛选
XHR或Fetch类型的请求。 - 寻找包含“search”或“illust”字样、且返回数据是JSON的请求。通常,Pixiv的搜索API地址类似于:
https://www.pixiv.net/ajax/search/artworks/{关键词}?word={关键词}&order=date_d&mode=all&p={页码}&... - 点击这个请求,查看它的
Headers(请求头) 和Preview(预览)。在Headers里,你需要关注的是Request Headers,特别是Cookie和Referer,我们的爬虫需要模拟这些。在Preview里,你能看到结构化的JSON数据,里面包含了作品ID、标题、作者等信息。
4.2 构造API请求并解析数据
假设我们找到了搜索API的规律,就可以用代码来请求了:
def search_illustrations(keyword, page=1, mode='safe'): """ 搜索插画 :param keyword: 搜索关键词 :param page: 页码 :param mode: 搜索模式, safe/r18 :return: 作品ID列表 """ # 这个URL格式是分析出来的, 可能随网站更新而变化 search_url = f'https://www.pixiv.net/ajax/search/artworks/{keyword}' params = { 'word': keyword, 'order': 'date_d', # 按日期排序 'mode': mode, 'p': page, 's_mode': 's_tag', 'type': 'all', 'lang': 'zh' } # 关键: 必须设置Referer, 通常就是搜索页的URL headers = { 'Referer': f'https://www.pixiv.net/tags/{keyword}/artworks' } try: response = session.get(search_url, params=params, headers=headers) response.raise_for_status() # 检查请求是否成功 data = response.json() # 解析JSON, 提取作品ID。 具体路径需要根据实际API返回结构调整 # 这里是一个示例路径, 你需要用 print(json.dumps(data, indent=2)) 来查看真实结构 illust_ids = [] if data.get('body') and data['body'].get('illustManga'): for item in data['body']['illustManga']['data']: illust_id = item.get('id') if illust_id: illust_ids.append(illust_id) return illust_ids except requests.exceptions.RequestException as e: print(f"搜索请求失败: {e}") return [] except json.JSONDecodeError as e: print(f"解析JSON失败: {e}") return []这个函数返回了一个作品ID的列表。ID是Pixiv上每个作品的唯一标识。
4.3 获取作品详情与原图链接
有了作品ID,我们就可以访问作品详情页的API,获取更详细的信息,特别是原图(或大图)的URL。
def get_illust_detail(illust_id): """ 获取作品详情 :param illust_id: 作品ID :return: 包含图片URLs等信息的字典 """ detail_url = f'https://www.pixiv.net/ajax/illust/{illust_id}' headers = { 'Referer': f'https://www.pixiv.net/artworks/{illust_id}' } try: response = session.get(detail_url, headers=headers) response.raise_for_status() data = response.json() illust_info = {} if data.get('error'): print(f"获取作品 {illust_id} 详情失败: {data.get('message')}") return illust_info body = data.get('body', {}) illust_info['title'] = body.get('title', '') illust_info['user_name'] = body.get('userName', '') illust_info['user_id'] = body.get('userId', '') illust_info['tags'] = [tag['tag'] for tag in body.get('tags', {}).get('tags', [])] # 获取图片URLs - 这是最关键的部分 urls = body.get('urls', {}) # 优先尝试获取原图, 如果没有则用大图 original_url = urls.get('original') regular_url = urls.get('regular') illust_info['image_url'] = original_url if original_url else regular_url # 处理多图作品 (漫画/多图插画) if body.get('pageCount', 1) > 1: pages_url = f'https://www.pixiv.net/ajax/illust/{illust_id}/pages' pages_resp = session.get(pages_url, headers=headers) pages_data = pages_resp.json() illust_info['pages'] = [page['urls']['original'] for page in pages_data.get('body', [])] else: illust_info['pages'] = [illust_info['image_url']] if illust_info['image_url'] else [] return illust_info except Exception as e: print(f"获取作品 {illust_id} 详情时发生错误: {e}") return {}注意事项:
Referer头在这里极其重要。Pixiv的图片服务器会严格检查Referer,如果请求图片时没有带上正确的来源页(即作品详情页),服务器会返回403 Forbidden或404 Not Found。这就是为什么我们必须在请求图片的headers里也加上Referer。
5. 图片下载与本地化管理
拿到图片URL后,下载本身很简单,但如何有序地保存到本地,是需要设计的。
5.1 下载单张图片
def download_image(image_url, save_path, referer): """ 下载单张图片 :param image_url: 图片URL :param save_path: 本地保存路径 :param referer: 用于设置Referer头的来源URL """ if not image_url: return False headers = { 'Referer': referer, # 必须!否则图片服务器会拒绝 'User-Agent': session.headers['User-Agent'] } try: # 流式下载, 适合大文件 response = session.get(image_url, headers=headers, stream=True, timeout=30) response.raise_for_status() # 检查文件是否已存在 (可选, 根据哈希判断更准确) if os.path.exists(save_path): print(f"文件已存在,跳过: {save_path}") return True with open(save_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) print(f"下载成功: {save_path}") return True except requests.exceptions.Timeout: print(f"下载超时: {image_url}") except requests.exceptions.RequestException as e: print(f"下载失败 {image_url}: {e}") except IOError as e: print(f"文件写入失败 {save_path}: {e}") return False5.2 设计本地存储结构
一个好的存储结构能让后期查找和管理事半功倍。我推荐按以下规则组织:
Pixiv_Downloads/ ├── [画师ID]_画师名/ │ ├── [作品ID]_作品标题/ │ │ ├── 0.jpg (或.png, .gif) │ │ ├── 1.jpg (第二张图, 如果有) │ │ └── info.json (保存作品元信息, 如标题、标签、上传时间) │ └── ... ├── 标签_オリジナル/ │ └── ... (类似画师目录结构) └── 收藏夹/ └── ...对应的目录创建和保存函数:
import os import json def save_illust_info(illust_info, base_dir='./Pixiv_Downloads'): """ 保存作品信息和图片 :param illust_info: 作品信息字典 :param base_dir: 下载根目录 """ user_id = illust_info.get('user_id', 'unknown') user_name = illust_info.get('user_name', 'unknown').replace('/', '_').replace('\\', '_') # 处理非法字符 illust_id = illust_info.get('id') # 需要从外部传入或info中包含 title = illust_info.get('title', 'untitled').replace('/', '_').replace('\\', '_')[:50] # 限制长度, 处理非法字符 # 创建目录: base_dir/用户ID_用户名/作品ID_标题 save_dir = os.path.join(base_dir, f"{user_id}_{user_name}", f"{illust_id}_{title}") os.makedirs(save_dir, exist_ok=True) # 保存元信息到JSON文件 info_path = os.path.join(save_dir, 'info.json') with open(info_path, 'w', encoding='utf-8') as f: json.dump(illust_info, f, ensure_ascii=False, indent=2) # 下载所有图片页面 pages = illust_info.get('pages', []) referer_url = f'https://www.pixiv.net/artworks/{illust_id}' for idx, img_url in enumerate(pages): # 根据URL确定文件扩展名 ext = os.path.splitext(img_url)[1] or '.jpg' # 处理可能带查询参数的URL, 如 .jpg?xxxx if '?' in ext: ext = ext.split('?')[0] filename = f"{idx}{ext}" save_path = os.path.join(save_dir, filename) download_image(img_url, save_path, referer_url) time.sleep(1) # 非常重要的礼貌性延迟, 避免请求过快实操心得:
time.sleep()是爬虫的“美德”。在请求之间(尤其是下载图片)添加延迟(比如1-3秒),可以显著降低对目标服务器的压力,减少被封IP的风险。这是“慢就是快”的体现。
6. 完整流程串联与高级技巧
现在我们把所有模块组合起来,形成一个完整的搜索并下载的流程。
def main(): # 1. 初始化会话和登录 (使用Cookie法) # ... (初始化session和设置Cookie的代码, 见3.2节) keyword = "オリジナル" max_pages = 3 # 控制爬取的页数, 避免过量 per_page_items = 30 # 假设每页30个作品 all_downloaded_ids = set() # 用于去重 for page in range(1, max_pages + 1): print(f"\n=== 正在搜索第 {page} 页, 关键词: {keyword} ===") # 2. 搜索作品, 获取ID列表 illust_ids = search_illustrations(keyword, page=page) if not illust_ids: print(f"第 {page} 页未找到作品或请求失败。") break print(f"找到 {len(illust_ids)} 个作品ID。") for illust_id in illust_ids: if illust_id in all_downloaded_ids: print(f"作品 {illust_id} 已下载, 跳过。") continue # 3. 获取作品详情 print(f"处理作品 ID: {illust_id}") illust_info = get_illust_detail(illust_id) if not illust_info or not illust_info.get('pages'): print(f"作品 {illust_id} 详情获取失败或无图片。") continue illust_info['id'] = illust_id # 把ID也存入信息字典 # 4. 保存作品信息和图片 save_illust_info(illust_info, base_dir='./Pixiv_Downloads') all_downloaded_ids.add(illust_id) # 5. 礼貌延迟, 避免请求过快 time.sleep(2) # 每页完成后也稍作延迟 time.sleep(3) print(f"\n全部任务完成!共处理了 {len(all_downloaded_ids)} 个作品。") if __name__ == '__main__': main()6.1 处理反爬虫策略
Pixiv除了Referer检查和登录状态验证,还可能采取其他措施:
- 频率限制:这是最直接的。我们的
time.sleep()就是应对这个。如果还遇到429(Too Many Requests)错误,需要进一步增加延迟,或使用更复杂的随机延迟(如time.sleep(random.uniform(1, 3)))。 - 请求头校验:除了
User-Agent和Referer,有些API可能还会检查Accept,Accept-Language,Sec-Fetch-*等头。尽量从浏览器中复制完整的请求头。 - IP封锁:如果单个IP在短时间内发出过多请求,可能会被暂时封锁。对于大规模爬取,需要考虑使用代理IP池。但这超出了基础爬虫的范围,且涉及额外成本和服务。
6.2 错误处理与重试机制
网络请求充满不确定性,健壮的爬虫必须有良好的错误处理。
def robust_request(url, session, max_retries=3, **kwargs): """带重试机制的请求函数""" for attempt in range(max_retries): try: resp = session.get(url, timeout=10, **kwargs) resp.raise_for_status() return resp except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: print(f"请求 {url} 超时或连接错误 (尝试 {attempt+1}/{max_retries}): {e}") if attempt < max_retries - 1: wait_time = 2 ** attempt # 指数退避 print(f"等待 {wait_time} 秒后重试...") time.sleep(wait_time) else: print(f"重试 {max_retries} 次后仍失败。") raise except requests.exceptions.HTTPError as e: if resp.status_code == 429: print("触发频率限制, 等待更长时间...") time.sleep(30) continue else: # 其他HTTP错误, 如403, 404, 通常重试无用 print(f"HTTP错误 {resp.status_code}: {e}") raise return None在download_image和get_illust_detail函数中,可以调用这个robust_request来代替直接的session.get。
7. 常见问题与排查技巧实录
即使按照步骤操作,你也可能会遇到各种问题。这里记录了一些典型问题和解决方法。
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 登录失败(Cookie法) | 1.PHPSESSID值错误或已过期。2. 复制的Cookie不完整,缺少其他必要项。 | 1. 重新登录Pixiv,复制全新的PHPSESSID。2. 检查浏览器中 pixiv.net域名下是否有其他看起来重要的Cookie(如device_token),一并复制。 |
| 登录失败(密码法) | 1. 登录接口或参数已变更。 2. 密码未加密或加密方式错误。 3. 触发验证码。 | 1. 使用浏览器开发者工具,重新录制一次登录过程,对比请求参数。 2. 逆向登录页面的JavaScript,找到密码加密函数。 3. 手动处理验证码,或寻找可用的OCR服务集成。 |
| 搜索API返回空数据 | 1. API URL或参数格式已更新。 2. 请求头(特别是 Referer)不正确。3. 登录状态失效。 | 1. 再次使用Network面板分析最新的搜索请求。 2. 确保 Referer头与浏览器中发出的请求完全一致。3. 重新获取有效的Cookie。 |
| 获取作品详情失败 | 1. 作品ID不存在或已被删除。 2. 作品是R-18内容,而当前登录模式为 safe。3. 请求头缺失。 | 1. 手动在浏览器访问该作品链接确认。 2. 在搜索或详情请求中尝试将 mode参数改为all或r18(需账号设置允许)。3. 补全 Referer和X-Requested-With等头。 |
| 图片下载返回403错误 | 1.Referer头缺失或错误(最常见)。2. 图片URL已过期或需要特定令牌。 | 1.确保下载图片的请求中,Referer头设置为该作品详情页的URL,如https://www.pixiv.net/artworks/123456。2. 图片URL可能来自 urls.original或urls.regular,如果都失败,尝试从详情页HTML中解析新的图片地址。 |
| 下载的图片损坏或很小 | 下载到了预览图(缩略图),而非原图。 | 检查代码中获取的图片URL。确保使用的是urls.original(原图),如果为空再降级使用urls.regular(大图)。多图作品要使用/pages接口返回的URL数组。 |
| 很快被限制访问 | 请求频率过高,触发了反爬虫的风控。 | 1.大幅增加请求间隔,在关键请求(如详情页、图片下载)之间加入time.sleep(random.uniform(2, 5))。2. 模拟更真实的人类行为,比如随机浏览一些其他页面。 3. 考虑使用高质量的代理IP。 |
最后再分享一个小技巧:在开发调试阶段,善用print(json.dumps(data, indent=2))将API返回的复杂JSON数据漂亮地打印出来。这是你理解数据结构最直接的方式。不要猜,要看服务器实际返回了什么。同时,将关键的中间数据(如作品ID列表、图片URL)保存到本地的文本文件里,这样即使程序中途出错,你也有记录可以恢复,不必每次都从头开始爬。这个项目最耗时的部分往往是调试和适应网站的变化,一个稳定的登录状态和清晰的数据流日志,能帮你节省大量时间。