Python爬虫(requests,Chrome的cookie文件,多线程)

爬虫的目标

  • 我的目的是爬取某直播网站某一天所有频道的网络流量数据,每一个频道有一个自己的页面,显示可视化后的流量数据,而要获取这个频道页面的url,需要先访问另一个列表页面,每一个列表页面包含了10个频道的属性信息(如channelid, starttime, endtime)。因此,爬虫过程整体分两步:
    1. 访问列表页面,通过指定日期和页数设置http请求的params。获取返回的页面内容后,保存到本地文件。
    2. 读取本地文件中保存的频道属性信息,作为访问频道页面请求的params,访问频道页面,获取返回的内容(即频道的流量数据)后,保存到本地文件

第一步:爬取列表页面

浏览器访问列表页面时怎么做的?

  • 通过chrome浏览器的DevTools(快捷键F12)中对访问列表页面的网络监控,可以知道在最终成功访问到目标页面之前,实际上先跳转到了4个别的页面(xxpreLogin, xxcaLogin, xxLogin, sendxxToken)进行登陆,最后才返回我要的访问列表的内容。以上是我在打开浏览器第一次访问该页面的过程,但后来在页面内点击转到别的页数时,只有单纯的一条访问请求了。为什么之后不用登陆认证呢?因为后来的请求中带有第一次认证后得到的cookie,服务器确认过眼神验证了cookie后,就同意给出所请求的页面数据。

