当前位置:   article > 正文

[Python 爬虫] 使用 Scrapy 爬取新浪微博用户信息(四) —— 应对反爬技术(选取 User-Agent、添加 IP代理池以及Cookies池 )__ga 反爬

_ga 反爬

上一篇:[Python 爬虫] 使用 Scrapy 爬取新浪微博用户信息(三) —— 数据的持久化——使用MongoDB存储爬取的数据

最近项目有些忙,很多需求紧急上线,所以一直没能完善《 使用 Scrapy 爬取新浪微博用户信息》这一系列的博客,今天好不容易闲下来,就完成这一系列最后一节:选取 User-Agent、添加 IP代理池以及Cookies池。在上一篇博客中,我们介绍了如何对爬取的用户信息进行持久化处理,存入了 MongDB,但是并没有限制爬取速度,导致爬虫程序频繁出现 418 响应码,这是微博反爬的一种策略,这一篇博客我们就来介绍如何应对目标网页的反爬程序,需要注意的是,微博反爬策略是针对用户的,在只用单用户的情况下,只能降低爬取频率,当然,如果手里有一批账号,可以采用多账号的Cookies池,当出现 418 请求时,就切换 Cookies,由于我目前只有一个账号,因此我只能通过降低爬取频率来应对新浪微博的反爬策略,但是我会用单个账号获取多个 Cookies 来模拟多账户情况下的Cookies池。

 

选取 User-Agent

User Agent 中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。(数据来自:百度百科 User-Agent

User-Agent 可以通过浏览器调试模式,然后选择 Network,任意查看一个连接,就能找到,如下图:

在这里我们通过上网查找了几个 User-Agent,添加到 settings.py 文件内,代码如下:

  1. # User-Agent 列表,提供随机 User-Agent
  2. USER_AGENT_LIST = [
  3. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
  4. "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
  5. "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
  6. "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
  7. "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
  8. "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
  9. "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
  10. "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
  11. "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
  12. "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
  13. "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
  14. "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
  15. "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
  16. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
  17. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
  18. "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
  19. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11",
  20. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
  21. "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER)",
  22. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)",
  23. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.84 Safari/535.11 LBBROWSER",
  24. "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
  25. "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)",
  26. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
  27. "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SV1; QQDownload 732; .NET4.0C; .NET4.0E; 360SE)",
  28. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
  29. "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
  30. "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
  31. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
  32. "Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; zh-cn) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
  33. "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b13pre) Gecko/20110307 Firefox/4.0b13pre",
  34. "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0",
  35. "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11",
  36. "Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10",
  37. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
  38. ]

添加了 User-Agent 列表之后,我们在 middleware.py 中使用它,我们在 middleware.py  定义一个名为 RandomUserAgentMiddleware 的类,该类继承了 UserAgentMiddleware 类,在开始构造一个爬虫请求前,会调用 RandomUserAgentMiddleware 类的 from_crawler() 方法,构造请求后,发送请求前,将会执行 RandomUserAgentMiddleware  类 的 from_crawler() 方法,代码如下:

  1. from scrapy.http.headers import Headers
  2. from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
  3. class RandomUserAgentMiddleware(UserAgentMiddleware):
  4. """
  5. 随机选取 代理(User-Agent)
  6. """
  7. def __init__(self, user_agent):
  8. self.user_agent = user_agent
  9. self.headers = Headers()
  10. @classmethod
  11. def from_crawler(cls, crawler):
  12. """
  13. 开始构造请求前执行的方法\n
  14. :param crawler:整个爬虫的全局对象\n
  15. :return:
  16. """
  17. # 从配置里获取 用户代理(User-Agent) 列表
  18. return cls(user_agent=crawler.settings.get('USER_AGENT_LIST'))
  19. def process_request(self, request, spider):
  20. """
  21. 发送请求前执行的方法\n
  22. :param request:请求\n
  23. :param spider:爬虫应用\n
  24. :return:
  25. """
  26. # 从 代理 列表中随机选取一个 代理
  27. agent = random.choice(self.user_agent)
  28. print('当前 User-Agent :', agent)
  29. self.headers['User-Agent'] = agent
  30. request.headers = self.headers

目前,我们定义了在中间件(middleware)中定义了随机选取 User-Agent 的类,但是如果要使用该类,还得在 settings.py 中启用该中间件,并设置优先级,代码如下:

  1. DOWNLOADER_MIDDLEWARES = {
  2. 'sina_scrapy.middlewares.RandomUserAgentMiddleware': 10
  3. }

好了,现在打开控制台(Console)使用 scrapy crawl sina_user 命令启动爬虫,可以看到输出如下的日志信息,每次爬虫请求使用的 User-Agent 都是从 USER_AGENT_LIST 中随机获取的,如下图所示:

 

添加 IP代理池

