博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Python网络爬虫5 - 爬取QQ空间相册
阅读量:7230 次
发布时间:2019-06-29

本文共 11247 字,大约阅读时间需要 37 分钟。

自毕业后,就再也没有用过QQ,QQ空间里记录的是些并不精彩的青葱岁月,但好歹也是份回忆,近日想着学以致用,用Python把QQ空间相册的所有照片爬取下来,以作备份。

分析QQ空间

登录QQ空间

爬取第一步,分析站点,首先需要知道如何登录QQ空间。最初想法是用requests库配置登录请求,模拟登录,但是不久便放弃了这一思路,请看下图↓

根据登录按钮绑定的监听事件可以追踪到该按钮的点击事件如下:

账号加密是必然的,但这一堆堆的代码真心不好解析,有耐心的勇士尽情一试!

在排除这种登录方法后,选择selenium模拟用户登录不失为省时省力的方法,而且我们只是需要通过selenium完成登录,获取到Cookies和后面讲述的g_tk参数后,就可以停用了,所以效率并不太低。

分析空间相册

登录以后,页面会跳转至 [https://user.qzone.qq.com/{QQ_NUMBER}](javascript:;), 这时把鼠标移到导航栏你会发现,所有的导航栏链接都是javascript:; ?。没错就是这么坑,一切都是暗箱操作。

当然这并不难处理,使用调试工具捕获点击后产生的请求,然后过滤出正确的请求包即可。因为网络包非常多,那么怎么过滤呢,猜想相册数据的API必然会返回个列表list,尝试过滤list然后逐个排除,最后定位到请求包。下面是通过fcg_list过滤后的数据包,列表信息以jsonp格式返回,稍作处理即可当做json格式来读取(后面有讲)。

HeadersResponse可以分别获取到两组重要信息:

  1. request 获取相册列表所需的请求信息,包括请求链接和参数
  2. response 数据包包含的所有相册的信息,是每个相册所含照片对应的请求包参数的数据来源

先看请求包:

# urlhttps://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/fcg_list_album_v3# argsg_tk: 477819917callback: shine0_Callbackt: 691481346hostUin: 123456789uin: 123456789appid: 4inCharset: utf-8outCharset: utf-8source: qzoneplat: qzoneformat: jsonpnotice: 0filter: 1handset: 4pageNumModeSort: 40pageNumModeClass: 15needUserInfo: 1idcNum: 4callbackFun: 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过滤数据包,找到该相册的照片信息

同样的道理,根据数据包可以获取照片列表信息的请求包和响应信息,先看请求:

# urlhttps://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/cgi_list_photo# argsg_tk: 477819917callback: shine0_Callbackt: 952444063mode: 0idcNum: 4hostUin: 123456789topicId: V13LmPKk0JLNRYnoTopic: 0uin: 123456789pageStart: 0pageNum: 30skipCmtCount: 0singleurl: 1batchId: notice: 0appid: 4inCharset: utf-8outCharset: utf-8source: qzoneplat: qzoneoutstyle: jsonformat: jsonpjson_esc: 1question: answer: callbackFun: shine0_: 1551790719497复制代码

其中有几个关键参数:

  1. g_tk - 与相册列表参数一致
  2. topicId - 与相册列表参数中的id一致
  3. pageStart - 本次请求照片的起始编号
  4. pageNum - 本次请求的照片数量

为了一次性获取所有照片,可以将pageStart设为0,pageNum设为所有相册所含照片的最大值。

同样可以对上面的参数进行简化,在相册列表请求参数的基础上添加topicIdpageStartpageNum三个参数即可。

下面来看返回的照片列表信息:

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, 上面同样只截取了一张照片的信息,后面一部分返回的是当前相册的一些基本信息。totalInAlbumtotalInPage存储了当前相册总共包含的照片数及本次返回的照片数。而我们需要下载的图片链接则是url

OK, 到此,所有请求和响应数据都分析清楚了,接下来便是coding的时候了。

确定爬取方案

  1. 创建qqzone类,初始化用户信息
  2. 使用Selenium模拟登录
  3. 获取Cookiesg_tk
  4. 使用requests获取相册列表信息
  5. 遍历相册,获取照片列表信息并下载照片

创建qqzone类

class qqzone(object):    """QQ空间相册爬虫"""    def __init__(self, user):        self.username = user['username']        self.password = user['password']复制代码

模拟登录

from selenium import webdriverfrom selenium.webdriver.common.keys import Keysfrom 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))复制代码

此处需要注意的是:

  1. 使用selenium需要安装对应的webdriver
  2. 可以通过webdriver.Chrome()指定浏览器位置,否则默认从环境变量定义的路径查找
  3. 如果电脑打开浏览器较慢,可能需要在driver.getsleep几秒

获取 Cookies

使用selenium获取Cookies非常方便

self.cookies = driver.get_cookies()复制代码

获取 g_tk

获取g_tk最开始可以说是本爬虫最大的难点,因为从网页中根本找不到直接写明的数值,只有各种函数调用。为此我全局搜索,发现好多地方都有其获取方式。

最后选择了其中一处,通过selenium执行脚本的功能成功获取到了g_tk

self.g_tk = driver.execute_script('return QZONE.FP.getACSRFToken()')复制代码

到此,selenium的使命就完成了,剩下的将通过requests来完成。

初始化 request.Session

接下来需要逐步生成请求然后获取数据。但是为方便起见,这里使用会话的方式请求数据,配置好cookieheaders,省的每次请求都设置一遍。

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数据,最后解析所需的nameid

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复制代码

爬取测试

  • 爬取过程

  • 爬取结果

写在最后

  1. 如果将请求参数中的formatjsonp改成json,则可以直接获取json数据
  2. 本用例并未使用多进程或多线程,所以速率不算快,还有待优化的地方
  3. 该爬虫已存放至开源项目,欢迎交流

本文首发于

转载地址:http://izcfm.baihongyu.com/

你可能感兴趣的文章
windows下kangle虚拟主机-架设java空间的教程及心得
查看>>
Discuz! X2.5:文件目录结构
查看>>
我的友情链接
查看>>
TCP/IP协议及首部初了解
查看>>
防火墙iptables
查看>>
CUDA搭建
查看>>
memcached与PostgreSQL缓存命中机制
查看>>
百度地图路线检索(3)
查看>>
linux netstat 命令详解
查看>>
对前几篇blog的环境等的补充说明
查看>>
Curl命令使用解析大全
查看>>
MySQL日期函数
查看>>
【00】Effective Java
查看>>
.NET重构—单元测试重构
查看>>
SMB简介sabma服务(一)
查看>>
ANT简明教程
查看>>
Eclipse Luna WTP 与 Tomcat 8 的整合存在一个很头疼的 Bug
查看>>
小数在计算机里面的存放
查看>>
数据结构中的各种树简单解释
查看>>
我的朗科运维第七课
查看>>