我用什么方法能到达列表页面?

  • 第一个方法:模拟整个访问列表的步骤,包括登陆认证等步骤,获取最终访问列表页面时所需的信息(如cookie, token)后再访问。这是最符合浏览器思路的一个方法,但登陆步骤繁多;对于没有详细学习过HTML和JS的我来说,照猫画虎地发送请求容易,但理解对方发来的response就比较难了。另外,这4个别的页面有hppts请求,涉及到数据的加密,因此要考虑的因素很多。

  • 所以我换了一个 更粗暴直接的方法:直接查看访问列表页面的最后一步带了哪些cookie,如果我带上同样的cookie,访问同样的url,肯定也能访问成功。此处确定了我要从浏览器监控页面复制的信息:url(带params),cookie。把复制的cookie保存到本地文件里,我的程序访问时从文件中读取cookie并添加到请求的headers中,把url中的params分离出来重新构造同格式但不同值的params,baseurl/params/headers准备完毕后就可以发送请求了。

  • 写http请求时需要明确的三个基本内容:

    1. 访问该页面时实际请求的url是什么(浏览器地址栏的url通常不是请求数据所用的实际url)
    2. 访问该页面需要的请求params是什么(比如请求第几页)
    3. 访问该页面的headers要有什么(比如最重要的cookie)
  • 这里我使用了基本的requests库,代码如下:

    from urllib import parse
    from urllib.parse import urlsplit, parse_qs
    import requests
    
    # 读取cookie
    cookie_filename = 'cookies.txt'
    cookiefile = open(cookie_filename, 'r')
    cookies = {}
    for line in cookiefile.read().split(';'):
        name, value = line.strip().split('=',1)
        cookies[name] = value
    cookiefile.close()
    
    # 访问列表的start_page到end_page页
    url = 'http://xxxx.xxxxxx.com/xxx/xxxx.do?' \
          'startTime=xxxx&endTime=xxxx¤tPage=xxxx&pt=xxxx'
    start_page, end_page = 1, 1000
    for page in range(start_page, end_page):
        baseurl = parse.splitquery(url)[0]
        params = parse_qs(urlsplit(url).query)
        params['currentPage'] = page
        params['startTime'] = '2018-01-01 00:00:00'
        params['endTime'] = '2018-01-01 23:59:59'
        response = requests.get(baseurl, params=params, cookies=cookies)
        content = response.content.decode()
        pagefile = 'page' + str(page) + '.txt'
        with open(pagefile, 'w', encoding='utf-8') as pfile:
            pfile.write(content)
    
  • 像上面说的一样,先复制下来url到代码中,再复制cookies到文件中,这个代码就能成功运行了。 BUT,好景不长,这段代码在成功运行了大约不到十分钟后,突然取不到数据而是取回了像初次访问一样的登陆认证页面。经过一番分析,我发现目标页面的url中有一个param实际是跟时间有关的(指上面代码中的pt),cookies中也有类似服务器端的心跳时间cookie,多半是服务器看我带的这个心跳cookie太老了不在它可以回答的时间区间内,所以返回让我重新认证。另外,浏览器每次发出请求(哪怕是请求同样的页面)时有一个cookie值都会随机改变,目前分析应该是和证书有关。所以,虽然用同样的一对pt和cookie大约能持续几分钟的成功访问,但如果要在崩了以后还能继续访问,只是更新pt到最新时间经尝试是不行的。好在我要访问的列表页面总页数不是很多,所以我将代码稍加修改,记录每次崩时访问到哪一页了,然后用浏览器再访问一次,复制新的url和cookies,从断掉的地方接着跑。

  • 改动后的代码如下:

    from urllib import parse
    from urllib.parse import urlsplit, parse_qs
    import requests
    
    # 读取cookie
    cookie_filename = 'cookies.txt'
    cookiefile = open(cookie_filename, 'r')
    cookies = {}
    for line in cookiefile.read().split(';'):
        name, value = line.strip().split('=',1)
        cookies[name] = value
    cookiefile.close()
    
    # 访问列表的start_page到end_page页
    url = 'http://xxxx.xxxxxx.com/xxx/xxxx.do?' \
          'startTime=xxxx&endTime=xxxx¤tPage=xxxx&pt=xxxx'
    start_page, end_page = 1, 1000
    for page in range(start_page, end_page):
        baseurl = parse.splitquery(url)[0]
        params = parse_qs(urlsplit(url).query)
        params['currentPage'] = page
        params['startTime'] = '2018-01-01 00:00:00'
        params['endTime'] = '2018-01-01 23:59:59'
        response = requests.get(baseurl, params=params, cookies=cookies)
        # 如果返回的是登陆认证页面的地址,记录访问到哪一页了,退出循环
        resurl = parse.splitquery(response.url)[0]
        if resurl == 'https://xxxLogin.xxxx.xxx':
            print('Failed at page '+str(page)+', please restart.')
            break
        # 成功访问时,保存数据
        content = response.content.decode()
        pagefile = 'page' + str(page) + '.txt'
        with open(pagefile, 'w', encoding='utf-8') as pfile:
            pfile.write(content)
    
  • 到此可以爬下来所有列表页面的内容了,很明显,每十分钟人工刷新浏览器再复制粘贴一次是非常耗时的,这样的代码在后面爬取每个频道的流量信息时是不可取的(加载一页列表大约1-3秒,而加载一页频道流量信息可能要10+秒),不过对于非时间敏感型的请求来说,上面的代码是足够用了。

  • 本着能少一次复制就少一次的原则,我希望每次只复制url,而cookie的信息肯定能从浏览器(这里用的chrome)的本地缓存文件中读,就不需要我花费宝贵的时间复制到我准备的文件里了。因此我查了Chrome浏览器在系统中保存cookie的位置,修改代码,使 每次开始爬取时直接读取浏览器的cookie

  • 改动后的代码如下:

    from urllib import parse
    from urllib.parse import urlsplit, parse_qs
    import requests
    import sqlite3
    from win32crypt import CryptUnprotectData
    
    # 从Chrome的cookie文件读取cookie
    cookie_path = r'C:\Users\ann\AppData\Local\Google\Chrome\User Data\Default\Cookies'
    host = '.xxx.com'
    sql = 'select host_key, name, encrypted_value from cookies '\
          'where host_key = \'' + host + '\''
    with sqlite3.connect(cookie_path) as conn:
        cu = conn.cursor()
        cookies = {name:CryptUnprotectData(encrypted_value)[1].decode() '\
                  'for host_key, name, encrypted_value in cu,execute(sql).fetchall()}
    
    # 访问列表的start_page到end_page页
    url = 'http://xxxx.xxxxxx.com/xxx/xxxx.do?' \
          'startTime=xxxx&endTime=xxxx¤tPage=xxxx&pt=xxxx'
    start_page, end_page = 1, 1000
    for page in range(start_page, end_page):
        baseurl = parse.splitquery(url)[0]
        params = parse_qs(urlsplit(url).query)
        params['currentPage'] = page
        params['startTime'] = '2018-01-01 00:00:00'
        params['endTime'] = '2018-01-01 23:59:59'
        response = requests.get(baseurl, params=params, cookies=cookies)
        # 如果返回的是登陆认证页面的地址,记录访问到哪一页了,退出循环
        resurl = parse.splitquery(response.url)[0]
        if resurl == 'https://xxxLogin.xxxx.xxx':
            print('Failed at page '+str(page)+', please restart.')
            break
        # 成功访问时,保存数据
        content = response.content.decode()
        pagefile = 'page' + str(page) + '.txt'
        with open(pagefile, 'w', encoding='utf-8') as pfile:
            pfile.write(content)
    
  • 至此,每当访问崩了,我还是得手动用浏览器访问一次,复制url,从断掉的地方继续跑。显然,即使不用复制cookies了,这个代码还是不能满足爬取频道流量的大量数据的要求。

  • 因此,爬取频道流量信息时我用了另一种思路进行访问,不用cookie信息,解放了双手,实现了全自动爬取。不过,要访问频道流量信息页面,还得先知道频道的一些属性(比如channelId, startTime, endTime),把属性写进params,才能获得正确的频道页面,因此,下一步我们先从爬下来的列表页面中提取频道属性信息。