在反爬技术中,很多目标网页会记录访问者的 ip,并通过计算单位时间内同一 ip 访问网站的次数,如果次数过高,则被视为爬虫程序,然后服务器将拦截该请求。对此,不难想到,采取使用多个 ip 来访问目标网站,可以有效的应对这种反爬机制。对于 ip 资源的来源,目前网上有很多代理,提供了很多可用的 ip,例如:西刺代理快代理等。需要注意的是,代理网站提供的 ip 是有时效的,因此,我们需要动态地获取代理 ip,在这里,我只是采用最简单地爬虫爬取代理网站的 ip,我们并不能保证所有获取的 ip 的都是可用的,因此还需要增加校验机制,确定获取的 ip 是否有效,最直接的办法就是利用 ping 命令,去 ping ip,看该 ip 是否能够 ping 通,为此,我们新建一个包,名为 utils,然后在该包下新建 crawl_proxy.py 脚本,脚本如下:

  1. # -*-* encoding:UTF-8 -*-
  2. # author : mengy
  3. # date : 2019/9/1
  4. # python-version : Python 3.7.0
  5. # description :
  6. import re, subprocess as sp, time, json
  7. from urllib import request
  8. from bs4 import BeautifulSoup
  9. from sina_scrapy.utils.cache_utils import Cache
  10. from sina_scrapy.utils.thread_pool import ThreadPool
  11. executor = ThreadPool()
  12. cache = Cache()
  13. # 西刺代理 URL
  14. PROXY_IP_XICI_URL = 'https://www.xicidaili.com/nn/%s'
  15. # 快代理 URL
  16. PROXY_IP_QUICK_URL = 'https://www.kuaidaili.com/free/inha/%s/'
  17. # 模拟请求头
  18. PROXY_IP_XICI_HEADERS = {
  19. 'Host': 'www.xicidaili.com',
  20. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
  21. 'Connection': 'keep-alive',
  22. 'Upgrade-Insecure-Requests': '1',
  23. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36',
  24. 'Accept - Encoding': 'gzip, deflate, br',
  25. 'Accept - Language': 'zh - CN, zh;q = 0.9, en;q = 0.8',
  26. 'Cookie': '_free_proxy_session = BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiJTBhMGNlZjVlYjdjNDU5NjY3ZDNlOGU0YmQ4NTU0OTBhBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMVZpMzIrOVV3aFp5cnJXR3hTVUtFRy9ud0MxMGtyY2R3WjJzMjltSFNSeEE9BjsARg % 3D % 3D - -55779e702f4e95b04fa84eafbb70ccb4006cd839;Hm_lvt_0cf76c77469e965d2957f0553e6ecf59 = 1558427855, 1558427893, 1558427898, 1558427901;Hm_lpvt_0cf76c77469e965d2957f0553e6ecf59 = 1558428119'
  27. }
  28. PROXY_IP_QUICK_HEADERS = {
  29. 'Host': 'www.kuaidaili.com',
  30. 'Connection': ' keep-alive',
  31. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36',
  32. 'Cookie': 'channelid=bdtg_a10_a10a1; sid=1559283308913843; _ga=GA1.2.594886518.1559283655; _gid=GA1.2.578719903.1559283655; Hm_lvt_7ed65b1cc4b810e9fd37959c9bb51b31=1559283656; Hm_lpvt_7ed65b1cc4b810e9fd37959c9bb51b31=1559283719'
  33. }
  34. # 代理ip列表在缓存中的命名
  35. PROXY_IP_NAMESPACE = 'POOL_PROXY_IPS'
  36. # 缓存中代理 ip 失效时间(s)
  37. PROXY_IP_EXPIRE = 15 * 60
  38. # ping ip 最高丢包率(%)
  39. MAX_LOST = 75
  40. # ping ip 最大延迟时间(ms)
  41. MAX_TIMEOUT = 1000
  42. def get_ips(pages=1, refresh=False):
  43. """
  44. 获取代理ip,优先从缓存取,如果缓存为空,则爬取新的代理 ip,并更新缓存\n
  45. :param refresh: 是否强制爬取\n
  46. :return:
  47. """
  48. if refresh:
  49. return crawl_quick(pages)
  50. else:
  51. # 从缓存中查询代理ip
  52. data = cache.lrange(name=PROXY_IP_NAMESPACE, start=0, end=100)
  53. if not data:
  54. print(u'缓存数据为空!开始爬取高匿代理ip')
  55. return crawl_quick(pages)
  56. else:
  57. return data
  58. def sub_thread(ip_info):
  59. """
  60. 校验 ip 是否连通\n
  61. :param ip_info:
  62. :return:
  63. """
  64. if check_ip(ip_info.get('ip')):
  65. # 将可用的 ip 放入缓存
  66. cache.lpush(PROXY_IP_NAMESPACE, json.dumps(ip_info))
  67. # 如果 ip 可用,则返回 ip 的信息
  68. return json.dumps(ip_info)
  69. else:
  70. return None
  71. def crawl_quick(page=1):
  72. """
  73. 请求 快代理 爬取高匿代理 ip\n
  74. :param page:
  75. :return:
  76. """
  77. print(u'请求 快代理 爬取高匿代理 ip')
  78. assert 1 <= page <= 10, '页数有效范围为(1 - 10)'
  79. validate_ips = []
  80. for i in range(page):
  81. req = request.Request(url=PROXY_IP_QUICK_URL % str(i + 1), headers=PROXY_IP_QUICK_HEADERS)
  82. response = request.urlopen(req)
  83. if response.status == 200:
  84. # 解析页面元素
  85. soap = BeautifulSoup(str(response.read(), encoding='utf-8'), 'lxml')
  86. ip_table = soap.select('#list > table > tbody > tr')
  87. ips = []
  88. # 获取当前页的所有 ip 信息
  89. for data in ip_table:
  90. item = data.text.split('\n')
  91. info = {}
  92. ip, port, area, proxy_type, protocol, alive_time, check_time = item[1], item[2], item[5], item[3], item[
  93. 4], '', item[7]
  94. url = str.lower(protocol) + "://" + ip + ":" + port
  95. # 将 ip 信息封装成字典
  96. info.update(ip=ip, port=port, area=area, type=proxy_type, protocol=protocol, alive_time=alive_time,
  97. check_time=check_time, url=url, add_time=int(time.time()))
  98. ips.append(info)
  99. # 遍历爬取的 ip 信息,校验 ip 是否连通
  100. tasks = [executor.submit(sub_thread, (ip_info)) for ip_info in ips]
  101. # 轮询所有完成的线程,查询线程的执行结果
  102. for task in executor.completed_tasks(tasks):
  103. data = task.result()
  104. if data:
  105. # 将线程执行结果返回
  106. validate_ips.append(data)
  107. # 降低爬取频率
  108. time.sleep(2.5)
  109. # 当还没有子线程返回可用的 ip 时,再次查询缓存
  110. if not validate_ips:
  111. validate_ips = cache.lrange(name=PROXY_IP_NAMESPACE, start=0, end=100)
  112. # 设置缓存超时时间
  113. cache.expire(name=PROXY_IP_NAMESPACE, time=PROXY_IP_EXPIRE)
  114. print(u'本次爬取 ip :%d 条,有效:%d 条' % (15 * page, len(validate_ips)))
  115. return validate_ips
  116. def check_ip(ip):
  117. """
  118. 通过 ping ip 来验证 ip 是否有效\n
  119. :param ip: 待 ping 的 ip
  120. :return:
  121. """
  122. assert ip, 'ip 不能为空!'
  123. # CMD 命令(windows)
  124. cmd = 'ping -n 4 -w 4 %s' % ip
  125. # 参数 shell 设为 true,程序将通过 shell 来执行,subprocess.PIPE 可以初始化 stdin , stdout 或 stderr 参数。表示与子进程通信的标准流
  126. p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE, shell=True)
  127. out = p.stdout.read().decode('gbk')
  128. # 丢失率
  129. lost_ratio = re.compile(u'(\d+)% 丢失', re.IGNORECASE).findall(out)
  130. # 平均耗时
  131. avg_time = re.compile(u'平均 = (\d+)', re.IGNORECASE).findall(out)
  132. # 如果失败率高于最高丢包率则丢弃
  133. if lost_ratio[0] and int(lost_ratio[0]) > MAX_LOST:
  134. print('%s 失败率过高!丢弃' % ip)
  135. return False
  136. # 如果响应时间高于最大延迟时间则丢弃
  137. if avg_time and int(avg_time[0]) > MAX_TIMEOUT:
  138. print('%s 响应时间过长,网络不稳定,丢弃' % ip)
  139. return False
  140. return True

