前沿拓展:
网页登陆qq空间
你必须在这个IP上,长时间登入、 在QQ软件上保存信息:
request 获取相册列表所需的请求信息,包括请求链接和参数response 数据包包含的所有相册的信息,是每个相册所含照片对应的请求包参数的数据来源
先看请求包:
# url
https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/fcg_list_album_v3
# args
g_tk: 477819917
callback: shine0_Callback
t: 691481346
hostUin: 123456789
uin: 123456789
appid: 4
inCharset: utf-8
outCharset: utf-8
source: qzone
plat: qzone
format: jsonp
notice: 0
filter: 1
handset: 4
pageNumModeSort: 40
pageNumModeClass: 15
needUserInfo: 1
idcNum: 4
callbackFun: shine0
_: 1551788226819
其中hostUin, uin都是QQ号,g_tk是必须的且每次重新登录都会更新(后面有讲如何获取),其它有些参数不是必须的,我尝试后整理出如下请求参数:
query = {
‘g_tk’: self.g_tk,
‘hostUin’: self.username,
‘uin’: self.username,
‘appid’: 4,
‘inCharset’: ‘utf-8’,
‘outCharset’: ‘utf-8’,
‘source’: ‘qzone’,
‘plat’: ‘qzone’,
‘format’: ‘jsonp’
}
接下来看jsonp格式的跨域响应包:
shine0_Callback({
“code”:0,
“subcode”:0,
“message”:””,
“default”:0,
“data”:
{
“albumListModeSort” : [
{
“allowAccess” : 1,
“anonymity” : 0,
“bitmap” : “10000000”,
“classid” : 106,
“comment” : 11,
“createtime” : 1402661881,
“desc” : “”,
“handset” : 0,
“id” : “V13LmPKk0JLNRY”,
“lastuploadtime” : 1402662103,
“modifytime” : 1408271987,
“name” : “毕业季”,
“order” : 0,
“pre” : “http://b171.photo.store.qq.com/p**?/V13LmPKk0JLNRY/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!/a/dIY29GUbJgAA”,
“priv” : 1,
“pypriv” : 1,
“total” : 4,
“viewtype” : 0
},
shine0_Callback是请求包的callbackFun参数决定的,如果没这个参数,响应包会以_Callback作为默认名,当然这都不重要。所有相册信息以json格式存入albumListModeSort中,上面仅截取了一个相册的信息。
相册信息中,name代表相册名称,id作为唯一标识可用于请求该相册内的照片信息,而pre仅仅是一个预览缩略图的链接,无关紧要。
分析单个相册
与获取相册信息类似,进入某一相册,使用cgi_list过滤数据包,找到该相册的照片信息
photo list
同样的道理,根据数据包可以获取照片列表信息的请求包和响应信息,先看请求:
# url
https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/cgi_list_photo
# args
g_tk: 477819917
callback: shine0_Callback
t: 952444063
mode: 0
idcNum: 4
hostUin: 123456789
topicId: V13LmPKk0JLNRY
noTopic: 0
uin: 123456789
pageStart: 0
pageNum: 30
skipCmtCount: 0
singleurl: 1
batchId:
notice: 0
appid: 4
inCharset: utf-8
outCharset: utf-8
source: qzone
plat: qzone
outstyle: json
format: jsonp
json_esc: 1
question:
answer:
callbackFun: shine0
_: 1551790719497
其中有几个关键参数:
g_tk – 与相册列表参数一致topicId – 与相册列表参数中的id一致pageStart – 本次请求照片的起始编号pageNum – 本次请求的照片数量
为了一次性获取所有照片,可以将pageStart设为0,pageNum设为所有相册所含照片的最大值。
同样可以对上面的参数进行简化,在相册列表请求参数的基础上添加topicId,pageStart和pageNum三个参数即可。
下面来看返回的照片列表信息:
shine0_Callback({
“code”:0,
“subcode”:0,
“message”:””,
“default”:0,
“data”:
{
“limit” : 0,
“photoList” : [
{
“batchId” : “1402662093402000”,
“browser” : 0,
“cameratype” : ” “,
“cp_flag” : false,
“cp_x” : 455,
“cp_y” : 388,
“desc” : “”,
“exif” : {
“exposureCompensation” : “”,
“exposureMode” : “”,
“exposureProgram” : “”,
“exposureTime” : “”,
“flash” : “”,
“fnumber” : “”,
“focalLength” : “”,
“iso” : “”,
“len**odel” : “”,
“make” : “”,
“meteringMode” : “”,
“model” : “”,
“originalTime” : “”
},
“forum” : 0,
“frameno” : 0,
“height” : 621,
“id” : 0,
“is_video” : false,
“is_weixin_mode” : 0,
“i**ultiup” : 0,
“lloc” : “NDN0sggyKs3**lOg6eYgh**0ZR**AAA!”,
“modifytime” : 1402661792,
“name” : “QQ图片20140612104616”,
“origin” : 0,
“origin_upload” : 0,
“origin_url” : “”,
“owner” : “123456789”,
“ownername” : “123456789”,
“photocubage” : 91602,
“phototype” : 1,
“picmark_flag” : 0,
“picrefer” : 1,
“platformId” : 0,
“platformSubId” : 0,
“poiName” : “”,
“pre” : “http://b171.photo.store.qq.com/p**?/V13LmPKk0JLNRY/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!/a/dIY29GUbJgAA&bo=pANtAgAAAAABCeY!”,
“raw” : “http://r.photo.store.qq.com/p**?/V13LmPKk0JLNRY/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!/r/dIY29GUbJgAA”,
“raw_upload” : 1,
“rawshoottime” : 0,
“shoottime” : 0,
“shorturl” : “”,
“sloc” : “NDN0sggyKs3**lOg6eYgh**0ZR**AAA!”,
“tag” : “”,
“uploadtime” : “2014-06-13 20:21:33”,
“url” : “http://b171.photo.store.qq.com/p**?/V13LmPKk0JLNRY/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!/b/dIY29GUbJgAA&bo=pANtAgAAAAABCeY!”,
“width” : 932,
“yurl” : 0
},
// …
]
“t” : “952444063”,
“topic” : {
“bitmap” : “10000000”,
“browser” : 0,
“classid” : 106,
“comment” : 1,
“cover_id” : “NDN0sggyKs3**lOg6eYgh**0ZR**AAA!”,
“createtime” : 1402661881,
“desc” : “”,
“handset” : 0,
“id” : “V13LmPKk0JLNRY”,
“is_share_album” : 0,
“lastuploadtime” : 1402662103,
“modifytime” : 1408271987,
“name” : “毕业季”,
“ownerName” : “707922098”,
“ownerUin” : “707922098”,
“pre” : “http://b171.photo.store.qq.com/p**?/V13LmPKk0JLNRY/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!/a/dIY29GUbJgAA”,
“priv” : 1,
“pypriv” : 1,
“share_album_owner” : 0,
“total” : 4,
“url” : “http://b171.photo.store.qq.com/p**?/V13LmPKk0JLNRY/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!/b/dIY29GUbJgAA”,
“viewtype” : 0
},
“totalInAlbum” : 4,
“totalInPage” : 4
}
返回的照片信息都存于photoList, 上面同样只截取了一张照片的信息,后面一部分返回的是当前相册的一些基本信息。totalInAlbum, totalInPage存储了当前相册总共包含的照片数及本次返回的照片数。而我们需要下载的图片链接则是url!
OK, 到此,所有请求和响应数据都分析清楚了,接下来便是coding的时候了。
确定爬取方案
创建qqzone类,初始化用户信息使用Selenium模拟登录获取Cookies和g_tk使用requests获取相册列表信息遍历相册,获取照片列表信息并下载照片
创建qqzone类
class qqzone(object):
“””QQ空间相册爬虫”””
def __init__(self, user):
self.username = user[‘username’]
self.password = user[‘password’]
模拟登录
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import WebDriverExceptio
# …
def _login_and_get_args(self):
“””登录QQ,获取Cookies和g_tk”””
opt = webdriver.ChromeOptions()
opt.set_headless()
driver = webdriver.Chrome(chrome_options=opt)
driver.get(‘https://i.qq.com/’)
# time.sleep(2)
logging.info(‘User {} login…’.format(self.username))
driver.switch_to.frame(‘login_frame’)
driver.find_element_by_id(‘switcher_plogin’).click()
driver.find_element_by_id(‘u’).clear()
driver.find_element_by_id(‘u’).send_keys(self.username)
driver.find_element_by_id(‘p’).clear()
driver.find_element_by_id(‘p’).send_keys(self.password)
driver.find_element_by_id(‘login_button’).click()
time.sleep(1)
driver.get(‘https://user.qzone.qq.com/{}’.format(self.username))
此处需要注意的是:
使用selenium需要安装对应的webdriver可以通过webdriver.Chrome()指定浏览器位置,否则默认从环境变量定义的路径查找如果电脑打开浏览器较慢,可能需要在driver.get后sleep几秒
获取 Cookies
使用selenium获取Cookies非常方便
self.cookies = driver.get_cookies()
获取 g_tk
获取g_tk最开始可以说是本爬虫最大的难点,因为从网页中根本找不到直接写明的数值,只有各种函数调用。为此我全局搜索,发现好多地方都有其获取方式。
g_tk
最后选择了其中一处,通过selenium执行脚本的功能成功获取到了g_tk!
self.g_tk = driver.execute_script(‘return QZONE.FP.getACSRFToken()’)
到此,selenium的使命就完成了,剩下的将通过requests来完成。
初始化 request.Session
接下来需要逐步生成请求第二获取数据。但是为方便起见,这里使用会话的方式请求数据,配置好cookie和headers,省的每次请求都设置一遍。
def _init_session(self):
self.session = requests.Session()
for cookie in self.cookies:
self.session.cookies.set(cookie[‘name’], cookie[‘value’])
self.session.headers = {
‘Referer’: ‘https://qzs.qq.com/qzone/photo/v7/page/photo.html?init=photo.v7/module/albumList/index&navBar=1’,
‘User-Agent’: ‘Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36’
}
请求相册信息
获取相册信息,需要先封装好请求参数,第二通过session.get爬取数据,再通过正则匹配以json格式读取jsonp数据,最后解析所需的name和id。
def _get_ablum_list(self):
“””获取相册的列表信息”””
album_url = ‘{}{}’.format(
‘https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/fcg_list_album_v3?’,
self._get_query_for_request())
logging.info(‘Getting ablum list id…’)
resp = self.session.get(album_url)
data = self._load_callback_data(resp)
album_list = {}
for item in data[‘data’][‘albumListModeSort’]:
album_list[item[‘name’]] = item[‘id’]
return album_list
其中的参数组合来自下面的函数_get_query_for_request函数。
def _get_query_for_request(self, topicId=None, pageStart=0, pageNum=100):
“””获取请求相册信息或照片信息所需的参数
Args:
topicId: 每个相册对应的唯一标识符
pageStart: 请求某个相册的照片列表信息所需的起始页码
pageNum: 单次请求某个相册的照片数量
Returns:
一个组合好所有请求参数的字符串
“””
query = {
‘g_tk’: self.g_tk,
‘hostUin’: self.username,
‘uin’: self.username,
‘appid’: 4,
‘inCharset’: ‘utf-8’,
‘outCharset’: ‘utf-8’,
‘source’: ‘qzone’,
‘plat’: ‘qzone’,
‘format’: ‘jsonp’
}
if topicId:
query[‘topicId’] = topicId
query[‘pageStart’] = pageStart
query[‘pageNum’] = pageNum
return ‘&’.join(‘{}={}’.format(key, val) for key, val in query.items())
其中的jsonp解析函数如下,主体部分就是一个正则匹配,非常简单。
def _load_callback_data(self, resp):
“””以json格式解析返回的jsonp数据”””
try:
resp.encoding = ‘utf-8′
data = loads(re.search(r’.*?(({.*}).*?).*’, resp.text, re.S)[1])
return data
except ValueError:
logging.error(‘Invalid input’)
解析并下载照片
获取相册列表后,逐个请求照片列表信息,进而逐一下载
def _get_photo(self, album_name, album_id):
“””获取单个相册的照片列表信息,并下载该相册所有照片”””
photo_list_url = ‘{}{}’.format(
‘https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/cgi_list_photo?’,
self._get_query_for_request(topicId=album_id))
logging.info(‘Getting photo list for album {}…’.format(album_name))
resp = self.session.get(photo_list_url)
data = self._load_callback_data(resp)
if data[‘data’][‘totalInPage’] == 0:
return None
file_dir = self.get_path(album_name)
for item in data[‘data’][‘photoList’]:
path = ‘{}/{}.jpg’.format(file_dir, item[‘name’])
logging.info(‘Downloading {}-{}’.format(album_name, item[‘name’]))
self._download_image(item[‘url’], path)
下载图片也是通过request,记得设置超时时间。
def _download_image(self, url, path):
“””下载单张照片”””
try:
resp = self.session.get(url, timeout=15)
if resp.status_code == 200:
open(path, ‘wb’).write(resp.content)
except requests.exceptions.Timeout:
logging.warning(‘get {} timeout’.format(url))
except requests.exceptions.ConnectionError as e:
logging.error(e.__str__)
finally:
pass
爬取测试
爬取过程
capturing
爬取结果
downloaded photos
写在最后
如果将请求参数中的format由jsonp改成json,则可以直接获取json数据本用例并未使用多进程或多线程,所以速率不算快,还有待优化的地方
拓展知识:
原创文章,作者:九贤生活小编,如若转载,请注明出处:http://www.wangguangwei.com/47309.html