第二步:从列表页面文件获取频道属性信息

  • 从浏览器访问的列表页面来看,一页列表有10条频道属性信息,因此我到保存的列表页面文件中观察(文件里保存的直接是每个频道的数据,不是这一页的html代码),每条频道信息开头都有'"channelhead"'字样(没错带双引号,这里channelhead不是真实信息只是我举个例子),每条频道的属性包括type, channelId, startTime, endTime等。这里我只需要type=1的频道的信息,因此读文件时需要加判断条件。
  • 这里我结合了python的find函数和简单的正则表达式进行提取,代码如下:
    import re
    
    start_page, end_page = 1, 1000
    query_str = ['channelId startTime endTime']
    
    for page in range(start_page, end_page):
        # 读出第page页的列表页面文件
        pagefile = 'page' + str(page) + '.txt'
        pfile = open(pagefile, 'r', encoding='utf-8')
        data = pfile.read()
        pfile.close()
        # 从列表页面数据有选择地提取频道属性
        query_count = data.count('"allowVideo"')
        index1 = data.find('"allowVideo"')
        for i in range(query_count-1):
            # 找到一条频道信息的区间[index1, index2]
            index2 = data.find('"allowVideo"', index1+1)
            query_data = data[index1:index2]
            if query_data.find('"type":"1"') > 0:
                # channelId:先用find找到channelid位置,再用正则匹配提取区间内的数字
                index_chid = query_data.find('channelId')
                chid = re.sub(r'\D', '', query_data[index_chid, index_chid+30])
                # startTime
                index_st = query_data.find('startTime')
                st = query_data[index_st+12, index_st+19]
                # endTime
                index_et = query_data.find('endTime')
                et = query_data[index_et+10, index_et+19]
                str = chid + ' ' + st + ' ' + et
                query_str.append(str)
            index1 = index2
    
    # 把所有的query存入另一个文件
    # (实际上我得到的query的属性远不止这些,因此是分很多文件存的,
    # 由于文件读写操作不是此次爬虫的重点,所以这里不细讲)
    query_filename = 'query.txt'
    with open(query_filename, 'w', encoding='utf-8') as qfile:
        for item in query_str:
            qfile.write(item + '\n')
    
  • 到此我提取了所有需要的频道属性的信息,只待拿着这些信息去请求频道网络流量页面了