在以上脚本中,一次性从代理网站爬取了一页的 ip,每页 15 条数据,然后将爬取的 ip 存入缓存 Redis 中,并且我们设置了休眠时间为 2.5s ,避免爬取过快,被目标服务器拦截。另外,增加了线程池,用子线程来校验 ip 是否可用,并设定丢弃丢包率大于 75% 的ip。缓存工具类 cache_utils.py 以及线程池工具类 thread_pool,都在 utils 包下,完整代码如下:

cache_utils

  1. # -*-* encoding:UTF-8 -*-
  2. # author : mengy
  3. # date : 2019/5/21
  4. # python-version : Python 3.7.0
  5. # description : Redis 缓存相关操作
  6. import redis
  7. # Redis 主机地址
  8. CACHE_HOST = '127.0.0.1'
  9. # Redis 端口
  10. CACHE_PORT = '6379'
  11. # 设置写入的键值对中的value为str类型
  12. CACHE_DECODE_RESPONSES = True
  13. class Cache(object):
  14. __pool = redis.ConnectionPool(host=CACHE_HOST, port=CACHE_PORT, decode_responses=CACHE_DECODE_RESPONSES)
  15. def __init__(self):
  16. self.__redis = redis.Redis(connection_pool=self.__pool)
  17. def delete(self, *names):
  18. """
  19. 根据name删除redis中的任意数据类型\n
  20. :param names: key或者命名空间
  21. :return:
  22. """
  23. self.__redis.delete(*names)
  24. def exists(self, name):
  25. """
  26. 检测redis的name是否存在\n
  27. :param name: key或者命名空间
  28. :return:
  29. """
  30. return self.__redis.exists(name)
  31. def keys(self, pattern='*'):
  32. """
  33. 根据* ?等通配符匹配获取redis的name\n
  34. :param pattern: 通配符
  35. :return:
  36. """
  37. return self.__redis.keys(pattern)
  38. def expire(self, name, time):
  39. """
  40. 为某个name设置超时时间\n
  41. :param name: key或者命名空间\n
  42. :param time: 超时时间(s)
  43. :return:
  44. """
  45. if not self.exists(name):
  46. raise Exception(name + ' 不存在')
  47. self.__redis.expire(name, time)
  48. def type(self, name):
  49. """
  50. 获取name对应值的类型\n
  51. :param name: key或者命名空间\n
  52. :return:
  53. """
  54. return self.__redis.type(name)
  55. def rename(self, src, dst):
  56. """
  57. 重命名key或者命名空间\n\n
  58. :param src: 原key或者命名空间\n
  59. :param dst: 修改后的key或者命名空间\n
  60. :return:
  61. """
  62. if self.exists(dst):
  63. raise Exception(dst + ' 已存在')
  64. if not self.exists(src):
  65. raise Exception(src + ' 不存在')
  66. self.__redis.rename(src, dst)
  67. # ------------------------字符串-----------------------------
  68. def get(self, key):
  69. """
  70. 获取指定字符串值\n
  71. :param key:单个键\n
  72. :return:
  73. """
  74. return self.__redis.get(key)
  75. def mget(self, *keys):
  76. """
  77. 批量获取指定字符串值\n
  78. :param keys:多个键\n
  79. :return:
  80. """
  81. return self.__redis.mget(keys)
  82. def set(self, key, value, px=None):
  83. """
  84. 字符串设置值 \n
  85. :param key:键\n
  86. :param value:值\n
  87. :param px:过期时间(ms)\n
  88. :return:
  89. """
  90. self.__redis.set(name=key, value=value, px=px)
  91. def mset(self, **map):
  92. """
  93. 字符串批量设置值\n
  94. :param map:批量设置的键值字典\n
  95. :return:
  96. """
  97. self.__redis.mset(mapping=map)
  98. # -------------------------Hash-----------------------------
  99. def hget(self, name, key):
  100. """
  101. 在name对应的hash中根据key获取value \n
  102. :param name: 命名空间
  103. :param key: 命名空间下对应的键
  104. :return:
  105. """
  106. return self.__redis.hget(name=name, key=key)
  107. def hmget(self, name, *keys):
  108. """
  109. 在name对应的hash中获取多个key的值\n
  110. :param name: 命名空间\n
  111. :param keys: 命名空间下的多个键
  112. :return:
  113. """
  114. return self.__redis.hmget(name=name, keys=keys)
  115. def hgetall(self, name):
  116. """
  117. 获取name对应hash的所有键值 \n
  118. :param name:命名空间 \n
  119. :return:
  120. """
  121. return self.__redis.hgetall(name=name)
  122. def hset(self, name, key, value):
  123. """
  124. name对应的hash中设置一个键值对(不存在,则创建,否则,修改)\n
  125. :param name: 命名空间
  126. :param key: 命名空间下对应的键
  127. :param value: 命名空间下对应的值
  128. :return:
  129. """
  130. self.__redis.hset(name=name, key=key, value=value)
  131. def hmset(self, name, **map):
  132. """
  133. 在name对应的hash中批量设置键值对\n
  134. :param name:命名空间\n
  135. :param map:键值对\n
  136. :return:
  137. """
  138. self.__redis.hmset(name=name, mapping=map)
  139. def hexists(self, name, key):
  140. """
  141. 检查name对应的hash是否存在当前传入的key\n
  142. :param name: 命名空间\n
  143. :param key: 命名空间下对应的键
  144. :return:
  145. """
  146. return self.__redis.hexists(name=name, key=key)
  147. def hdel(self, name, keys):
  148. """
  149. 批量删除指定name对应的key所在的键值对\n
  150. :param name:命名空间\n
  151. :param keys:要删除的键\n
  152. :return:
  153. """
  154. self.__redis.hdel(name, keys)
  155. # -------------------------List-----------------------------
  156. def lpush(self, name, *values, left=True):
  157. """
  158. 在name对应的list中添加元素,每个新的元素都添加到列表的最左边\n
  159. :param name: 命名空间
  160. :param values: 值
  161. :param left: 是否添加到列表的最左边,True:最左边,False:最右边,默认为True
  162. :return:
  163. """
  164. if left:
  165. self.__redis.lpush(name, *values)
  166. else:
  167. self.__redis.rpush(name, *values)
  168. def lset(self, name, index, value):
  169. """
  170. 对list中的某一个索引位置重新赋值\n
  171. :param name: 命名空间
  172. :param index: 索引位置
  173. :param value: 要插入的值
  174. :return:
  175. """
  176. self.__redis.lset(name=name, index=index, value=value)
  177. def lrem(self, name, count, value):
  178. """
  179. 删除name对应的list中的指定值\n
  180. :param name:命名空间\n
  181. :param count:num=0 删除列表中所有的指定值;num=2 从前到后,删除2个;num=-2 从后向前,删除2个
  182. :param value:要删除的值
  183. :return:
  184. """
  185. self.__redis.lrem(name=name, count=count, value=value)
  186. def lpop(self, name):
  187. """
  188. 移除列表的左侧第一个元素,返回值则是第一个元素\n
  189. :param name: 命名空间\n
  190. :return: 第一个元素
  191. """
  192. return self.__redis.lpop(name=name)
  193. def lindex(self, name, index):
  194. """
  195. 根据索引获取列表内元素\n
  196. :param name: 命名空间\n
  197. :param index: 索引位置
  198. :return:
  199. """
  200. return self.__redis.lindex(name=name, index=index)
  201. def lrange(self, name, start, end):
  202. """
  203. 获取指定范围内的元素\n
  204. :param name: 命名空间\n
  205. :param start: 起始位置
  206. :param end: 结束位置
  207. :return:
  208. """
  209. return self.__redis.lrange(name=name, start=start, end=end)
  210. def ltrim(self, name, start, end):
  211. """
  212. 移除列表内没有在该索引之内的值\n
  213. :param name: 命名空间\n
  214. :param start: 起始位置
  215. :param end: 结束位置
  216. :return:
  217. """
  218. self.__redis.ltrim(name=name, start=start, end=end)
  219. # -------------------------Set-----------------------------
  220. def sadd(self, name, *values):
  221. """
  222. 给name对应的集合中添加元素\n
  223. :param name:命名空间\n
  224. :param values:集合
  225. :return:
  226. """
  227. self.__redis.sadd(name, *values)
  228. def smembers(self, name):
  229. """
  230. 获取name对应的集合的所有成员\n
  231. :param name: 命名空间\n
  232. :return:
  233. """
  234. return self.__redis.smembers(name=name)
  235. def sdiff(self, name, *others):
  236. """
  237. 在第一个name对应的集合中且不在其他name对应的集合的元素集合,即,name集合对于其他集合的差集\n
  238. :param name:主集合\n
  239. :param others:其他集合\n
  240. :return:
  241. """
  242. # print(*others)
  243. return self.__redis.sdiff(name, *others)
  244. def sinter(self, name, *names):
  245. """
  246. 获取多个name对应集合的交集\n
  247. :param name: 主集合\n
  248. :param names: 其他集合\n
  249. :return:
  250. """
  251. return self.__redis.sinter(name, *names)
  252. def sunion(self, name, *names):
  253. """
  254. 获取多个name对应集合的并集\n
  255. :param name: 主集合\n
  256. :param names: 其他集合\n
  257. :return:
  258. """
  259. return self.__redis.sunion(name, *names)
  260. def sismember(self, name, value):
  261. """
  262. 检查value是否是name对应的集合内的元素\n
  263. :param name:命名空间\n
  264. :param value:待检查的值\n
  265. :return:
  266. """
  267. return self.__redis.sismember(name=name, value=value)
  268. def smove(self, src, dst, value):
  269. """
  270. 将某个元素从一个集合中移动到另外一个集合\n
  271. :param src: 原集合\n
  272. :param dst: 目标集合\n
  273. :param value: 待移动的值
  274. :return:
  275. """
  276. self.__redis.smove(src=src, dst=dst, value=value)
  277. def spop(self, name):
  278. """
  279. 从集合的右侧移除一个元素,并将其返回\n
  280. :param name: 命名空间\n
  281. :return:
  282. """
  283. return self.__redis.spop(name=name)
  284. def srem(self, name, *values):
  285. """
  286. 删除name对应的集合中的某些值\n
  287. :param name: 命名空间\n
  288. :param values: 要删除的值
  289. :return:
  290. """
  291. self.__redis.srem(name, *values)

