美文网首页
Python爬虫(requests,Chrome的cookie文

Python爬虫(requests,Chrome的cookie文

作者: 酸辣粉多加辣椒 | 来源:发表于2018-07-02 22:33 被阅读0次

    爬虫的目标

    • 我的目的是爬取某直播网站某一天所有频道的网络流量数据,每一个频道有一个自己的页面,显示可视化后的流量数据,而要获取这个频道页面的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&currentPage=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&currentPage=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&currentPage=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文

          本文链接:https://www.haomeiwen.com/subject/kweruftx.html