Python爬虫实战:requests+selenium攻克Pixiv登录与图片抓取
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秒),还可以考虑以下策略:
- 使用代理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) - 伪装请求头 :除了
User-Agent和Referer,还可以随机添加一些其他常见的请求头,如Accept-Language,Accept-Encoding等,让请求看起来更“自然”。 - 分散爬取任务 :不要一次性爬取一个画师的所有作品(可能成百上千)。可以分多次进行,每次爬取一部分,中间间隔数小时甚至一天。
6.3 数据去重与增量爬取
为了避免重复下载,我们在 download_image 函数中做了简单的文件存在性检查。但在实际项目中,你可能需要更精细的管理,比如维护一个本地数据库(如SQLite),记录已下载的作品ID、图片URL、下载时间、画师ID等元数据。每次爬取前先查询数据库,只下载新的作品。这不仅能节省时间和流量,也能在爬虫中断后从中断点恢复。
6.4 错误处理与日志记录
一个工业级的爬虫必须有完善的错误处理和日志记录。我们上面的代码使用了 try...except 来捕获异常,但还不够。建议使用Python的 logging 模块,将不同级别的信息(DEBUG, INFO, WARNING, ERROR)输出到文件和控制台。对于网络请求错误、解析错误、文件IO错误等,都应该被捕获并记录,而不是让程序直接崩溃。同时,可以为每个失败的任务设置重试机制(例如,重试3次)。
7. 伦理、法律与最佳实践:做一个负责任的爬虫使用者
最后,也是最重要的一部分,我们必须再次强调爬虫使用的边界。
- 尊重
robots.txt:访问https://www.pixiv.net/robots.txt。虽然Pixiv的robots.txt可能没有明确禁止所有爬虫,但我们必须遵守其中规定的爬取延迟(Crawl-delay),并避免爬取明确禁止的路径。 - 控制爬取速度 :这是对网站服务器的基本尊重。将请求间隔设置得足够长,我的经验是,在非高峰时段,单线程爬虫的间隔最好不低于3-5秒。绝对避免开启多线程疯狂请求。
- 仅用于个人用途 :你爬取的数据(图片)版权归原作者所有。严禁用于任何商业目的,包括但不限于售卖、未经授权的二次创作、训练AI模型等。最好在下载的图片文件中保留原作品的ID、标题等信息。
- 关注画师的设置 :有些画师会在个人简介或作品描述中注明“禁止转载”或“禁止保存”。虽然技术上仍可爬取,但从道德层面,应尊重画师的意愿。
- 不要给服务器带来负担 :如果你的爬虫导致目标网站响应变慢或影响其他用户,请立即停止。
编写爬虫是一项强大的技能,但能力越大,责任越大。通过今天分享的这套从登录、解析到下载的完整方案,以及其中融入的诸多细节和避坑经验,希望你能不仅实现Pixiv图片的自动化收藏,更能理解其背后的技术逻辑和伦理考量,安全、合规、高效地运用这项技术。在实际操作中,最考验人的往往不是代码本身,而是对目标网站反爬策略的耐心分析和应对,以及对可能遇到的各种边界情况的周全处理。多测试,多观察,你的爬虫会越来越稳健。
更多推荐


所有评论(0)