thread_pool 

  1. # -*-* encoding:UTF-8 -*-
  2. # author : mengy
  3. # date : 2019/5/23
  4. # python-version : Python 3.7.0
  5. # description : 线程池
  6. from concurrent.futures import ThreadPoolExecutor, as_completed
  7. # 最大线程数
  8. MAX_WORKERS = 50
  9. class ThreadPool:
  10. __instance = None
  11. def __init__(self):
  12. self.__executor = ThreadPoolExecutor(MAX_WORKERS)
  13. def __new__(cls, *args, **kwargs):
  14. """
  15. 使用单例模式\n
  16. :param args:
  17. :param kwargs:
  18. :return:
  19. """
  20. if cls.__instance is None:
  21. cls.__instance = object.__new__(cls)
  22. return cls.__instance
  23. def submit(self, func, *args, **kwargs):
  24. return self.__executor.submit(func, *args, **kwargs)
  25. def batch_submit(self, func, *args, **kwargs):
  26. return [self.submit(func, *item, **kwargs) for item in args]
  27. @staticmethod
  28. def completed_tasks(tasks):
  29. return as_completed(tasks)

至此,我们已经创建了自己的 ip 池,接下来就需要运用到我们的爬虫程序中。在 middleware.py 中新建 IPProxyMiddleware 类,在该类的构造方法中,首先从我们的 ip 池获取一页的 ip ,然后再请求之前,对请求对象 Request 设置代理的 url,然后在请求完成之后,根据目标网页的返回码进行判断,如果请求发生异常,则从 ip 池中随机取出一条 ip ,重新构造 Request,待下一次请求,另外,我们在 settings.py 文件中指定了失败的最大次数为 5 次,意味着,如果一个请求失败超过 5 次,则放弃该请求。该逻辑实现是在 process_response () 方法中。该方法如果返回值是 request,则表示重新将该请求发送到 Scheduler ,该请求将再次被执行;如果返回是 response ,则表示该请求已经完成,不会将对该请求返回的数据进行处理。具体代码如下:

  1. class IPProxyMiddleware(object):
  2. """
  3. IP 代理池中间件
  4. """
  5. def __init__(self):
  6. # 爬取有效 ip
  7. self.ip_list = crawl_proxy.get_ips(pages=3)
  8. # 请求已经失败的次数
  9. self.retry_time = 0
  10. self.index = random.randint(0, len(self.ip_list) - 1)
  11. def process_request(self, request, spider):
  12. """
  13. 处理将要请求的 Request
  14. :param request:
  15. :param spider:
  16. :return:
  17. """
  18. # 失败重试次数
  19. self.retry_time = 0
  20. #
  21. # if len(self.ip_list) < 5:
  22. # self.ip_list.extend(crawl_proxy.get_ips(refresh=True))
  23. # 随机选取 ip
  24. proxy = json.loads(self.ip_list[self.index])
  25. print('选取的 ip:' + proxy.get('url'))
  26. # 设置代理
  27. request.meta['Proxy'] = proxy.get('url')
  28. def process_response(self, request, response, spider):
  29. """
  30. 处理返回的 Response
  31. :param request:
  32. :param response:
  33. :param spider:
  34. :return:
  35. """
  36. # 针对4**、和5** 响应码,重新选取 ip
  37. if re.findall('[45]\d+', str(response.status)):
  38. print(u'[%s] 响应状态码:%s' % (response.url, response.status))
  39. if self.retry_time > settings.get('MAX_RETRY', 5):
  40. return response
  41. if response.status == 418:
  42. sec = random.randrange(30, 35)
  43. print(u'休眠 %s 秒后重试' % sec)
  44. # time.sleep(sec)
  45. self.retry_time += 1
  46. proxy = json.loads(random.choice(self.ip_list))
  47. print('失败 %s 次后,重新选取的 ip:%s' % (self.retry_time, proxy.get('url')))
  48. request.meta['Proxy'] = proxy.get('url')
  49. return request
  50. return response

