新学期学的Python数据分析(笑)搞了个期末作业,作为绅士自然不可能进行常规操作,于是本着se图是第一生产力去搞了个Pixiv的爬虫,然后又本着给后代的诸位绅士们指路(没错我也成为了光)的心情,来搞了个博客,由于是第一次写博客这玩意,很多功能还不是很清楚,有错漏的地方还请和我联系。
前提材料:实现科学上网
直接上图:Pixiv登录界面链接
进入登录页面,然后F12进入开发者选项——选择Network——选择XHR,然后直接点登录,查看抓到的POST包:login?lang=zh,直接看Headers往下拉到底查看Form Date,看到有一堆的东西,这就是我们点击登录后主机向Pixiv发送的登录数据,其中最重要的是post_key,这玩意是我们登录的时候必须要有的,但是经多次检查发现这玩意是随机生成的,于是将其复制到网页源代码查找,发现post_key直接放在了源代码里面。
既然post_key直接放在了源代码中,我们提取起来就很方便了。从源代码中提取数据的方法有很多种,我选择用BeautifulSoup 进行网页分析。
from bs4 import BeautifulSoup as bs
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36',
'referer': 'https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index'
# 注意校验码referer,不添加会被反爬403错误,可以在抓包里找
}
url = 'https://accounts.pixiv.net/login?return_to=https%3A%2F%2Fwww.pixiv.net%2F&lang=zh&source=pc&view_type=page'
html = requests.get(url, headers=headers).text
key_soup = bs(html, 'lxml')
post_key = key_soup.find('input')['value'] # 查找post_key
这样post_key就获取到了,当然除了这种方法还有用正则啊什么的,自行探索。
首先要理解,直接使用requests的get是不行的,因为模拟登录后没有保持登录等于没登。所以我使用requests.session,让请求变成会话,这样就能保证登录状态了。
将代码整合一下。
from bs4 import BeautifulSoup as bs
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36',
'referer': 'https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index'
# 注意校验码referer,不添加会被反爬403错误,可以在抓包里找
}
accounts_url = 'https://accounts.pixiv.net/login?return_to=https%3A%2F%2Fwww.pixiv.net%2F&lang=zh&source=pc&view_type=page'
login_url = 'https://accounts.pixiv.net/api/login?lang=zh' # 登录URL
se = requests.session()
pixiv_key_html = se.get(accounts_url, headers=headers).text
pixiv_key_soup = bs(pixiv_key_html, 'lxml')
post = pixiv_key_soup.find('input')['value']
data = {
'pixiv_id': '', # 你的用户名
'password': '', # 你的登陆密码
'post_key': post,
"ref": "wwwtop_accounts_index",
"source": "pc",
'return_to': 'https://www.pixiv.net/'
}
dare = se.post(login_url, data=data, headers=headers).text # 登录
da = json.loads(dare)
print(da)
当然现在这样登录会被要求google v3验证,就是该死的人机验证(请选择你的消防栓),模拟登陆到此战败。
那么有没有解决方法?有,使用 selenium/splinter+requests吧,上一段基本完整的代码。
注意:食用以下代码需要下载chormdrver
import time
from splinter.browser import Browser
browser = None
browser_name = "chrome" # 浏览器名
driver_path = "chromedriver.exe" # 打开浏览器的驱动
base_url = "https://www.pixiv.net/ranking.php" # pixiv排行榜页面
browser = Browser(driver_name=browser_name, executable_path=driver_path) # 打开浏览器
browser.visit(base_url) # 访问页面
time.sleep(2) # 暂停两秒,等待浏览器加载完成
print(".......请在打开的浏览器中登录........")
browser.find_link_by_text('登录').click() # 点击登录
username = ''
password = ''
if username is not None:
browser.find_by_xpath('//input[@autocomplete="username"]').fill(username)
if password is not None:
browser.find_by_xpath('//input[@autocomplete="current-password"]').fill(password)
browser.find_by_text('登录').last.click()
那为什么我还要写这些呢,因为除了Pixiv,也许我们还要爬其他的一些要登录的网页,那么至少我们掌握了模拟登陆的开发流程。
而且我所爬取的排行榜并不需要模拟登陆,如果我模拟登陆成功那么到结题展示的时候我很可能会社会性死亡,所以我最终放弃了模拟登陆。
我们进入排行榜页面,继续F12——Network——XHR
好像没有什么数据包?那我们把页面往下拉,直到第51位,这时候传来了新的数据包
我们点进去,将里面的Request URL复制然后新页面打开,会得到满屏幕的数据,但是这些数据是不能直接读取的,我们需要进行解析,ctrl+a将内容全部复制到www.json.cn,这样数据就清晰明了了。
注意到url和illust_id,我们直接点击url很大概率是403或者是小图,所以这里的url并没有太大作用,我们需要的是illust_id,再通过id来构建真正的url。
假如我们把url的p值改为1,那么我们就得到了第一页的数据,以此推类。
python提供了关于json操作的库,库名就是json,我们可以使用json库对json数据进行提取。
import json
list_id = []
url_rank = 'https://www.pixiv.net/ranking.php?' # 排行榜url
params_rank = { # 排行榜url接口,通过分析url链接构建
'mode': moder, # 榜名,可以日榜:daily、周榜:weekly、月榜:monthly
'content': 'illust', # 搜索类型,可根据自己需要更改,见pixiv排行榜链接头
'p': '1', # 页数,最大为10页,每页50条
'format': 'json'
}
url_get = requests.get(url_rank, headers=headers, params=params_rank).text
url_json = json.loads(url_get)
# 注意json返回后是一个字典,我门通过索引字典取得illust_id
for dict1 in url_json['contents']: # 获取图片id
list_id.append(dict1['illust_id'])
到此我们应该可以想到如何构建真正的图片url了,即通过修改小图的url实现真正的url,但这样并不能获取到所有的图片,因为有的图片存在多页,排行榜提供的url仅能让我们下载上榜的一页,那我们如何实现获取所有的图片url?
随便点进一张图片进行数据分析,我们会发现并没有得到有用的数据,于是我们找到一个存在多页的排行榜图片进行分析,我们开启抓包后再点击查看全部,发现一个新数据包。
打开该包的url提取数据进行json分析,我们发现:
这样我们的所有url就可以通过json提取获取到了。
list_url = []
ID = list_id.pop(0) # 提取列表第一个URL并删除
url_page = 'https://www.pixiv.net/ajax/illust/' + str(ID) + '/pages?lang=zh' # 查询真实ID包
url_text = requests.get(url_page, headers=headers).text
url_testjson = json.loads(url_text)
for dict2 in url_testjson['body']: # 获取url
list_url.append(dict2['urls']['original'])
url = dict2['urls']['original']
print(f'获取链接:{url}')
到这里基本上可以知道接下来的代码怎么写了,不多说了,直接上代码。
import os
j = 1 # 图片编号
for i in list_url:
headers['referer'] = 'https://www.pixiv.net/ranking.php?mode=monthly' # 注意添加防盗链
pixiv_test = requests.get(i, headers=headers)
path = params_rank['mode'] + '/' + str(j) + '.jpg' # 文件名创建
if not os.path.exists(params_rank['mode']): # 如果没有文件夹,则创建文件夹
os.mkdir(params_rank['mode'])
with open(path, 'wb') as f:
f.write(pixiv_test.content) # 以二进制保存图片
j += 1
print(f'图片{j}正在保存...')
到此代码就基本完成了,但是我们仍然需要进行代码优化,一是为了方便自己和他人阅读,二是Pixivt图片的下载速度实在是龟速。
在获取图片url和下载图片两处地方,我们可以加入多线程进行优化,提升代码运行速度。这里仅展示获取图片url部分:
import threading
glock = threading.Lock() # 创建一个锁
def url_get():
while True:
glock.acquire() # 加锁
if len(list_id) == 0:
glock.release() # 释放锁
break
else:
ID = list_id.pop(0) # 提取列表第一个URL并删除
glock.release() # 释放锁
url_page = 'https://www.pixiv.net/ajax/illust/' + str(ID) + '/pages?lang=zh'
url_text = requests.get(url_page, headers=headers).text
url_testjson = json.loads(url_text)
for dict2 in url_testjson['body']: # 获取url
list_url.append(dict2['urls']['original'])
url = dict2['urls']['original']
print(f'获取链接:{url}')
for j in range(3): # 多线程并发
urlget = threading.Thread(target=url_get())
urlget.start()
以上代码仅实现了单页的获取,我们需要修改代码,使其实现多页排行榜获取。
还有各种格式问题,创建类和整合变量能让我们的代码更加清晰明了,封装函数则是让我们的代码更加方便操作以实现不同功能。
感谢各位能看到这里,这是凉生真正意义上完成的第一个爬虫,虽然还有很多缺陷,比如懒得实现的自助搜索功能(主要是搜索出来的图片质量参差不齐,让人难受),还有模拟登陆失败导致无法自动爬取某些奇怪的图片(懂的都懂),不过结果终归是好的。不过,不要直接拿代码当做你的作业,鬼知道老师有什么办法查到(笑)。
最后,望诸位绅士共勉。
循循而进,一往无前。
import json
import os
import requests
from bs4 import BeautifulSoup as bs
import threading
class Pixiv:
def __init__(self, moder, pn):
self.pixiv_username = ''
self.pixiv_password = ''
self.accounts_url = 'https://accounts.pixiv.net/login?return_to=https%3A%2F%2Fwww.pixiv.net%2F&lang=zh&source=pc&view_type=page' # 登录界面连接
self.login_url = 'https://accounts.pixiv.net/api/login?lang=zh' # 登录URL
self.post = [] # 获取登录所需的随机cookie
# 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36' Linux请求头
self.headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36',
'referer': 'https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index'
# 校验码,不添加会被反扒403错误
}
self.list_id = [] # id列表
self.list_url = [] # 排行榜url列表
self.url_rank = 'https://www.pixiv.net/ranking.php?' # 排行榜url
self.params_rank = { # 排行榜url接口
'mode': moder,
'content': 'illust',
'p': pn,
'format': 'json'
}
self.i = 1 # 图片id
self.glock = threading.Lock() # 锁
def login(self): # 模拟登陆
se = requests.session()
pixiv_key_html = se.get(self.accounts_url, headers=self.headers).text
pixiv_key_soup = bs(pixiv_key_html, 'lxml')
self.post = pixiv_key_soup.find('input')['value']
# print(self.post)
data = {
'pixiv_id': self.pixiv_username,
'password': self.pixiv_password,
'post_key': self.post,
"ref": "wwwtop_accounts_index",
"source": "pc",
'return_to': 'https://www.pixiv.net/'
}
dare = se.post(self.login_url, data=data, headers=self.headers).text # 登录
da = json.loads(dare)
print(da)
se.close()
# Pixiv.test(self)
# Pixiv.url_get(self)
def id_get(self): # 获取rank作品id
if self.params_rank['mode'] == '1':
self.params_rank['mode'] = 'daily' # 日排行
self.headers['referer'] = 'https://www.pixiv.net/ranking.php?mode=daily&content=illust'
elif self.params_rank['mode'] == '2':
self.params_rank['mode'] = 'weekly' # 周排行
self.headers['referer'] = 'https://www.pixiv.net/ranking.php?mode=weekly&content=illust'
elif self.params_rank['mode'] == '3':
self.params_rank['mode'] = 'monthly' # 月排行
self.headers['referer'] = 'https://www.pixiv.net/ranking.php?mode=monthly&content=illust'
for u in range(int(self.params_rank['p'])):
self.params_rank['p'] = str(u+1)
# print(self.params_rank)
url_get = requests.get(self.url_rank, headers=self.headers, params=self.params_rank).text
url_json = json.loads(url_get)
# print(url_json)
for dict1 in url_json['contents']: # 获取图片id
self.list_id.append(dict1['illust_id'])
# Pixiv.test(self)
def url_get(self): # 多线程获取url
while True:
self.glock.acquire() # 加锁
if len(self.list_id) == 0:
self.glock.release() # 释放锁
break
else:
ID = self.list_id.pop(0) # 提取列表第一个URL并删除
self.glock.release() # 释放锁
test1 = 'https://www.pixiv.net/ajax/illust/' + str(ID) + '/pages?lang=zh' # 查询真实ID包
url_text = requests.get(test1, headers=self.headers).text
url_testjson = json.loads(url_text)
for dict2 in url_testjson['body']: # 获取url
self.list_url.append(dict2['urls']['original'])
url = dict2['urls']['original']
print(f'获取链接:{url}')
# def test(self): # 测试保存
# j = 1
# for i in self.dict_rank:
# self.headers['referer'] = 'https://www.pixiv.net/ranking.php?mode=monthly'
# pixiv_test = requests.get(i, headers=self.headers)
# path = self.params_rank['mode'] + '/' + str(j) + '.jpg'
# if not os.path.exists(self.params_rank['mode']):
# os.mkdir(self.params_rank['mode'])
# with open(path, 'wb') as f:
# f.write(pixiv_test.content)
# j += 1
# print(f'图片{i}正在保存...')
def download(self): # 多线程下载
while True:
self.glock.acquire() # 加锁
if len(self.list_url) == 0:
self.glock.release() # 释放锁
break
else:
if not os.path.exists(self.params_rank['mode']): # 创建文件夹
os.mkdir(self.params_rank['mode'])
url = self.list_url.pop(0) # 提取列表第一个URL并删除
self.glock.release() # 释放锁
# 修改文件名
if url[-3:] == 'jpg':
path = self.params_rank['mode'] + '/' + str(self.i) + '.jpg'
elif url[-3:] == 'png':
path = self.params_rank['mode'] + '/' + str(self.i) + '.png'
pixiv_img = requests.get(url, headers=self.headers)
with open(path, 'wb') as f:
f.write(pixiv_img.content)
print(f'图片{self.i}正在保存...')
self.i += 1
def main():
print('##————Pixiv————##')
moder = input('请输入排行榜的时间(日:1/周:2/月:3):')
pn = input('请输入你想要多少页(50/页):')
pixivc = Pixiv(moder, pn)
pixivc.id_get()
for j in range(3):
urlget = threading.Thread(target=pixivc.url_get())
urlget.start()
for i in range(3):
download = threading.Thread(target=pixivc.download())
download.start()
print(f'爬取结束,共保存{pixivc.i-1}张图片')
if __name__ == "__main__":
main()