自畢業後,就再也沒有用過QQ,QQ空間裏記錄的是些並不精彩的青蔥歲月,但好歹也是份回憶,近日想着學以至用,用Python
把QQ空間相冊的全部照片爬取下來,以做備份。javascript
爬取第一步,分析站點,首先須要知道如何登陸QQ空間。最初想法是用requests
庫配置登陸請求,模擬登陸,可是不久便放棄了這一思路,請看下圖↓html
根據登陸按鈕綁定的監聽事件能夠追蹤到該按鈕的點擊事件以下:java
帳號加密是必然的,但這一堆堆的代碼真心很差解析,有耐心的勇士盡情一試!python
在排除這種登陸方法後,選擇selenium
模擬用戶登陸不失爲省時省力的方法,並且咱們只是須要經過selenium
完成登陸,獲取到Cookies
和後面講述的g_tk
參數後,就能夠停用了,因此效率並不過低。git
登陸之後,頁面會跳轉至 [https://user.qzone.qq.com/{QQ_NUMBER}](javascript:;), 這時把鼠標移到導航欄你會發現,全部的導航欄連接都是javascript:;
😳。沒錯就是這麼坑,一切都是暗箱操做。github
固然這並不難處理,使用調試工具捕獲點擊後產生的請求,而後過濾出正確的請求包便可。由於網絡包很是多,那麼怎麼過濾呢,猜測相冊數據的API必然會返回個列表list
,嘗試過濾list
而後逐個排除,最後定位到請求包。下面是經過fcg_list
過濾後的數據包,列表信息以jsonp
格式返回,稍做處理便可當作json
格式來讀取(後面有講)。web
從Headers
和Response
能夠分別獲取到兩組重要信息:chrome
request
獲取相冊列表所需的請求信息,包括請求連接和參數response
數據包包含的全部相冊的信息,是每一個相冊所含照片對應的請求包參數的數據來源先看請求包:json
# 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\/psb?\/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
過濾數據包,找到該相冊的照片信息
一樣的道理,根據數據包能夠獲取照片列表信息的請求包和響應信息,先看請求:
# 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" : "",
"lensModel" : "",
"make" : "",
"meteringMode" : "",
"model" : "",
"originalTime" : ""
},
"forum" : 0,
"frameno" : 0,
"height" : 621,
"id" : 0,
"is_video" : false,
"is_weixin_mode" : 0,
"ismultiup" : 0,
"lloc" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!",
"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\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!\/a\/dIY29GUbJgAA&bo=pANtAgAAAAABCeY!",
"raw" : "http:\/\/r.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!\/r\/dIY29GUbJgAA",
"raw_upload" : 1,
"rawshoottime" : 0,
"shoottime" : 0,
"shorturl" : "",
"sloc" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!",
"tag" : "",
"uploadtime" : "2014-06-13 20:21:33",
"url" : "http:\/\/b171.photo.store.qq.com\/psb?\/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" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!",
"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\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!\/a\/dIY29GUbJgAA",
"priv" : 1,
"pypriv" : 1,
"share_album_owner" : 0,
"total" : 4,
"url" : "http:\/\/b171.photo.store.qq.com\/psb?\/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
獲取相冊列表信息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
幾秒使用selenium
獲取Cookies
很是方便
self.cookies = driver.get_cookies()
複製代碼
獲取g_tk
最開始能夠說是本爬蟲最大的難點,由於從網頁中根本找不到直接寫明的數值,只有各類函數調用。爲此我全局搜索,發現好多地方都有其獲取方式。
最後選擇了其中一處,經過selenium
執行腳本的功能成功獲取到了g_tk
!
self.g_tk = driver.execute_script('return QZONE.FP.getACSRFToken()')
複製代碼
到此,selenium
的使命就完成了,剩下的將經過requests
來完成。
接下來須要逐步生成請求而後獲取數據。可是爲方便起見,這裏使用會話的方式請求數據,配置好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
複製代碼
format
由jsonp
改爲json
,則能夠直接獲取json
數據本文首發於www.litreily.top