第三步:获取每个频道网络流量页面

  • 第一步的方法,虽然能绕过登陆认证的步骤,也能自动从Chrome的本地cookie文件中提取我要的cookie,但每隔几分钟我还是得手动刷新浏览器并复制其url粘贴到代码中,整个操作非常笨拙耗时效率低下。于是我又换了一个思路:有什么方法能 直接调用Chrome浏览器来访问 吗?我不想知道它访问的细节,只要给我返回的最终结果就好。于是我发现了 selenium库的webdriver 方法。

  • 调用webdriver.Chrome()会生成一个浏览器对象,浏览器对象的.get(url)方法就能使浏览器自己带上合适的cookie并返回相应页面,浏览器的.page_source属性就是页面的内容啦。

  • 使用selenium库的webdriver调用Chrome浏览器访问频道页面并存入文件的代码如下:

    from selenium import webdriver
    import time
    form urllib import parse
    import os
    
    baseurl = 'http://xxx.xxxx.xxx/xxx.do'
    # query文件的前缀,每个query文件都有多条channel的属性信息
    query_fileadd = r'D:\xxx\query'
    # 每个query文件对应一个装channelpage的文件夹
    channelpage_fileadd = r'D:\xxx\ChannelPages\Query'
    
    start, end = 1, 10
    
    # 初始化一个Chrome的driver
    browser = webdriver.Chrome()
    for i in range(start, end+1):
        # 读取第i个query文件的所有queries
        query_filename = query_fileadd + str(i) + '.txt'
        qfile = open(query_filename, 'r')
        query_list = []
        for line in qfile:
            query_list.append(line)
        qfile.close()
        # 确保创建了channelpage的路径
        cpdir = os.path.join(channelpage_fileadd, str(i))
        if not os.path.exists(cpdir):
            os.mkdir(cpdir)
        # 根据query_list发送http请求获取channel page
        for i in range(len(query_list))
            # url的拼接
            q = query_list[i].split(' ')
            chid, st, et = q[0], q[1], q[2]
            pt = str(int(time.time()))
            params = dict(startTime=st, endTime=et, channelId=chid, pt=pt)
            url = baseurl + '?' + parse.urlencode(query=params)
            # 获取channel page的数据
            browser.get(url)
            channelpagedata = browser.page_source
            # 写入channel文件
            cpfname = os.path.join(cpdir, 'channel'+str(i)+'.txt')
            with open(cpfname, 'w', encoding='utf-8') as cf:
                cf.write(channelpagedata)
    browser.quit()
    
    • 上面的代码实现了一键访问下载频道页面的目标,解放了双手,非常开心!然而过了几分钟之后,我眉头一皱,发现 事情并没有这么简单
    • 之前提到过,浏览器访问一个频道页面大约要10+秒,导致上面调用浏览器的代码下载非常龟速,那么如何能让下载速度提高呢? 多线程。这里我需要的数据没有下载先后的要求,存数据的文件也是每个频道有独立的文件,所以甚至连锁也用不上。
    • 每个线程负责的任务:这里我有多个query的文件,每个文件有多条channel的属性,每条channel属性对应访问一次channel页面。假设query文件一共50个,每个文件有1000条属性,简单来分配的话,每条线程负责10个query文件共5条线程就行(如果内存容量比较大,那可以更多条,速度会相对更快)。
    • 这里多线程我使用的 threading 库,多线程访问并存储的代码如下:
    from selenium import webdriver
    from urllib import parse
    import os
    import threading
    
    def getChannelPages(startpage, endpage, query_fileadd, channelpage_fileadd, baseurl, params):
        browser = webdriver.Chrome()
        for i in range(startpage, endpage, 100):
            # 读取文件的query存入query_list
            qfname = query_fileadd + str(i) + '.txt'
            qfile = open(qfname)
            query_list = []
            for line in qfile:
                query_list.append(line)
            qfile.close()
            # 确保创建了存channel page的路径
            cpdir = os.path.join(channelpage_fileadd, str(i))
            if not os.path.exists(cpdir):
                os.mkdir(cpdir)
            # 根据query_list发送http请求获取channel page
            for index in range(len(query_list)):
                # url的拼接
                query = query_list[index].split(' ')
                chid, st, et = query[0], query[1], query[2]
                pt = str(int(time.time()))
                params['startTime'] = st
                params['endTime'] = et
                params['channelId'] = chid
                url = baseurl + '?' + parse.urlencode(query=params)
                # 获取channel page数据
                browser.get(url)
                channelpagedata = browser.page_source
                # 写入channel文件
                cpfadd = os.path.join(cpdir, 'channel' + str(index) + '.txt')
                with open(cpfadd, 'w', encoding='utf-8') as cf:
                    cf.write(channelpagedata)
        browser.quit()
    
    if __name__ == '__main__':
        query_fileadd = r'D:\xxx\query'
        channelpage_fileadd = r'D\xxx\ChannelPages'
        # channel页面参数设置: baseurl, params
        baseurl = 'http://xxxx.xxxx.xxx/xxx.do'
        params = dict(startTime='',endTime='',channelId='')
        threads = []
        # 初始化各线程
        startpage, endpage = 1, 1000
        for i in range(startpage, endpage, 200):
            t = threading.Thread(target=getChannelPages, args=(i, i+199, query_fileadd, baseurl, params))
            threads.append(t)
        # 开始表演
        for t in threads:
            t.start()
        for t in threads:
            t.join()
    
    • 目前代码实现了多线程爬虫的功能,大幅提高了爬虫的速度,不过还有很多可以优化的部分(比如换一种线程负责的功能,或者改成多进程,还有异步,除了线程以外,对频道属性的提取可以用更高效的正则表达式而不是find函数)。

    完整代码

    • 下面是爬取列表页,提取query,爬取频道页的完整代码,其中爬取列表页也改成多线程方式了。
    from selenium import webdriver
    from urllib import parse
    import time
    import re
    import os
    import threading
    
    def getHistoryPages(startpage, endpage, listpage_fileadd, baseurl, params):
        # 访问第startpage页到第endpage页的列表页面,每100页存入一个文件
        browser = webdriver.Chrome()
        for bigpage in range(startpage, endpage, 100):
            if bigpage+99 > endpage:
                end = endpage
            else:
                end = bigpage + 99
            page_data = []
            for page in range(bigpage, end+1):
                params['currentPage'] = str(page)
                params['pt'] = str(int(time.time))
                url = baseurl + '?' + parse.urlencode(query=params)
                browser.get(url)
                page_data.append(browser.page_source)
            listpage_filename = listpage_fileadd + str(bigpage) + '-' + str(bigpage+99) + '.txt'
            lfile = open(listpage_filename, 'w', encoding='utf-8')
            for item in page_data:
                lfile.write(item+'\n')
            lfile.close()
            del page_data
        browser.quit()
    
    def extractQueries(listpage_filename, query_filename):
        # 把给定文件里的page data转换成query
        query_str = ['channelId startTime endTime']
        file = open(listpage_filename, 'r', encoding='utf-8')
        data = file.read()
        file.close()
        query_count = data.count('"allowVideo"')
        index1 = data.find('"allowVideo"')
        for i in range(query_count - 1):
            index2 = data.find('"allowVideo"', index1 + 1)
            query_data = data[index1:index2]
            if query_data.find('"type":"1"'):
            index_chid = query_data.find('channelId')
            chid = re.sub(r'\D', '', query_data[index_chid, index_chid+30])
            index_st = query_data.find('startTime')
            st = query_data[index_st+12, index_st+19]
            index_et = query_data.find('endTime')
            et = query_data[index_et+10, index_et+19]
            str = chid + ' ' + st + ' ' + et
            query_str.append(str)
        index1 = index2
        # 把query_str写入文件
        qfile = open(query_filename, 'w', encoding='utf-8')
        for item in query_str:
            qfile.write(item+'\n')
    
    def getChannelPages(startpage, endpage, query_fileadd, channelpage_fileadd, baseurl, params):
        browser = webdriver.Chrome()
        for i in range(startpage, endpage, 100):
            # 读取文件的query存入query_list
            qfname = query_fileadd + str(i) + '-' + str(i+99) '.txt'
            qfile = open(qfname)
            query_list = []
            for line in qfile:
                query_list.append(line)
            qfile.close()
            # 确保创建了存channel page的路径
            cpdir = os.path.join(channelpage_fileadd, str(i) + '-' + str(i+99))
            if not os.path.exists(cpdir):
                os.mkdir(cpdir)
            # 根据query_list发送http请求获取channel page
            for index in range(len(query_list)):
                # url的拼接
                query = query_list[index].split(' ')
                chid, st, et = query[0], query[1], query[2]
                pt = str(int(time.time()))
                params['startTime'] = st
                params['endTime'] = et
                params['channelId'] = chid
                url = baseurl + '?' + parse.urlencode(query=params)
                # 获取channel page数据
                browser.get(url)
                channelpagedata = browser.page_source
                # 写入channel文件
                cpfadd = os.path.join(cpdir, 'channel' + str(index) + '.txt')
                with open(cpfadd, 'w', encoding='utf-8') as cf:
                    cf.write(channelpagedata)
        browser.quit()
    
    if __name__ == '__main__':
    
        listpage_fileadd = r'D:\xxx\xxx\page'
        query_fileadd = r'D:\xxx\query'
        channelpage_fileadd = r'D\xxx\ChannelPages'
    
        # 列表页面参数设置: baseurl, params
        baseurl = 'http://xxxx.xxxx.xxx/xxx.do'
        params = dict(startTime='2018-01-01 00:00:00', endTime='2018-01-01 00:00:00')
        startpage, endpage = 1, 1000
        threads = []
        for page in range(startpage, endpage, 200):
            t = threading.Thread(target=getHistoryPages, args=(page, page+199, listpage_fileadd, baseurl, params))
            threads.append(t)
        for t in threads:
            t.start()
        for t in threads:
            t.join()
    
        # 从channel list文件中提取channel的属性
        startpage, endpage = 1, 1000
        for listpage in range(startpage, endpage, 100):
            listpage_filename = listpage_fileadd + str(listpage) + '-' + str(listpage+99) + '.txt'
            query_filename = query_fileadd + str(listpage) + '-' + str(listpage+99) + '.txt'
            extractQueries(listpage_filename, query_filename)
    
        # channel页面参数设置: baseurl, params
        baseurl = 'http://xxxx.xxxx.xxx/xxx.do'
        params = dict(startTime='',endTime='',channelId='')
        threads = []
        startpage, endpage = 1, 1000
        for i in range(startpage, endpage, 200):
            t = threading.Thread(target=getChannelPages, args=(i, i+199, query_fileadd, baseurl, params))
            threads.append(t)
        for t in threads:
            t.start()
        for t in threads:
            t.join()
    
    

    后续

    • 爬下来这么多数据,肯定不能存在同一个电脑里,因此之后还经历了设置ftp server等步骤对爬下来的数据进行转移,这里不细讲了,网上有很多教程。

你可能感兴趣的:(Python爬虫(requests,Chrome的cookie文件,多线程))