PixivSpider/Pixiv.py

294 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
P站小爬虫 爬每日排行榜
环境需求Python3.8+ / Redis
项目地址https://github.com/nyaasuki/PixivSpider
"""
import re
import os
try:
import requests
import redis
except:
print('检测到缺少必要包!正在尝试安装!.....')
os.system(r'pip install -r requirements.txt')
import requests
import redis
requests.packages.urllib3.disable_warnings()
error_list = []
class PixivSpider(object):
def __init__(self, db=0):
self.ajax_url = 'https://www.pixiv.net/ajax/illust/{}/pages' # id
self.top_url = 'https://www.pixiv.net/ranking.php'
self.r = redis.Redis(host='localhost', port=6379, db=db, decode_responses=True)
def get_list(self, pid):
"""
获取作品所有页面的URL
:param pid: 作品ID
"""
try:
# 检查Redis中是否已记录该作品已完全下载
if self.r.get(f'downloaded:{pid}') == 'complete':
print(f'作品ID:{pid}已在Redis中标记为完全下载跳过')
return None
# 发送请求获取作品的所有图片信息
response = requests.get(self.ajax_url.format(pid), headers=self.headers, verify=False)
json_data = response.json()
# 检查API返回是否有错误
if json_data.get('error'):
print(f'获取作品ID:{pid}失败:{json_data.get("message")}')
return pid
# 从返回数据中获取图片列表
images = json_data.get('body', [])
if not images:
print(f'作品ID:{pid}没有图片')
return pid
# 获取Redis中已下载的页面记录
downloaded_redis = set()
for i in range(len(images)):
if self.r.get(f'downloaded:{pid}_p{i}') == 'true':
downloaded_redis.add(i)
# 检查本地已下载的文件并更新Redis记录
if os.path.exists('./img'):
for f in os.listdir('./img'):
if f.startswith(f'{pid}_p'):
page = int(re.search(r'_p(\d+)\.', f).group(1))
if self.r.get(f'downloaded:{pid}_p{page}') != 'true':
self.r.set(f'downloaded:{pid}_p{page}', 'true')
print(f'发现本地文件并更新Redis记录{f}')
# 使用Redis记录作为唯一来源
downloaded = downloaded_redis
# 遍历所有图片进行下载
for image in images:
# 检查图片数据格式是否正确
if 'urls' not in image or 'original' not in image['urls']:
print(f'作品ID:{pid}的图片数据格式错误')
continue
# 获取原图URL和页码信息
original_url = image['urls']['original']
page_num = int(re.search(r'_p(\d+)\.', original_url).group(1))
# 检查是否已下载过该页面优先使用Redis记录
if page_num in downloaded:
print(f'作品ID:{pid}{page_num}页在Redis中已标记为下载跳过')
continue
# 下载图片如果下载失败返回作品ID以便后续处理
why_not_do = self.get_img(original_url)
if why_not_do == 1:
return pid
except requests.exceptions.RequestException as e:
print(f'获取作品ID:{pid}时发生网络错误:{str(e)}')
return pid
except Exception as e:
print(f'处理作品ID:{pid}时发生错误:{str(e)}')
return pid
def get_img(self, url):
"""
下载单个图片
:param url: 原图URL格式如https://i.pximg.net/img-original/img/2024/12/14/20/00/36/125183562_p0.jpg
:return: 0表示下载成功1表示下载失败
"""
# 确保下载目录存在
if not os.path.isdir('./img'):
os.makedirs('./img')
# 从URL提取作品ID、页码和文件扩展名
match = re.search(r'/(\d+)_p(\d+)\.([a-z]+)$', url)
if not match:
print(f'无效的URL格式: {url}')
return 1
# 解析URL信息并构建文件名
illust_id, page_num, extension = match.groups()
file_name = f"{illust_id}_p{page_num}.{extension}"
# 检查Redis中是否已记录为下载
if self.r.get(f'downloaded:{illust_id}_p{page_num}') == 'true':
print(f'Redis记录{file_name}已下载,跳过')
return 0
# 作为备份检查,验证文件是否存在
if os.path.isfile(f'./img/{file_name}'):
# 如果文件存在但Redis没有记录更新Redis记录
self.r.set(f'downloaded:{illust_id}_p{page_num}', 'true')
print(f'文件已存在但Redis未记录已更新Redis{file_name}')
return 0
# 开始下载流程
print(f'开始下载:{file_name} (第{int(page_num)+1}张)')
t = 0 # 重试计数器
# 最多重试3次
while t < 3:
try:
# 下载图片设置15秒超时
img_temp = requests.get(url, headers=self.headers, timeout=15, verify=False)
if img_temp.status_code == 200:
break
print(f'下载失败,状态码:{img_temp.status_code}')
t += 1
except requests.exceptions.RequestException as e:
print(f'连接异常:{str(e)}')
t += 1
# 如果重试3次都失败放弃下载
if t == 3:
print(f'下载失败次数过多,跳过该图片')
return 1
# 将图片内容写入文件
with open(f'./img/{file_name}', 'wb') as fp:
fp.write(img_temp.content)
# 下载成功后在Redis中记录
self.r.set(f'downloaded:{illust_id}_p{page_num}', 'true')
# 获取作品总页数并检查是否已下载所有页面
page_count = self.r.get(f'total_pages:{illust_id}')
if not page_count:
# 当前页号+1可能是总页数保守估计
self.r.set(f'total_pages:{illust_id}', str(int(page_num) + 1))
elif int(page_num) + 1 == int(page_count):
# 如果当前是最后一页,检查是否所有页面都已下载
all_downloaded = all(
self.r.get(f'downloaded:{illust_id}_p{i}') == 'true'
for i in range(int(page_count))
)
if all_downloaded:
self.r.set(f'downloaded:{illust_id}', 'complete')
print(f'作品ID:{illust_id}已完全下载')
print(f'下载完成并已记录到Redis{file_name}')
return 0
def get_top_url(self, num):
"""
获取每日排行榜的特定页码数据
:param num: 页码数1-10
:return: None
"""
params = {
'mode': 'daily',
'content': 'illust',
'p': f'{num}',
'format': 'json'
}
response = requests.get(self.top_url, params=params, headers=self.headers, verify=False)
json_data = response.json()
self.pixiv_spider_go(json_data['contents'])
def get_top_pic(self):
"""
从排行榜数据中提取作品ID和用户ID
并将用户ID存入Redis数据库中
:return: 生成器返回作品ID
"""
for url in self.data:
illust_id = url['illust_id'] # 获取作品ID
illust_user = url['user_id'] # 获取用户ID
yield illust_id # 生成作品ID
self.r.set(illust_id, illust_user) # 将PID保存到Redis中
@classmethod
def pixiv_spider_go(cls, data):
"""
存储排行榜数据供后续处理
:param data: 排行榜JSON数据中的contents部分
"""
cls.data = data
@classmethod
def pixiv_main(cls):
"""
爬虫主函数
1. 选择Redis数据库
2. 获取或设置Cookie
3. 配置请求头
4. 遍历排行榜页面1-10页
5. 下载每个作品的所有图片
6. 处理下载失败的作品
"""
# 选择Redis数据库
while True:
try:
print("\n可用的Redis数据库:")
for i in range(6):
print(f"{i}.DB{i}")
db_choice = input("\n请选择Redis数据库 (0-5): ")
db_num = int(db_choice)
if 0 <= db_num <= 5:
break
print("错误请输入0到5之间的数字")
except ValueError:
print("错误:请输入有效的数字")
global pixiv
pixiv = PixivSpider(db_num)
print(f"\n已选择 DB{db_num}")
# 从Redis获取Cookie如果没有则要求用户输入
cookie = pixiv.r.get('cookie')
if not cookie:
cookie = input('请输入一个cookie')
pixiv.r.set('cookie', cookie)
# 配置请求头包含必要的HTTP头部信息
cls.headers = {
'accept': 'application/json',
'accept-language': 'zh-CN,zh;q=0.9,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6',
'dnt': '1',
'cookie': f'{cookie}',
'referer': 'https://www.pixiv.net/',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36'
}
print('开始抓取...')
# 遍历排行榜前10页
for i in range(1, 11, 1): # p站每日排行榜最多为500个50个/页 x 10页
pixiv.get_top_url(i) # 获取当前页的排行榜数据
for j in pixiv.get_top_pic(): # 遍历当前页的所有作品
k = pixiv.get_list(j) # 下载作品的所有图片
if k: # 如果下载失败将作品ID添加到错误列表
error_list.append(k)
# 清理下载失败的作品记录
for k in error_list:
pixiv.r.delete(k)
if __name__ == '__main__':
try:
print('正在启动Pixiv爬虫...')
print('确保已安装并启动Redis服务')
print('确保已准备好有效的Pixiv Cookie')
# 运行主程序
PixivSpider.pixiv_main()
print('爬虫运行完成')
except redis.exceptions.ConnectionError:
print('错误无法连接到Redis服务请确保Redis服务正在运行')
except KeyboardInterrupt:
print('\n用户中断运行')
except Exception as e:
print(f'发生错误:{str(e)}')