最后,不要忘记在 settings.py 中启用 IPProxyMiddleware 中间件:

  1. DOWNLOADER_MIDDLEWARES = {
  2. 'sina_scrapy.middlewares.RandomUserAgentMiddleware': 10,
  3. 'sina_scrapy.middlewares.IPProxyMiddleware': 30,
  4. }

再次启动爬虫程序,我们可以看到,已经从我们的 ip 池中获取到 ip 并进行请求,当有异常发生时,将会再次从 ip 池中获取 ip,并重新请求。

 

Cookies 池

在本篇博客的开头就提到,新浪微博的反爬机制是针对账号的,但是我只有一个账号,因此这一小节提到的 Cookies 池对于我们 新浪微博爬虫程序来说,也许并不能起到太大的左右,但是如果你有多个微博账号,Cookies 池的方法将能够极大地提高你程序的爬取效率。方法都是一样的。Cookies 池和前面提到的 User-Agent 池、ip 池的原理是一样的,都是获取一组的数据,存入 Redis 中,等到需要使用的时候,在随机从 Redis 中取出数据,进行处理。

我们在 utils 包下新建一个文件 simulate_login.py 用于编写模拟登录新浪微博的脚本,在这里,我分别实现了微博移动网页版模拟登录(https://weibo.cn/)以及新浪微博 PC 网页版(https://weibo.com)。登录的难度不一致,可以根据需要自己选择。其中 PC 网页版的登录逻辑,可以参考《模拟新浪微博登录(Python+RSA加密算法)》这篇博客,里面有详细的分析,在这里就不一一赘述了,只是需要注意相应的 js 版本可能已经过时了。具体实现如下:

  1. # -*-* encoding:UTF-8 -*-
  2. # author : mengy
  3. # date : 2019/6/26
  4. # python-version : Python 3.7.0
  5. # description : 模拟登录新浪微博
  6. import base64
  7. import urllib
  8. import rsa
  9. import binascii
  10. import json
  11. import re
  12. import http.cookiejar
  13. import urllib.request
  14. from sina_scrapy.utils.cache_utils import Cache
  15. # cookies 在缓存中的有效期(s)
  16. COOKIES_EXPIRES = 3 * 24 * 60 * 60
  17. def urlopen(url, callback=None, data=None, timeout=5):
  18. """
  19. 重写 urllib 的 urlopen 方法,该方法能够将 cookies 作为参数传给回调函数\n
  20. :param url:请求的地址或者 url.request.Request() 对象\n
  21. :param callback:回调函数\n
  22. :param data:请求数据\n
  23. :param timeout:超时时间(s),默认为 5s\n
  24. :return:
  25. """
  26. cookie = http.cookiejar.CookieJar()
  27. handler = urllib.request.HTTPCookieProcessor(cookie)
  28. opener = urllib.request.build_opener(handler)
  29. response = opener.open(url, data=data, timeout=timeout)
  30. if callback:
  31. callback(cookie)
  32. return response
  33. class LoginBase(object):
  34. """
  35. 微博模拟登录基类,实现了新浪移动微博网页版(https://weibo.cn/)的模拟登录\n
  36. """
  37. # 缓存工具
  38. __cache = Cache()
  39. # 移动网页版 cookies 在缓存中的命名
  40. COOKIES_NAMESPACE = 'MOBILE_WEB_POOL_COOKIES'
  41. def __init__(self, username, password):
  42. # 微博账号
  43. self.__username = username
  44. # 微博密码
  45. self.__password = password
  46. # 记录 cookies,按照 domain 分组
  47. self.cookies = {}
  48. @property
  49. def username(self) -> str:
  50. return self.__username
  51. @property
  52. def password(self) -> str:
  53. return self.__password
  54. def save_cookies(self, cookie: http.cookiejar.CookieJar):
  55. """
  56. 保存 Cookies\n
  57. :param cookie:
  58. :return:
  59. """
  60. # 按照 domain 分组记录所访问过 url 的 cookies
  61. for item in cookie:
  62. tmp = self.cookies.get(item.domain)
  63. if tmp:
  64. tmp.update({item.name: item.value})
  65. else:
  66. self.cookies.update({item.domain: {item.name: item.value}})
  67. def login(self):
  68. """
  69. 微博移动网页版模拟登录(https://weibo.cn/),代码实现逻辑根据网页版 js,有一定的时效性\n
  70. :return:
  71. """
  72. print(u'微博移动网页版模拟登录(https://weibo.cn/)开始...')
  73. # 登录地址
  74. url = 'https://passport.weibo.cn/sso/login'
  75. # 默认 Headers
  76. headers = {
  77. 'Referer': 'https://passport.weibo.cn/signin/login?entry=mweibo&r=https%3A%2F%2Fweibo.cn&page=9.com&uid=1260427471&_T_WM=c6e864f47316ecbaf8607a214d4bb3fa',
  78. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'
  79. }
  80. # 构造模拟登录请求的表单数据
  81. data = {
  82. 'username': self.__username,
  83. 'password': self.__password,
  84. 'savestate': 1,
  85. 'r': 'https://weibo.cn',
  86. 'ec': 0,
  87. 'pagerefer': '',
  88. 'entry': 'mweibo',
  89. 'wentry': '',
  90. 'loginfrom': '',
  91. 'client_id': '',
  92. 'code': '',
  93. 'qq': '',
  94. 'mainpageflag': 1,
  95. 'hff': '',
  96. 'hfp': ''
  97. }
  98. try:
  99. # 格式化 请求数据
  100. post_data = urllib.parse.urlencode(data).encode('gbk')
  101. # 构造请求
  102. req = urllib.request.Request(url=url, data=post_data, headers=headers, method='POST')
  103. # 使用自定义的请求方法,保存请求的 cookies
  104. response = urlopen(url=req, callback=self.save_cookies, timeout=10)
  105. # 将返回的数据转化成 dict
  106. result = json.loads(response.read().decode('gbk'))
  107. if result.get('retcode') == 20000000:
  108. print(u'登录成功!')
  109. # 登录成功后返回的 url
  110. crossdomainlist = result.get('data').get('crossdomainlist')
  111. # 依次访问 url,获取 cookies 并保存
  112. if crossdomainlist:
  113. for item in dict(crossdomainlist).values():
  114. urlopen(item, self.save_cookies)
  115. else:
  116. print(u'登录失败!')
  117. # 将 cookies 放入缓存 redis
  118. self.push_cache()
  119. return True
  120. except Exception as e:
  121. print(u'解析失败', e)
  122. return False
  123. def push_cache(self):
  124. assert self.cookies, u'请先模拟登录'
  125. # self.__cache.lpush(self.COOKIES_NAMESPACE, json.dumps(self.cookies))
  126. self.__cache.hset(self.COOKIES_NAMESPACE, self.username, json.dumps(self.cookies))
  127. # 设置 cookies 的有效时间(三天)
  128. self.__cache.expire(self.COOKIES_NAMESPACE, COOKIES_EXPIRES)
  129. def get_cookies(self, domain=None, is_force_login=False):
  130. """
  131. 获取 cookies\n
  132. :param domain:域名
  133. :param is_force_login:是否强制登录(默认为 False)\n
  134. :return:
  135. """
  136. # 从 redis 获取 cookies
  137. # data = self.__cache.lrange(namespace, 0, 1)
  138. data = self.__cache.hget(self.COOKIES_NAMESPACE, self.username)
  139. if is_force_login or not data:
  140. # 如果 redis 中没有 cookies,则模拟登录,重新获取 cookies
  141. if self.login():
  142. cookies = self.cookies
  143. else:
  144. raise Exception(u'获取 Cookies 失败!')
  145. else:
  146. print(u'从缓存中获取 cookies')
  147. cookies = json.loads(data)
  148. if domain:
  149. return cookies.get(domain)
  150. return cookies
  151. class LoginForSinaCom(LoginBase):
  152. """
  153. 模拟新浪微博 PC 网页版(https://weibo.com)登录,登录后,将 cookies 保存到 redis 缓存中,并提供获取 cookies 的方法
  154. """
  155. # PC 网页版 cookies 在缓存中的命名
  156. COOKIES_NAMESPACE = 'PC_WEB_POOL_COOKIES'
  157. def __init__(self, username, password):
  158. LoginBase.__init__(self, username, password)
  159. def encrypt_name(self) -> str:
  160. """
  161. 用 base64 加密用户名 \n
  162. :return:
  163. """
  164. return base64.encodebytes(bytes(urllib.request.quote(self.username), 'utf-8'))[:-1].decode('utf-8')
  165. def encrypt_passwd(self, **kwargs) -> str:
  166. """
  167. 使用 rsa 加密密码\n
  168. :param kwargs:
  169. :return:
  170. """
  171. try:
  172. # 拼接明文
  173. message = str(kwargs['servertime']) + '\t' + str(kwargs['nonce']) + '\n' + str(self.password)
  174. # 10001 为 js 加密文件中的加密因子,16进制
  175. key = rsa.PublicKey(int(kwargs['pubkey'], 16), 0x10001)
  176. # 使用 rsa 加密拼接后的密码
  177. encrypt_pwd = rsa.encrypt(message.encode('utf-8'), key)
  178. # 将加密后的密文转化成 AscII 码
  179. final_pwd = binascii.b2a_hex(encrypt_pwd)
  180. return final_pwd
  181. except Exception as e:
  182. print(e)
  183. return None
  184. def pre_login(self) -> dict:
  185. """
  186. 预登录,请求 prelogin_url 链接地址 获取 servertime,nonce,pubkey 和 rsakv \n
  187. :return:
  188. """
  189. # 预登录地址
  190. pre_login_url = 'http://login.sina.com.cn/sso/prelogin.php?entry=sso&callback=sinaSSOController.preloginCallBack&su=%s&rsakt=mod&client=ssologin.js(v1.4.19)' % self.encrypt_name()
  191. try:
  192. response = urlopen(pre_login_url, callback=self.save_cookies, timeout=5)
  193. # 提取响应结果
  194. preloginCallBack = re.compile('\((.*)\)').search(str(response.read(), 'UTF-8'))
  195. if preloginCallBack:
  196. result = json.loads(preloginCallBack.group(1))
  197. else:
  198. raise Exception(u'解析响应结果失败!')
  199. return result
  200. except Exception as e:
  201. print(e)
  202. return None
  203. def login(self):
  204. """
  205. 登录新浪微博 PC 网页版(https://weibo.com)\n
  206. :return:
  207. """
  208. print(u'新浪微博 PC 网页版(https://weibo.com)登录开始...')
  209. # 预登录
  210. result = self.pre_login()
  211. # 加密用户账号
  212. encodedUserName = self.encrypt_name()
  213. serverTime = result.get('servicetime')
  214. nonce = result.get('nonce')
  215. rsakv = result.get('rsakv')
  216. # 加密密码
  217. encodedPassWord = self.encrypt_passwd(**result)
  218. # 构造请求数据
  219. post_data = {
  220. "entry": "weibo",
  221. "gateway": "1",
  222. "from": "",
  223. "savestate": "7",
  224. "qrcode_flag": 'false',
  225. "useticket": "1",
  226. "pagerefer": "https://login.sina.com.cn/crossdomain2.php?action=logout&r=https%3A%2F%2Fweibo.com%2Flogout.php%3Fbackurl%3D%252F",
  227. "vsnf": "1",
  228. "su": encodedUserName,
  229. "service": "miniblog",
  230. "servertime": serverTime,
  231. "nonce": nonce,
  232. "pwencode": "rsa2",
  233. "rsakv": rsakv,
  234. "sp": encodedPassWord,
  235. "sr": "1680*1050",
  236. "encoding": "UTF-8",
  237. "prelt": "194",
  238. "url": "https://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack",
  239. "returntype": "META"
  240. }
  241. # 登录地址
  242. url = 'https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.19)'
  243. # 打包请求数据
  244. data = urllib.parse.urlencode(post_data).encode('GBK')
  245. headers = {
  246. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'
  247. }
  248. try:
  249. # 请求登录
  250. req = urllib.request.Request(url=url, data=data, headers=headers)
  251. response = urlopen(req, callback=self.save_cookies)
  252. text = response.read().decode('GBK')
  253. except Exception as e:
  254. print(e)
  255. try:
  256. # 获取第一次重定向地址
  257. login_url = re.compile('location\.replace\("(.*)"\)').search(text).group(1)
  258. # 第一次重定向
  259. response = urlopen(login_url, callback=self.save_cookies)
  260. data = response.read().decode('GBK')
  261. # 获取第二次重定向地址
  262. jump_url = re.compile("location\.replace\('(.*)'\)").search(data).group(1)
  263. # 第二次重定向
  264. response = urlopen(jump_url, callback=self.save_cookies)
  265. data = response.read().decode('utf-8')
  266. # 获取服务器返回的加密的 用户名
  267. name = re.compile('"userdomain":"(.*)"').search(data).group(1)
  268. index = 'http://weibo.com/' + name
  269. # 第三次跳转到首页
  270. urlopen(index, callback=self.save_cookies)
  271. print(u'登录成功!')
  272. # 将 cookies 放入缓存 redis
  273. self.push_cache()
  274. return True
  275. except Exception as e:
  276. print(u'登录失败!,异常:', e)
  277. return False

有了 Cookies 池以后,就需要编写中间件类使用 Cookies 池的数据了,在 middleware.py 中添加如下类:

  1. class CookiesMiddleware(object):
  2. """
  3. 登录 Cookies 中间件
  4. """
  5. def __init__(self):
  6. self.cookies = simulate_login.LoginBase(settings.get('SINA_ACCOUNT'), settings.get('SINA_PASSWD')).get_cookies()
  7. def process_request(self, request, spider):
  8. cookies = self.cookies.get('.weibo.cn')
  9. request.cookies = cookies

判断代码实现很简单,只是在请求之前,从 Cookies 中获取一个 Cookies,然后赋给 Request。另外,在前面的使用 ip 池的时候,我们有根据返回码来确定是不是要重新从 ip 池中拉取 ip,同样的,我们可以增加判断,如果响应码为 418 ,就重新从 Cookies 池中获取 Cookies,另外,我们的微博账号密码是配置在 settings.py 中的 SINA_ACCOUNT 以及 SINA_PASSWD,需要替换成自己的账号密码。代码如下:

  1. if response.status == 418:
  2. # 出现 418 重新获取 cookies
  3. request.cookies = simulate_login.LoginBase(settings.get('SINA_ACCOUNT'),
  4. settings.get('SINA_PASSWD')).get_cookies('.weibo.cn')
  5. sec = random.randrange(30, 35)
  6. print(u'休眠 %s 秒后重试' % sec)
  7. # time.sleep(sec)

好了,接着,我们在 settings.py 中启用 CookiesMiddleware 中间件,代码如下:

  1. DOWNLOADER_MIDDLEWARES = {
  2. 'sina_scrapy.middlewares.RandomUserAgentMiddleware': 10,
  3. 'sina_scrapy.middlewares.CookiesMiddleware': 20,
  4. 'sina_scrapy.middlewares.IPProxyMiddleware': 30,
  5. }
  6. SINA_ACCOUNT = 'XXXXXXXXXXX'
  7. SINA_PASSWD = 'XXXXXXXXXXXX'

再次运行爬虫程序,将会从 Cookies 池中获取 Cookies。

 

总结

本系列博客代码均已提交至我的 GitHub,有需要的可以移步下载,如需更详细的代码(数据分析,使用 pyecharts2.0 和 pyplot 绘制图表,包括:日爬取量统计图、微博用户性别分布统计图、微博用户年龄分布统计图、新浪微博用户活跃度分布图、新浪微博用户粉丝和关注数分布图、新浪微博用户分布地图等),请从 CSDN下载。pyecharts2.0 数据分析效果预览:

  • 日爬取量统计图

  • 微博用户性别分布统计图

  • 微博用户年龄分布统计图

  • 新浪微博用户活跃度分布图

  • 新浪微博用户粉丝和关注数分布图

  • 新浪微博用户分布地图

 

写在最后

到这里,使用 Scrapy 爬取新浪微博用户信息系列博客就已经结束了,在这一系列博客中,我们学习了 Scrapy 框架的基本原理以及基本用法,当然 Scrapy 的强大之处远不止如此,例如:下载器中间件(DownloaderMiddleware)、基于 Redis 的分布式 Scrapy等。学无止境,希望大家不要止步于此。想要掌握 Scrapy 的全部功能,需要自己慢慢探索、实践。大家加油!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/56899
推荐阅读
相关标签
  

闽ICP备14008679号