• 第九部分 代理的使用(代理设置,代理池的搭建,用代理获取微信公众号文章)


    网站采取的反爬虫措施有:弹出验证码,需要登录。检测某个IP在单位时间内的请求次数,超过规定的某个值,服务器拒绝服务,返回一些错误信息,这是封IP。

    既然服务器封IP,可采用某种方式伪装IP,让服务器不能识别由本机发起的请求,这样来避免封IP。这时就需要使用到代理。

    一、 代理的设置
    代理有免费代理和付费代理。免费代理多数情况下不好用,付费代理比较靠谱。付费代理不用多,稳定可用即可。
    西刺免费代理:http://www.xicidaili.com
    现在获取西刺网站上的免费代理IP做一个测试。IP获取代码如下所示:

     1 import json
     2 import pandas as pd
     3 from selenium import webdriver
     4 from selenium.webdriver.common.by import By
     5 from selenium.webdriver.support.ui import WebDriverWait
     6 from selenium.webdriver.support import expected_conditions as EC
     7 
     8 URL = 'https://www.xicidaili.com/'
     9 _FILENAME = "xicidailiip.json"
    10 
    11 class GetXiciDailiIp():
    12     def __init__(self):
    13         self.url = URL
    14         self.browser = webdriver.Chrome()
    15         self.wait = WebDriverWait(self.browser, 20)
    16         self.http_ip_port = {}
    17 
    18     def __del__(self):
    19         self.browser.close()
    20 
    21     def get_pagesource(self):
    22         """
    23         获取网页源代码
    24         :return: 网页源代码
    25         """
    26         self.browser.get(self.url)
    27         self.wait.until(EC.presence_of_element_located((By.ID, "ip_list")))
    28         page_source = self.browser.page_source
    29         return page_source
    30 
    31     def parse_ip(self, page_source):
    32         """
    33         使用pandas解析网页中的IP地址
    34         :param page_source: 网页源代码
    35         :return: self.http_ip_port, 包含协议类型,IP地址及端口
    36         """
    37         df1 = pd.read_html(page_source) # df1 是列表,df1[0] 才是 DataFrame
    38         df2 = df1[0][[5, 1, 2]].dropna()   # 选取 协议类型、IP地址、端口列后,去掉所有的NA值行
    39         proto = list(df2[5])  # 获取协议类型列,转化成列表
    40         ip = list(df2[1])
    41         port = list(df2[2])
    42         N = len(proto)
    43         s = ['HTTP', 'HTTPS', 'socks4/5']
    44         for i in range(N):
    45             if proto[i] in s:
    46                 ip_port = ip[i] + ":" + port[i]
    47                 if proto[i] in self.http_ip_port:
    48                     if ip_port not in self.http_ip_port[proto[i]]:
    49                         self.http_ip_port[proto[i]].append(ip_port)
    50                 else:
    51                     self.http_ip_port[proto[i]] = [ip_port]
    52             else:
    53                 continue
    54         return self.http_ip_port
    55 
    56     def ipport_to_file(self, http_ip_port):
    57         with open(_FILENAME, 'w') as f:
    58             json.dump(http_ip_port, f)
    59 
    60     def crack(self):
    61         page_source = self.get_pagesource()
    62         http_ip_port = self.parse_ip(page_source)
    63         self.ipport_to_file(http_ip_port)
    64         #return http_ip_port
    65 
    66 
    67 if __name__ == "__main__":
    68     crack = GetXiciDailiIp()
    69     crack.crack()

    1、 使用urllib代理设置
    先使用最基础的urllib,来了解下代理的设置方法,代码如下所示:

     1 import json
     2 from urllib.error import URLError
     3 from urllib.request import ProxyHandler, build_opener
     4 import b2_get_xicidaili_ip as B2
     5 
     6 ipportfile = B2._FILENAME   # 保存的 IP 及 PORT 文件名称
     7 with open(ipportfile, 'r') as f:
     8     ips_ports = json.load(f)
     9 
    10 N = 0
    11 while N < 20:
    12     if ips_ports.get('HTTPS', None) and ips_ports.get('HTTP', None):
    13         proxy_handler = ProxyHandler({
    14             'http': 'http://' + ips_ports['HTTP'][N],   # http://171.83.165.125:9999
    15             'https': 'https://' + ips_ports['HTTPS'][N],
    16         })
    17     opener = build_opener(proxy_handler)
    18     try:
    19         response = opener.open('http://httpbin.org/get', timeout=30)
    20         if response.status == 200:
    21             print(response.read().decode('utf-8'))
    22             break
    23         else:
    24             N += 1
    25             continue
    26     except URLError as e:
    27         N += 1
    28         print(e.reason)

    运行结果如下所示:

    {
      "args": {},
      "headers": {
        "Accept-Encoding": "identity",
        "Cache-Control": "max-age=259200",
        "Host": "httpbin.org",
        "User-Agent": "Python-urllib/3.6"
      },
      "origin": "171.83.165.125, 171.83.165.125",
      "url": "https://httpbin.org/get"
    }

    这里使用 ProxyHandler 设置代理,参数是字典类型,键名为协议类型,键值是代理IP及端口。在代理前面需要加上协议,即http或https。当请求的连接是 http 协议时,ProxyHandler 会调用 http 代理。当请求链接是 https 协议时,会调用 https 代理。这里生效的代理是 http://171.83.165.125:9999。

    创建完 ProxyHandler 对象后,接下来利用 build_opener() 方法传入该对象来创建一个 Opener,这样相当于此 Opener 已经设置好代理。下面直接调用 Opnener 对象的 open() 方法,就可以访问想要的链接。

    运行输出结果是一个 JSON,有一个字段是 origin,标明客户端的 IP。经验证,此IP确实为代理的IP,并不是真实的IP。这样就成功设置好代理,并隐藏真实的IP。

    如果是需要认证的代理,可用下面这样的方法设置:
    'http': 'http://' + "username:password@" + ips_ports['HTTP'][N],
    其它不做修改。这是在代理前面加入代理认证的用户名密码即可。其中username是用户名,password是密码,例如 username是michael,密码是 python,那么代理就是 michael:python@171.83.165.125:9999。

    如果代理是 SOCKS5类型,可用下面方式设置代理:

     1 import json, socks, socket
     2 from urllib import request
     3 from urllib.error import URLError
     4 import b2_get_xicidaili_ip as B2
     5 
     6 ipportfile = B2._FILENAME
     7 with open(ipportfile, 'r') as f:
     8     ips_ports = json.load(f)
     9 
    10 N = 0
    11 while N < 20:
    12     if ips_ports.get('socks4/5', None):
    13         ip, port = ips_ports['socks4/5'][N].split(":")
    14         socks.set_default_proxy(socks.SOCKS5, ip, int(port))
    15         socket.socket = socks.socksocket
    16     try:
    17         response = request.urlopen('http://httpbin.org/get', timeout=30)
    18         if response.status == 200:
    19             print(response.read().decode('utf-8'))
    20             break
    21         else:
    22             N += 1
    23             continue
    24     except URLError as e:
    25         N += 1
    26         print(e.reason)

    这段代码的运行,需要安装 socks 模块,可用下面命令进行安装:
    pip3 install PySocks

    免费代理不好用,请求多次都不能成功。在真正需要代理的场景,还是搞个付费代理靠谱。请求成功的话,输出与前面的一样。

    2、 requests代理设置
    requests的代理设置很简单,只要传入 proxies 参数即可。设置方式如下:

     1 import json, requests
     2 import b2_get_xicidaili_ip as B2
     3 
     4 ipportfile = B2._FILENAME   # 保存的 IP 及 PORT 文件名称
     5 with open(ipportfile, 'r') as f:
     6     ips_ports = json.load(f)
     7 
     8 N = 0
     9 while N < 20:
    10     if ips_ports.get('HTTPS', None) and ips_ports.get('HTTP', None):
    11         proxies = {
    12             'http': 'http://' + ips_ports['HTTP'][N],
    13             'https': 'https://' + ips_ports['HTTPS'][N],
    14         }
    15     try:
    16         response = requests.get('https://www.baidu.com', proxies=proxies, timeout=30)
    17         if response.status_code == 200:
    18             print(response.text)
    19             break
    20         else:
    21             N += 1
    22             continue
    23     except requests.exceptions.ConnectionError as e:
    24         N += 1
    25         print('Error', e.args)

    请求成功后,输出网页的源代码。requests的代理设置比 urllib简单很多,只要构造代理字典,然后通过 proxies参数即可,不需要重新构建 Opener。如果代理需要认证,同样在代理前加上用户名密码即可,写法如下所示:
    'http': 'http://' + "username:password" + ":" ips_ports['HTTP'][N],
    username和password即是用户名和密码。如要使用SOCKS5代理,可使用如下方式来设置:

    proxies = {
        'http': 'socks5://' + ips_ports['HTTP'][N],
        'https': 'socks5://' + ips_ports['HTTPS'][N],
    }

    这里需要额外安装一个模块,叫作 requests[socks],安装命令如下所示:
    pip3 install 'requests[socks]'

    还可以使用 socks 模块设置代理,设置方法如下所示:

    1 import requests, socks, socket
    2 
    3 socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 8000)    # IP 和端口可以改为代理网站上的IP和端口
    4 socket.socket = socks.socksocket
    5 try:
    6     response = requests.get('http://httpbin.org/get')
    7     print(response.text)
    8 except requests.exceptions.ConnectionError as e:
    9     print('Error', e.args)

    用这种方法设置SOCKS5代理,运行结果是一样的。此方法是全局设置。可以在不同情况下选用不同的方法。

    3、 Selenium使用代理
    Selenium设置代理有两种方式:一是使用 Chrome ,有界面浏览器;另一种是使用PhantomJS的无界面浏览器。

    3.1、 Chrome使用代理
    对于Chrome,使用 Selenium设置代理方法很简单,设置方法如下:

    1 from selenium import webdriver
    2 proxy = '171.83.165.139:9999'
    3 chrome_options = webdriver.ChromeOptions()
    4 chrome_options.add_argument('--proxy-server=http://' + proxy)
    5 browser = webdriver.Chrome(chrome_options=chrome_options)
    6 browser.get('http://httpbin.org/get')

    这里使用 ChromeOptions() 设置代理,在创建 Chrome 对象时用 chrome_options 参数传递即可。运行代码弹出Chrome浏览器,成功访问目标网站则在页面上显示下面的信息:

    {
      "args": {},
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Cache-Control": "max-age=259200",
        "Host": "httpbin.org",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
      },
      "origin": "171.83.165.139, 171.83.165.139",
      "url": "https://httpbin.org/get"
    }

    代理设置成功,origin就是代理IP的地址。

    认证代理设置过程省略。

    3.2、 PhantomJS使用代理
    PhantomJS代理设置方法可借助 service_args 参数,也就是命令行参数。代理设置方法如下:

    1 from selenium import webdriver
    2 service_args = [
    3     '--proxy=171.83.165.139:9999',
    4     '--proxy-type=http'
    5 ]
    6 browser = webdriver.PhantomJS(service_args=service_args)
    7 browser.get('http://httpbin.org/get')
    8 print(browser.page_source)

    这里使用 serivce_args 参数,将命令行的一些参数定义为列表,在初始化时候传递给 PhantomJS对象即可。输出如下所示:

    <html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">{
      "args": {},
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "zh-CN,en,*",
        "Cache-Control": "max-age=259200",
        "Host": "httpbin.org",
        "User-Agent": "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1"
      },
      "origin": "171.83.165.139, 171.83.165.139",
      "url": "https://httpbin.org/get"
    }
    </pre></body></html>

    输出结果中的 origin 就是代理的IP地址。如果是认证代理,只需要在 service_args中加入 --proxy-auth选项即可,只需将参数改为下面这样:
    service_args = [
    '--proxy=171.83.165.139:9999',
    '--proxy-type=http',
    '--proxy-auth=username:password'
    ]

    二、 代理池的维护
    在爬虫的时候,有些代理IP是不可用的,可能因某个IP多次访问同一个网站,造成该IP被封。这里需要提前做筛选,剔除掉不可用的代理,保留可用的代理。可搭建代理池来解决。

    在开始之前,需要安装 Redis数据库并启动服务,还需要安装 aiohttp、requests、redis-py、pyquery、Flask库。

    1、 代理池目标
    实现高效易用的代理池。基本模块分为4块:存储模块、获取模块、检测模块、接口模块
    存储模块:负责存储抓取下来的代理。首先要保证代理不重复, 要标识代理的可用情况,还要动态实时处理每个代理,所以一种比较高效和方便的存储方式就是使用Redis 的Sorted Set ,即有序集合。

    获取模块:定时在各大代理网站抓取代理。代理可以是免费公开代理也可以是付费代理,形式都是IP 加端口,为此尽量从不同来源获取,尽量抓取高匿代理,抓取成功之后将可用代理保存到数据库中。

    检测模块: 定时检测数据库中的代理。需要设置一个检测链接,最好是爬取哪个网站就检测哪个网站,这样更加有针对性,如果要做一个通用型的代理,那可以设置百度等链接来检测。另外,需要标识每一个代理的状态,如设置分数标识, 100 分代表可用,分数越少代表越不可用。检测一次,如果代理可用,我们可以将分数标识立即设置为100满分,也可以在原基础上加1分;如果代理不可用,可以将分数标识减1分,当分数戚到一定阔值后,代理就直接从数据库移除。通过这样的标识分数,我们就可以辨别代理的可用情况,选用的时候会更有针对性。

    接口模块: 需要用API 来提供对外服务的接口。其实我们可以直接连接数据库来取对应的数据,但是这样就需要知道数据库的连接信息,并且要配置连接,而比较安全和方便的方式就是提供一个Web API 接口,通过访问接口即可拿到可用代理。另外,由于可用代理可能有多个,那么可以设置一个随机返回某个可用代理的接口,这样就能保证每个可用代理都可以取到,实现负载均衡。

    2、 代理池的原理
    代理池大致可分为4个模块:存储模块、获取模块、检测模块、接口模块。
    存储模块:使用Redis的有序集合,用来做代理的去重和状态标识,中心模块和基础模块,将其他模块串联起来。
    获取模块:定时从代理网站获取代理,将获取的代理传递给存储模块,并保存到数据库。
    检测模块:定时通过存储模块获取所有代理,并对代理进行检测,根据不同的检测结果对代理设置不同的标识。
    接口模块:通过WebAPI提供服务接口,接口通过连接数据库并通过Web 形式返回可用的代理。

    3、 代理池的实现
    经过上述分析,下面用代码实现这4个模块。

    3.1、 存储模块
    使用Redis有序集合,集合的每一个元素不重复,对于代理池来说,集合的元素就变成了一个个代理,就是IP加端口形式,如1.1.1.1:8000,集合的元素就是这种形式。此外,有序集合的每一个元素有一个分数字段,分数可以重复,可以是浮点数类型,也可以是整数类型。该集合会根据每一个元素的分数对集合进行排序,数值小的排前面,数据值大的排后面,这样可实现集合元素的排序。

    对于代理,这个分数可以作为判断一个代理是否可用的标志,100分最高,代表最可用,0分最低,代表最不可用。如果要获取可用代理,可从代理池中随机获取分数最高的代理,这样可保证每个可用代理都会被调用到。

    分数是判断代理稳定性的重要标准,设置分数规则如下:
    (1)、分数100为可用,检测器定时循环检测每个代理可用情况,一旦检测到有可用代理就设置为100,检测到不可用就将分数减1,分数减至0后代理移除。
    (2)、新获取的代理分数为10,如果测试可行,分数立即设置为100,不可行则分数减1,分数减至0后代理移除。

    检测到代理可用就将分数立即设置为100,这样保证所有可用代理有更大机会被获取到。立即设置为100而不是每次加1,是因为代理是从各大免费网站获取的,一个代理并不稳定,5次请求,可能有3次都会失败。所以请求成功后就设置为100,避免过多的去测试请求,这样分数最高可用的机会也最大。

    先定义一个RedisClient类来操作数据库的有序集合,定义一些方法实现分数的设置、代理的获取等。代码如下所示:

      1 MAX_SCORE = 100
      2 MIN_SCORE = 0
      3 INITIAL_SCORE = 10
      4 REDIS_HOST = '192.168.64.50'
      5 REDIS_PORT = 6379
      6 REDIS_PASSWORD = None
      7 REDIS_KEY = 'proxies'
      8 
      9 import redis
     10 from random import choice
     11 import re
     12 
     13 class RedisClient(object):
     14     def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
     15         """
     16         初始化,参数是Reids的连接信息
     17         :param host: Redis 地址
     18         :param port: Redis 端口
     19         :param password: Redis 密码
     20         """
     21         self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True)
     22 
     23     def add(self, proxy, score=INITIAL_SCORE):
     24         """
     25         添加代理,设置分数为最高,默认分数为INITIAL_SCORE
     26         :param proxy: 代理
     27         :param score: 分数
     28         :return: 添加结果
     29         """
     30         if not re.match('d+.d+.d+.d+:d+', proxy):
     31             print('代理不符合规范', proxy, '不添加')
     32             return
     33         if not self.db.zscore(REDIS_KEY, proxy):
     34             return self.db.zadd(REDIS_KEY, {proxy: score})
     35 
     36     def random(self):
     37         """
     38         随机获取有效代理,首先尝试获取最高分数代理,如果最高分数不存在,则按照排名获取,否则异常
     39         :return: 随机代理
     40         """
     41         result = self.db.zrangebyscore(REDIS_KEY, MAX_SCORE, MAX_SCORE) # 选取分数最高的代理
     42         if len(result):
     43             return choice(result)
     44         else:
     45             result = self.db.zrevrange(REDIS_KEY, 0, 100)   # 索引从0到100的代理
     46             if len(result):
     47                 return choice(result)
     48             else:
     49                 raise PoolEmptyError
     50 
     51     def decrease(self, proxy):
     52         """
     53         代理无效时,代理分数值减一分,分数小于最小值,则代理删除
     54         :param proxy: 代理
     55         :return: 修改后的代理分数
     56         """
     57         score = self.db.zscore(REDIS_KEY, proxy)
     58         if score and score > MIN_SCORE:
     59             print("代理", proxy, "当前分数", score, "减1")
     60             return self.db.zincrby(REDIS_KEY, proxy, -1)
     61         else:
     62             print("代理", proxy, "当前分数", score, '移除')
     63             return self.db.zrem(REDIS_KEY, proxy)
     64 
     65     def exists(self, proxy):
     66         """
     67         判断代理是否在集合中
     68         :param proxy: 代理
     69         :return: 是否存在
     70         """
     71         return not self.db.zscore(REDIS_KEY, proxy) == None
     72 
     73     def max(self, proxy):
     74         """
     75         将代理设置为MAX_SCORE,代理有效时的设置
     76         :param proxy: 代理
     77         :return: 设置结束
     78         """
     79         print("代理", proxy, "可用,设置为", MAX_SCORE)
     80         return self.db.zadd(REDIS_KEY, {proxy: MAX_SCORE})
     81 
     82     def count(self):
     83         """
     84         获取当前集合的元素个数
     85         :return:数量
     86         """
     87         return self.db.zcard(REDIS_KEY)     # zcount(name, min, max)是在一个区间中的个数
     88 
     89     def all(self):
     90         """
     91         获取全部代理,检测使用
     92         :return: 全部代理列表
     93         """
     94         return self.db.zrangebyscore(REDIS_KEY, MIN_SCORE, MAX_SCORE)
     95 
     96     def batch(self, start, stop):
     97         """
     98         批量获取,获取指定索引范围内的代理,不是分数范围
     99         :param start: 开始索引
    100         :param stop: 结束索引
    101         :return: 代理列表
    102         """
    103         return self.db.zrevrange(REDIS_KEY, start, stop - 1)

    这里首先定义一些常量,如MAX_SCORE、MIN_SCORE、INITIAL_SCORE代表最大分数、最小分数、初始分数。还有一些Redis的连接信息。REDIS_KEY是有序集合的键名,用来获取代理存储所使用的有序集合。

    有了这些方法,在后面的模块中调用这个类来连接和操作数据库。要获取随机可用代理,调用 random()方法即可。

    3.2、 获取模块
    这里定义一个Crawler类从各大网站抓取代理,代码如下:

     1 import json, re
     2 from .utils import get_page
     3 from pyquery import PyQuery as pq
     4 
     5 class ProxyMetaclass(type):
     6     def __new__(cls, name, bases, attrs):
     7         count = 0
     8         attrs['__CrawlFunc__'] = []
     9         for k, v in attrs.items():
    10             if 'crawl_' in k:
    11                 attrs['__CrawlFunc__'].append(k)
    12                 count += 1
    13         attrs['__CrawlFuncCount__'] = count
    14         return type.__new__(cls, name, bases, attrs)
    15 
    16 class Crawler(object, metaclass=ProxyMetaclass):
    17     def get_proxies(self, callback):
    18         proxies = []
    19         for proxy in eval("self.{}()".format(callback)):
    20             print("成功获取到代理", proxy)
    21             proxies.append(proxy)
    22         return proxies
    23 
    24     def crawl_daili66(self, page_count=4):
    25         """
    26         获取代理66
    27         :param page_count: 页码
    28         :return: 代理
    29         """
    30         start_url = 'http://www.66ip.cn/{}.html'
    31         urls = [start_url.format(page) for page in range(1, page_count+1)]
    32         for url in urls:
    33             print('Crawling',url)
    34             html = get_page(url)
    35             if html:
    36                 doc = pq(html)
    37                 trs = doc('.containerbox table tr:gt(0)').items()
    38                 for tr in trs:
    39                     ip = tr.find('td:nth-child(1)').text()
    40                     port = tr.find('td:nth-child(2)').text()
    41                     yield ':'.join([ip, port])
    42 
    43     def crawl_xicidaili(self):
    44     for i in range(1, 3):
    45         start_url = 'http://www.xicidaili.com/nn/{}'.format(i)
    46         headers = {
    47             'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
    48             'Cookie':'_free_proxy_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiJTMxMTIyMjkwNDYzYjhlODY3MDY4NzI0NmViMzE1ZDFmBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMStES2RNNXNIL0ZnTkdpaUNhUitRVDB5a29PbGloVW44Qzc0WWNrQ2Q1T3c9BjsARg%3D%3D--7d5fcaeb32843a5d36f977d5f5d6c68541017953; Hm_lvt_0cf76c77469e965d2957f0553e6ecf59=1555326928,1555382306,1555465785,1555569224; Hm_lpvt_0cf76c77469e965d2957f0553e6ecf59=1555569269',
    49             'Host':'www.xicidaili.com',
    50             'Referer':'http://www.xicidaili.com/nn/3',
    51             'Upgrade-Insecure-Requests':'1',
    52         }
    53         html = get_page(start_url, options=headers)
    54         if html:
    55             find_trs = re.compile('<tr class.*?>(.*?)</tr>', re.S)
    56             trs = find_trs.findall(html)
    57             find_ip = re.compile('<td>(d+.d+.d+.d+)</td>')
    58             find_port = re.compile('<td>(d+)</td>')
    59             for tr in trs:
    60                 re_ip_address = find_ip.findall(tr)
    61                 re_port = find_port.findall(tr)
    62                 for address, port  in zip(re_ip_address, re_port):
    63                     address_port = address + ":" + port
    64                     yield address_port.replace(' ', '')

    这里将获取代理的方法都定义为 crawl 开头,这样以后还有代理网站时,可添加 crawl 开头的方法即可。代码中获取了代理66和西刺两个网站的免费代理,在方法中使用生成器,通过 yield 返回一个个代理。使用get_page()方法先获取网页,然后用pyquery和re解析,解析出ip和端口形式的代理后返回。

    在 Crawler 类中定义的 get_proxies() 方法,将所有以 crawl 开头的方法都调用一遍,获取每个方法返回的代理并组合成列表形式返回。这里用元类来实现所有以 crawl 开头的方法调用,先定义一个类 ProxyMetaclass,Crawl类将设置为元类,元类中实现了 __new__()方法,这个方法有固定几个参数,第四个参数 attrs 包含了类的一些属性,通过遍历 attrs 这个参数即可获取类的所有方法信息,就同遍历字典一样,键名对应方法的名,接着判断方法的开关是否 crawl,是则将其加入到 __CrawlFunc__属性中。这样就将所有以 crawl 开头的方法定义成一个属性,动态获取到所有以 crawl 开头的方法列表。

    如果以后有新的代理网站要抓取,只需要在类 Crawler 类中添加一个以 crawl 开头的方法即可。依照其它几个方法将其定义成生成器,抓取其网站的代理,然后通过 yield返回代理即可。这样方便扩展,也不用关心其他部分的实现逻辑。

    代理的添加也比较灵活,免费代理和付费代理都可以添加,付费代理的提取方式也是类似的。

    这里使用到的 utils中 get_page() 方法代码如下:

     1 import requests
     2 from requests.exceptions import ConnectionError
     3 
     4 base_headers = {
     5     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
     6     'Accept-Encoding': 'gzip, deflate, br',
     7     'Accept-Language': 'zh-CN,zh;q=0.9'
     8 }
     9 
    10 def get_page(url, options={}):
    11     """
    12     抓取代理网站的IP
    13     :param url: 代理网站网址
    14     :param options: 请求头部信息
    15     :return: 获取成功返回网页源代码,失败返回None
    16     """
    17     headers = dict(base_headers, **options) # base_headers 与 options 的键重复,则更新base_headers中的值
    18     try:
    19         response = requests.get(url, headers)
    20         print("抓取成功", url, response.status_code)
    21         if response.status_code == 200:
    22             return response.text
    23     except ConnectionError:
    24         print("抓取失败", url)
    25         return None

    接下来定义一个 Getter 类,用来动态地调用所有以 crawl 开头的方法,获取抓取到的代理,将其加入到数据库存储起来:

     1 import sys
     2 from .db import RedisClient
     3 from .crawler import Crawler
     4 
     5 POOL_UPPER_THRESHOLD = 10000
     6 class Getter():
     7     def __init__(self):
     8         self.redis = RedisClient()
     9         self.crawler = Crawler()
    10 
    11     def is_over_threshold(self):
    12         """
    13         判断是否到达代理池限制
    14         :return:
    15         """
    16         if self.redis.count() >= POOL_UPPER_THRESHOLD:
    17             return True
    18         else:
    19             return False
    20 
    21     def run(self):
    22         print('获取器开始执行')
    23         if not self.is_over_threshold():
    24             for callback_label in range(self.crawler.__CrawlFuncCount__):
    25                 callback = self.crawler.__CrawlFunc__[callback_label]
    26                 # 获取代理
    27                 proxies = self.crawler.get_proxies(callback)
    28                 sys.stdout.flush()
    29                 for proxy in proxies:
    30                     self.redis.add(proxy)

    Getter类是获取器类,变量 POOL_UPPER_THRESHOLD 表示的是代理池的最大数量,数量可以灵活配置。is_over_threshold() 方法用于判断代理池是否到达容量阈值,在这个方法中调用了 RedisClient类的 count() 方法来获取代理的数量进行判断,如果数量到达阈值,就返回True,否则返回 False,不想加这个限制的话,可将些方法都返回 True。

    接下来定义的 run() 方法,首先判断代理池是否到达阈值,然后调用 Crawler类的 __CrawFunc__ 属性,获取到所有以 crawl 开头的方法列表,依次通过 get_proxies() 方法调用,得到各个方法抓取到的代理,然后利用 RedisClient 的 add() 方法加入数据库,这样就完成了获取模块的工作。

    3.3、 检测模块
    对所有代理进行多轮检测,代理检测可用,分数就设置为 100,代理不可用,分数减1,这样可以实时改变每个代理的可用情况。要获取有效代理,只需要获取分数最高的代理即可。

    由于代理数量多,为了提高代理请求检测效率,这里使用异步请求库 aiohttp 进行检测。requests是同步请求库,发出请求后需要得到网页加载完成后才能继续往下执行,这个过程会阻塞等待响应,如果服务器响应很慢的话,在requests请求这个等待过程,可以调度其他请求或者进行网页解析。

    异步请求库就解决了这个问题,类似 JavaScript 中的回调,即在请求发出后,程序可继续执行做其它的事情,当响应到达时,程序再去处理这个响应。于是程序没有被阻塞,可充分利用时间和资源,大大提高效率。

    测试模块的代码实现如下:

     1 import asyncio
     2 import aiohttp
     3 import time, sys
     4 
     5 try:
     6     from aiohttp import ClientError
     7 except:
     8     from aiohttp import ClientProxyConnectionError as ProxyConnetionError
     9 from .db import RedisClient
    10 
    11 VALID_STATUS_CODES = [200, 302]
    12 TEST_URL = 'http://www.baidu.com'
    13 BATCH_TEST_SIZE = 100
    14 class Tester(object):
    15     def __init__(self):
    16         self.redis = RedisClient()
    17 
    18     async def test_single_proxy(self, proxy):
    19         """
    20         测试单个代理
    21         :param proxy: 代理IP
    22         :return:
    23         """
    24         conn = aiohttp.TCPConnector(verify_ssl=False)
    25         async with aiohttp.ClientSession(connector=conn) as session:
    26             try:
    27                 if isinstance(proxy, bytes):
    28                     proxy = proxy.decode('utf-8')
    29                 real_proxy = 'http://' + proxy
    30                 print('正在测试', proxy)
    31                 async with session.get(TEST_URL, proxy=real_proxy, timeout=15, allow_redirects=False) as response:
    32                     if response.status in VALID_STATUS_CODES:
    33                         self.redis.max(proxy)
    34                         print('代理可用', proxy)
    35                     else:
    36                         self.redis.decrease(proxy)
    37                         print('请求响应不合法', response.status, 'IP', proxy)
    38             except (ClientError, aiohttp.ClientConnectionError, asyncio.TimeoutError, AttributeError):
    39                 self.redis.decrease(proxy)
    40                 print('代理请求失败', proxy)
    41 
    42     def run(self):
    43         """
    44         测试主函数
    45         :return:
    46         """
    47         print('测试器开始运行')
    48         try:
    49             count = self.redis.count()
    50             print('当前剩余', count, '个代理')
    51             for i in range(0, count, BATCH_TEST_SIZE):
    52                 start = i
    53                 stop = min(i+BATCH_TEST_SIZE, count)
    54                 print('正在测试', start + 1, '-' ,stop, '个代理')
    55                 test_proxies = self.redis.batch(start, stop)
    56                 loop = asyncio.get_event_loop()
    57                 tasks = [self.test_single_proxy(proxy) for proxy in test_proxies]
    58                 loop.run_until_complete(asyncio.wait(tasks))
    59                 sys.stdout.flush()
    60                 time.sleep(5)
    61         except Exception as e:
    62             print('测试器发生错误', e.args)

    在这个模块中,类 Tester中 __init__()方法建立了一个 RedisClient对象,该对象可在类中其它地方使用。接下来定义一个test_single_proxy() 方法,这个方法检测单个代理的可用情况,参数是被检测的代理。在定义这个方法时加了关键字 async,表示是异步的。在方法内部创建了 aiohttp的 ClientSession 对象,该对象类似于 requests 的 Session 对象,可直接调用该对象的get()方法访问页面。这里代理的设置通过 proxy 参数传递给 get() 方法,在请求方法前面也要加上 async 关键字来标明是异步请求,这是 aiohttp 使用时的常见写法。

    测试连接由 TEST_URL 确定,可针对不同的网站设置该值。同一个代理IP在不同的网站请求,可能得到的返回结果是不一样的。这个TEST_URL 设置为一个稳定的网站(如百度)时,代理池比较通用。

    连接状态码由 VALID_STATUS_CODES变量定义,以列表的形式存在,包含正常状态码,某些目标网站可能会出现其他状态码,可进行相应设置。通过判断响应状态码是否在 VALID_STATUS_CODES 列表里,来判断代理是否可用,可用就调用 RedisClient 的 max()方法将代理分数设为 100,否则调用 decrease() 方法将代理分数减1,如果出现异常,同样将代理分数减1。

    另外一个常量 BATCH_TEST_SIZE 是批量测试的最大值,这里设置一批测试最多 100 个,可避免代理池过大时一次性测试全部代理导致内存开销过大。

    在 run() 方法里获取所有代理列表,使用 aiohttp 分配任务,启动运行,这样就进行了异步检测。aiohttp官方网站示例:http://aiohttp.readthedocs.io/。

    3.4、 接口模块
    要获取存储在数据库中的代理,可使用 RedisClient类连接Redis,然后调用 random() 方法,这样做虽然效率高,但是会有些问题。比如别人使用这个代理池,需要让他知道Redis连接用户名和密码,这样不安全。如果代理池在远程服务器上运行,但远程服务器的Redis 只允许本地连接,那么就不能远程直连Redis来获取代理。如果爬虫的主机没有连接 Redis 模块,或者爬虫不是由 Python语言编写的,就无法使用 RedisClient 来获取代理。如果 RedisClient 类或者数据结构有更新,爬虫端必须同步这些更新。这样是非常麻烦的。

    考虑到上面这些因素,可将代理池作为一个独立的服务运行,增加一个接口,以 Web API 的形式展示可用代理。这样获取代理只需要请求接口即可,上面的问题也可以避免。下面使用轻量级的库 Flask 来实现这个接口模块。代码如下所示:

     1 from flask import Flask, g
     2 from .db import RedisClient
     3 __all__ = ['app']
     4 app = Flask(__name__)
     5 def get_conn():
     6     if not hasattr(g, 'redis'):
     7         g.redis = RedisClient()
     8         return g.redis
     9 
    10 @app.route('/')
    11 def index():
    12     return '<h2>Welecome to Proxy Pool System</h2>'
    13 
    14 @app.route('/random')
    15 def get_proxy():
    16     """
    17     获取随机可用代理
    18     :return: 随机代理
    19     """
    20     conn = get_conn()
    21     return conn.random()
    22 
    23 @app.route('/count')
    24 def get_counts():
    25     """
    26     获取代理池总量
    27     :return: 代理池总量
    28     """
    29     conn = get_conn()
    30     return str(conn.count())
    31 
    32 if __name__ == '__main__':
    33     app.run()

    这里声明了一个 Flask 对象,定义了3个接口,分别是首页、随机代理面、获取数量页。运行后,Flask 会启动一个Web服务,只需要访问对应接口即可获取到可用代理。

    3.5、 调度模块
    调度模块就是调用前面定义的3个模块,将这3个模块通过多线程形式运行起来,代码如下:

     1 import time
     2 from multiprocessing import Process
     3 from .api import app
     4 from .getter import Getter
     5 from .tester import Tester
     6 
     7 TESTER_CYCLE = 20
     8 GETTER_CYCLE = 30
     9 TESTER_ENABLED = True
    10 GETTER_ENABLED = True
    11 API_ENABLED = True
    12 
    13 class Scheduler():
    14     def schedule_tester(self, cycle=TESTER_CYCLE):
    15         """
    16         定时测试代理
    17         :return:
    18         """
    19         tester = Tester()           # 初始化测试实例
    20         while True:
    21             print('测试器开始运行')
    22             tester.run()            # 开始测试
    23             time.sleep(cycle)       # 休眠一段时间后进行下一次测试
    24 
    25     def schedule_getter(self, cycle=GETTER_CYCLE):
    26         """
    27         定时获取代理
    28         :param cycle: 时间间隔
    29         :return:
    30         """
    31         getter = Getter()           # 初始化获取实例
    32         while True:
    33             print('开始抓取代理')
    34             getter.run()            # 抓取代理
    35             time.sleep(cycle)       # 休眠一段时间后下等下一次抓取
    36 
    37     def schedule_api(self):
    38         """
    39         开启API
    40         :return:
    41         """
    42         app.run(API_HOST, API_PORT) # 调用 flask 在页面上显示一个随机 IP
    43 
    44     def run(self):
    45         print('代理池开始运行')
    46 
    47         if TESTER_ENABLED:
    48             tester_process = Process(target=self.schedule_tester)   # 设置启动目标
    49             tester_process.start()                                  # 启动
    50 
    51         if GETTER_ENABLED:
    52             getter_process = Process(target=self.schedule_getter)
    53             getter_process.start()
    54 
    55         if API_ENABLED:
    56             api_process = Process(target=self.schedule_api)
    57             api_process.start()

    在这个调度模块代码中,3个常量TESTER_ENABLED、GETTER_ENABLED、API_ENABLED是布尔型,表示测试模块、获取模块、接口模块的开关。默认都是True,表示模块开启。

    启动入口是 run() 方法,这个方法分别判断3个模块的开关。如果开关开启,启动时程序就新建一个Process进程,设置好启动目标,然后调用start() 方法运行。3个进程可以并行执行,互不干扰。

    只需要调用 Scheduler 的 run() 方法即可启动整个代理池。

    3.6、 运行
    下面将代码整合下,让代理运行起来。运行后的代码池在控制台有输出,从输出可以看出可用代理设置为100,不可用代理分数减1。由于当前配置运行在 5555 端口,在浏览器地址栏打开 http://127.0.0.1:5555,可看到代理首页,访问 http://127.0.0.1:5555/random可获取随机可用代理。只要访问此接口即可获取一个随机可用代理。下面的代码是获取代理的总开关,运行这段代码就开始获取代理。

     1 from proxypool.scheduler import Scheduler
     2 import sys
     3 import io
     4 
     5 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
     6 
     7 def main():
     8     try:
     9         s = Scheduler()     # 初始化
    10         s.run()             # 调用 run() 方法开始运行
    11     except:
    12         main()
    13 if __name__ == '__main__':
    14     main()

    获取到的代理保存在Redis数据库中,可以通过访问 http://127.0.0.1:5555/random 来获取随机可用代理。访问 http://127.0.0.1:5555是首页页面。获取一个随机代理的代码如下所示:

     1 import os, sys, requests
     2 from bs4 import BeautifulSoup as bs
     3 
     4 dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     5 sys.path.insert(0, dir)
     6 
     7 PROXY_POOL_URL = 'http://127.0.0.1:5555/random'
     8 TEST_URL = 'http://docs.jinkan.org/docs/flask/'
     9 
    10 def get_proxy():
    11     """
    12     在本地flask生成的网站上获取一个随机可用的 IP
    13     :return: 代理IP
    14     """
    15     r = requests.get(PROXY_POOL_URL)
    16     proxy = bs(r.text, 'lxml').get_text()
    17     return proxy
    18 
    19 def crawl(url, proxy):
    20     """
    21     测试代理是否用
    22     :param url: 测试的目标网站
    23     :param proxy: 被测试的代理IP
    24     :return: 目标网站的源代码
    25     """
    26     proxies = {'http': proxy, 'https': proxy}
    27     try:
    28         r = requests.get(url, proxies=proxies)
    29         return r.text
    30     except requests.exceptions.ConnectionError as e:
    31         print('Error', e.args)
    32 
    33 def main():
    34     proxy = get_proxy()
    35     html = crawl(TEST_URL, proxy)
    36     print(html)
    37 
    38 if __name__ == '__main__':
    39     main()

    最后通过一个随机的代理IP去请求指定网址的代码如下所示:

     1 import requests
     2 from example import get_proxy
     3 #from ..proxypool.setting import TEST_URL
     4 TEST_URL = 'https://www.baidu.com'
     5 
     6 proxy = get_proxy()     # 获取代理IP
     7 
     8 proxies = {
     9     'http': 'http://' + proxy,
    10     'https:': 'https://' + proxy
    11 }
    12 
    13 print(TEST_URL)
    14 response = requests.get(TEST_URL, proxies=proxies, verify=False)
    15 if response.status_code == 200:
    16     print('Successfully')
    17     print(response.text)

    三、 使用代理爬取微信公众号文章
    链接是:https://weixin.sogou.com
    目标:利用代理爬取微信公众号文章,提取正文、发表日期、公众号等内容,将结果保存到MySQL数据库

    需要用到代理池,要用到的Python库有:aiohttp、requests、redis-py、pyquery、Flask、PyMySQL。

    1、 爬取分析
    搜狗对微信公众平台的公众号和文章做了整合。可通过上面的链接搜索到相关的公众号和文章。例如搜索 python,可以搜索到最新的文章。点击搜索后,搜索结果的URL中有很多无关GET请求参数,将无关的参数去掉,只保留 type 和 query 参数即可,例如https://weixin.sogou.com/weixin?type=2&query=python,类型为2 代表搜索微信文章,query为python代表搜索关键词为python。

    下拉网页,点击下一页即可翻页。要注意的是,没有登录只能看到10页的内容,登录后可以看到100页的内容。如果爬取更多内容,就需要登录并使用Cookies来爬取。搜狗微信的反爬能力很强,如连接刷新,站点就会弹出验证码页面,如图1-1所示。
    图1-1  验证码页面
                 图1-1 验证码页面

    这时网络请求出现了302跳转,返回状态码为302,跳转的链接开头为https://weixin.sogou.com/antispider/,这是一个反爬虫的验证页面。所以基本可以确定,如果服务器返回状态码是302而非200,则IP访问次数太高,IP被封禁,这些请求就是失败了。这种情况可以选择识别验证码,也可使用代理直接切换IP。

    遇到这情况,这次使用代理切换IP。代理使用前面搭建的代理池,还要更改检测的URL为搜狗微信的站点。对于反爬能力很强的网站,遇到这种返回状态需要重试。所以要采用另一种反爬方式,借助数据库构造一个爬取队列,待爬取的请求都放到队列里,如果请求失败了重新放回队列,就会重新调度爬取。

    这里采用 Redis 的队列数据结构,新的请求加入队列,有需要重试的请求也放回队列。调度时如果队列不为空,就把一个个请求取出来执行,得到响应后再进行解析,提取出想要的结果。

    这次采用MySQL存储,使用pymysql库,将抓取结果构造为一个字典,实现动态存储。

    经过上述分析,这次实现的功能有如下几点:
    修改代理池检测链接为搜狗微信站点;
    构造 Redis 爬取队列,用队列实现请求的存取;
    实现异常处理,失败的请求重新加入队列;
    实现翻页和提取文章列表,并把对应的请求加入队列;
    实现微信文章的信息提取;
    将提取的信息保存到MySQL。


    2、 请求构造
    首先实现一个请求 Request 的数据结构,这个请求要包含一些必要信息,如请求链接、请求方式、请求头、超时时间等。此外,对于某个请求,还需要实现对的方法来处理它的响应,所以要再加一个 Calllback回调函数。每次翻页请求需要代理来实现,所以还需要一个参数 NeedProxy。如果一个请求失败次数太多,就不再重新请求,还需要加失败次数的记录。

    这些字段都需要作为Request的一部分,组成一个完整的Request对象放入队列去调度,这样从队列获取出来的时候直接执行这个Request对象就行。

    使用继承requests库中的 Request 对象的方式来实现这个数据结构。requests 库中已经有了 Request对象,它将请求 Request作为一个整体对象去执行,得到响应后再返回。在requests库中的get()、post() 等方法都是通过执行Request对象实现的。

    先来看一下 Request对象的源码:

     1 class Request(RequestHooksMixin):
     2     def __init__(self,
     3             method=None, url=None, headers=None, files=None, data=None,
     4             params=None, auth=None, cookies=None, hooks=None, json=None):
     5 
     6         # Default empty dicts for dict params.
     7         data = [] if data is None else data
     8         files = [] if files is None else files
     9         headers = {} if headers is None else headers
    10         params = {} if params is None else params
    11         hooks = {} if hooks is None else hooks
    12 
    13         self.hooks = default_hooks()
    14         for (k, v) in list(hooks.items()):
    15             self.register_hook(event=k, hook=v)
    16 
    17         self.method = method
    18         self.url = url
    19         self.headers = headers
    20         self.files = files
    21         self.data = data
    22         self.json = json
    23         self.params = params
    24         self.auth = auth
    25         self.cookies = cookies

    这是 requests 库中 Request 对象的构造方法。这个 Request包含了请求方式、请求链接、请求头几个属性,但是相比前面分析的还差了几个。另外还需要实现一个特定的数据结构,在原先的基础上加入上文所提到的额外几个属性。这里需要继承 Request 对象重新实现一个请求,将这个类定义为 WeixinRequest,代码如下所示:

     1 from requests import Request
     2 TIMEOUT = 10
     3 class WeixinRequest(Request):
     4     def __init__(self, url, callback, method='GET', headers=None, need_proxy=False, fail_time=0,
     5                  timeout=TIMEOUT):
     6         Request.__init__(self, method, url, headers)
     7         self.callback = callback
     8         self.need_proxy = need_proxy
     9         self.fail_time = fail_time
    10         self.timeout = timeout

    这段代码中实现了 WeixinRequest 数据结构。在__init__()方法中先调用 Request 的 __init__()方法,然后加入额外的几个参数,定义为 callback、need_proxy、fail_time、timeout,分别代表回调函数、是否需要代理爬取、失败次数、超时时间。

    可将WeixinRequest作为一个整体来执行,一个个WeixinRequest对象都是独立的,每个请求都有自己的属性。例如,调用它的callback,就可知道这个请求响应应该用什么方法来处理,调用 fail_time 就可知道这个请求失败了多少次,判断失败次数是不是到了阈值,该不该丢弃这个请求。

    3、 请求队列实现
    构造请求队列,实现请求存取。存取就两个操作,一个是存,一个是取,使用Redis的 rpush() 和 lpop() 方法即可。在存取时不能直接存Request对象,Redis里面存的是字符串。在存Request对象前先将其序列化,取出时再将其反序列化,这个过程用pickle模块实现。代码如下所示:

     1 from redis import StrictRedis
     2 from pickle import dumps, loads
     3 from .request import WeixinRequest
     4 
     5 REDIS_HOST = '192.168.64.50'
     6 REDIS_PORT = 6379
     7 REDIS_PASSWORD = None
     8 REDIS_KEY = 'weixin'
     9 
    10 class RedisQueue():
    11     def __init__(self):
    12         """
    13         初始化Redis
    14         """
    15         self.db = StrictRedis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD)
    16 
    17     def add(self, request):
    18         """
    19         向队列添加序列化后的Request
    20         :param request: 请求对象
    21         :return: 添加结果
    22         """
    23         if isinstance(request, WeixinRequest):
    24             return self.db.rpush(REDIS_KEY, dumps(request))
    25         return False
    26 
    27     def pop(self):
    28         """
    29         取出一个Request并反序列化
    30         :return: Request or None
    31         """
    32         if self.db.llen(REDIS_KEY):
    33             return loads(self.db.lpop(REDIS_KEY))
    34         else:
    35             return False
    36 
    37     def clear(self):
    38         """删除数据库"""
    39         self.db.delete(REDIS_KEY)
    40 
    41     def empty(self):
    42         """判断队列是否为空"""
    43         return self.db.llen(REDIS_KEY) == 0
    44 
    45 if __name__ == '__main__':
    46     db = RedisQueue()
    47     start_url = 'https://www.baidu.com'
    48     weixin_request = WeixinRequest(url=start_url, callback='hello', need_proxy=True)
    49     db.add(weixin_request)
    50     request = db.pop()
    51     print(request)
    52     print(request.callback, request.need_proxy)

    在代码中 RedisQueue 类中的初始化方法中初始化了一个 StrictRedis对象。接着实现了 add() 方法,先判断 Request 的类型,如果是 WeixinRequest,就用 pickle 的 dumps() 方法序列化,然后调用 rpush() 方法加入队列。调用 pop() 方法将从队列中取出,再调用 pickle的 loads() 方法将其转化为 WeixinRequest 对象。empty() 方法返回队列是否为空,只要判断队列长度是否为 0 即可。

    4、 修改代理池
    在生成请求开始爬取之前,先找一些可用代理。将代理池检测的URL修改成搜狗微信站点,将被搜狗微信封禁的代理剔除掉,留下可用代理。将代理池的中的 TEST_URL 修改为 https://weixin.sogou.com/weixin?type=2&query=python,被本站点封禁的代理就会减分,正常请求的代理就会赋值为100,最后留下可用代理。

    修改后将获取模块、检测模块、接口模块的开关都设置为True,让代理池先运行一会。这时数据库中留下的100分代理就是针对搜狗微信的可用代理。现在访问代理接口,接口设置为5555,访问 http://127.0.0.1:5555/random即可获取随机可用代理。

    现在来定义一个函数get_proxy()获取随机代理,该函数封装在Spider() 类中。

     1 import requests
     2 PROXY_POOL_URL = 'http://192.168.64.50:5555/random'
     3 def get_proxy(self):
     4     """
     5     从代理池获取代理
     6     :return:
     7     """
     8     try:
     9         response = requests.get(PROXY_POOL_URL)
    10         if response.status_code == 200:
    11             print('Get Proxy', response.text)
    12             return response.text
    13         return None
    14     except requests.ConnectionError:
    15         return None


    5、 第一个请求
    前面的工作准备好后,下面就构造一个请求放到队列里以供调度。这里定义一个Spider类,前面的 get_proxy() 函数封装在这个类中,接下来实现start()方法,代码如下所示:

     1 from requests import Session
     2 from redisdb import RedisQueue
     3 from mysql import MySQL
     4 from request import WeixinRequest
     5 from urllib.parse import urlencode
     6 import requests
     7 
     8 PROXY_POOL_URL = 'http://192.168.64.50:5555/random'
     9 
    10 class Spider():
    11     base_url = 'https://weixin.sogou.com/weixin'
    12     keyword = 'python'
    13     headers = {
    14         'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
    15         'Accept-Encoding': 'gzip, deflate, br',
    16         'Accept-Language': 'zh-CN,zh;q=0.9',
    17         'Cache-Control': 'max-age=0',
    18         'Connection': 'keep-alive',
    19         'Cookie': 'CXID=339A2898FD48047A6358656B1D96964B; SUID=53C8B0755B6358656B6ED3DA000C9B6B; SUV=005D2CADB63586565BF21BEFCBBE3709; pgv_pvi=8799300608; ssuid=6762987792; ld=Elllllllll2t033xlllllVhER71lVlllTCKCjZllll9lVlllVllll5@@@@@@@@@@; LSTMV=51%2C491; LCLKINT=18260; ABTEST=0|1557396157|v1; weixinIndexVisited=1; ppinf=5|1557989778|1559199378|dHJ1c3Q6MToxfGNsaWVudGlkOjQ6MjAxN3x1bmlxbmFtZTozNzolRTclODMlQkQlRTclODElQUIuJUU4JUJFJUI5JUU1JTlGJThFfGNydDoxMDoxNTU3OTg5Nzc4fHJlZm5pY2s6Mzc6JUU3JTgzJUJEJUU3JTgxJUFCLiVFOCVCRSVCOSVFNSU5RiU4RXx1c2VyaWQ6NDQ6bzl0Mmx1R3RUUlI5TjZ4TnlHbU1lM3luRnpUUUB3ZWl4aW4uc29odS5jb218; pprdig=pDwOzQhJnqC3Kr9aBUzytoY1poJ3CRl9cWXScYt2JCLOhSqZrBEDERHkOrt1190yzK_IKSdAPdUFyo5AOvwgG-XzKCvBh7JJs2Xhg2_LmZA_kp7MvDaiyfXumeWcNjtVRVbkbKYutAzfIPkg1sYOfjcc8L_VCyv4lnLLT8sd8Kc; sgid=29-40667169-AVzdAZL0PsNicLSKyFy900f0; SNUID=CBFF0A6FD8DD5F2BCFC93687D80ADC83; ppmdig=155805432800000015f265f013d784e1a66564ab6b844c13; IPLOC=CN5100; sct=42; JSESSIONID=aaa_Q0ixqZPPi_V9ND1Qw',
    20         'Host': 'weixin.sogou.com',
    21         'Upgrade-Insecure-Requests': '1',
    22         'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
    23     }
    24     session = Session()
    25     queue = RedisQueue()
    26     mysql = MySQL()
    27 
    28     def get_proxy(self):...     # 该函数代码见前面
    29 
    30     def start(self):
    31     """
    32     初始化工作
    33     :return:
    34     """
    35     # 全局更新Headers
    36     self.session.headers.update(self.headers)
    37     start_url = self.base_url + "?" + urlencode({'query': self.keyword, 'type': 2})
    38     weixin_request = WeixinRequest(url=start_url, callback=self.parse_index, need_proxy=True)
    39     # 调度第一个请求,添加到调度队列
    40     self.queue.add(weixin_request)


    在这个Spider类中,设置了较多的全局变量,如keyword设置为python,headers为请求头。请求头信息可在浏览器里登录账号后,在开发者工具里将请求头复制出来,一定要带上Cookie字段,这样才能爬取100页的内容。接着初始了 Session 和 RedisQueue对象,它们分别用来执行请求和存储请求。另外还初始化了MySQL对象,这个类在后面进行定义。

    在start() 方法中,首先全局更新 headers,使得所有请求都能应用 Cookies。接着构造一个起始URL:https://weixin.sogou.com/weixin?type=2&query=python,随后修改 URL 构造了一个 WeixinRequest对象。回调函数是 Spider 类的parse_index()方法,也当这个请求成功后调用parse_index()来处理和解析。need_proxy 参数设置为 True,代表执行这个请求需要用到代理。随后调用 RedisQueue的add()方法,将这个请求加入队列,等待调度。

    6、 调度请求
    加入第一个请求后,调度开始。首先从队列中取出这个请求,将它的结果解析出来,生成新的请求加入队列,然后拿出新的请求,再生成新的请求加入队列,这样循环执行,直到队列中没有请求,则代表爬取结束。代码如下所示:

     1 VALID_STATUSES = [200]
     2 def schedule(self):
     3     """
     4     调度请求
     5     :return:
     6     """
     7     while not self.queue.empty():           # 数据库队列不为空
     8         weixin_request = self.queue.pop()   # 从数据库取出一个WeixinRequest对象,此时 weixin_request 是 WeixiRequest类的实例(对象)
     9         callback = weixin_request.callback  # 获取回调函数方法
    10         print('Schedule', weixin_request.rul)
    11         response = self.request(weixin_request)     # 调用Spider类的request方法,该方法返回的是响应
    12         if response and response.status_code in VALID_STATUSES:
    13             results = list(callback(response))      # 调用回调函数,回调函数指向两个,分别是:parse_detail()和parse_index()
    14             if results:
    15                 for result in results:
    16                     print('New Result', type(result))
    17                     if isinstance(result, WeixinRequest):   #
    18                         self.queue.add(result)      # 调用 RedisQueue 类的add方法添加到数据库队列中
    19                     if isinstance(result, dict):    # 如果返回对象是字典类型,就将文章保存到MySQL数据库中
    20                         self.mysql.insert('articles', result)   # 调用MySQL类中的insert方法
    21             else:
    22                 self.error(weixin_request)  # 调用Spider类中的error函数
    23         else:
    24             self.error(weixin_request)

    这里的 schedule() 方法,其内部是一个循环,循环的判断是队列不为空。当队列不空就调用 pop() 方法取出下一个请求,调用Spider类的 request() 方法执行这个请求,request() 方法实现代码如下:

     1 from requests import ReadTimeout, ConnectionError
     2 def request(self, weixin_request):
     3     """
     4     执行请求
     5     :param weixin_request: 请求
     6     :return: 响应
     7     """
     8     try:
     9         if weixin_request.need_proxy:
    10             proxy = self.get_proxy()
    11             if proxy:
    12                 proxies = {
    13                     'http': 'http://' + proxy,
    14                     'https': 'https://' + proxy,
    15                 }
    16                 return self.session.send(weixin_request.prepare(),      # 调用的是 Request类的 prepare方法
    17                                          timeout=weixin_request.timeout, allow_redirects=False, proxies=proxies)
    18         return self.session.send(weixin_request.prepare(), timeout=weixin_request.timeout, allow_redirects=False)
    19     except (ConnectionError, ReadTimeout) as e:
    20         print(e.args)
    21         return False

    在request()中,首先判断请求是否需要代理,如果需要就调用前面定义的 get_proxy() 方法获取代理,然后调用Session的send()方法执行这个请求。这里请求调用了 prepare() 方法转化为 Prepared Request,该用法的具体信息参考 https://2.python-requests.org//en/master/user/advanced/#prepared-requests,同时设置 allow_redirects 为 False,timeout是该请求的超时时间,最后响应返回。

    执行 request() 方法后会得到两种结果:一种是False,请求失败,连接错误;另一种是 Response对象,还需判断状态码,如果状态码合法,就进行解析,否则重新将请求加回队列。状态码合法就调用 WeixinRequest的回调函数进行解析,这里回调函数是 parse_index(),其代码如下所示:

     1 from pyquery import PyQuery as pq
     2 def parse_index(self, response):
     3     """
     4     解析索引页面
     5     :param response: 响应
     6     :return: 新的响应
     7     """
     8     doc = pq(response.text)
     9     # 找class为news-box标签下的class为news-list标签下的li标签下的class为txt-box标签下的h3标签下的a标签
    10     items = doc('.news-box .news-list li .txt-box h3 a').items()
    11     for item in items:
    12         # 循环执行完后,再去执行下面的 if 语句,也就是当前页请求完后,再请求下一页
    13         url = item.attr('href')     # 获取文章的链接
    14         weixin_request = WeixinRequest(url=url, callback=self.parse_detail)    # parse_detail是Spider类中的方法,通过回调函数获取页面的详细内容
    15         yield weixin_request
    16     # 获取下一页的标签连接,该标签是a标签,有id属性。值是 sogou_next
    17     next = doc('#sogou_next').attr('href')
    18     if next:
    19         url = self.base_url + str(next)     # 拼接 url,请求这个 url 切换到下一页
    20         # 初始化 WeixinRequest,并传入下一页的连接,回调函数调用自身,这时进入到下一页的爬取
    21         weixin_request = WeixinRequest(url=url, callback=self.parse_index, need_proxy=True)
    22         yield weixin_request

    这个parse_index()方法做了两件事,一件事是获取本页的所有微信文章链接,另一件事是获取下一页的链接,再构造成WeixinRequest对象后yield返回。然后 schedule()方法将返回的结果进行遍历,利用 isinstance()方法判断返回结果,如果返回结果是WeixinRequest,就其重新加入队列。

    这时,第一次循环结束。while循环继续执行。队列已经包含第一页内容的文章详情页请求和下一页的请求,所以第二次循环得到下一个请求就是文章详情页的请求,程序重新调用 request() 方法获取其响应,然后调用其对应的回调函数解析。解析详情页的回调函数方法一样,这次是 parse_detail() 方法,该方法也封装在 Spider 类,实现代码如下所示:

     1 def parse_detail(self, response):
     2     """
     3     解析详情页
     4     :param response: 响应
     5     :return: 微信公众号文章
     6     """
     7     doc = pq(response.text)     # 将文本内容转化为 pyquery 对象
     8     # 将文章内容构造成字典,以便于保存到数据库
     9     data = {
    10         'title': doc('.rich_media_title').text(),
    11         'content': doc('.rich_media_content').text(),
    12         'date': doc('#publish_time').text(),
    13         'nickname': doc('#js_profile_qrcode > div > strong').text(),
    14         'wechat': doc('#js_profile_qrcode > div > p:nth-child(3) > span').text()
    15     }
    16     yield data

    这个 parse_detail() 方法解析微信文章详情页内容,提取出标题、正文文本、发布日期、发布人昵称、微信公众号名称,将这些信息组合成一个字典返回。

    返回结果后还需要判断类型,如果是字典类型,程序就调用 mysql 对象的 insert() 方法将数据存入数据库。这时,第二次循环执行完成。第三次循环、第四次循环,循环往复,每个请求都有各自的回调函数,索引页解析完成后继续生成后续请求,详情页解析完成后返回结果以便存储,直到抓取完毕。到此,整个调度基本完成。下面进一步完善整个Spider类代码,完整Spider类代码如下所示:

      1 from requests import Session
      2 from .redisdb import RedisQueue
      3 from .mysql import MySQL
      4 from .request import WeixinRequest
      5 from urllib.parse import urlencode
      6 import requests
      7 from pyquery import PyQuery as pq
      8 from requests import ReadTimeout, ConnectionError
      9 
     10 PROXY_POOL_URL = 'http://192.168.64.50:5555/random'
     11 VALID_STATUSES = [200]
     12 MAX_FAILED_TIME = 20
     13 
     14 class Spider():
     15     base_url = 'https://weixin.sogou.com/weixin'
     16     keyword = 'python'
     17     headers = {
     18         'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
     19         'Accept-Encoding': 'gzip, deflate, br',
     20         'Accept-Language': 'zh-CN,zh;q=0.9',
     21         'Cache-Control': 'max-age=0',
     22         'Connection': 'keep-alive',
     23         'Cookie': 'CXID=339A2898FD48047A5738650B1D96964B; SUID=53C8B0755B68860A5B6ED3DA000C9B6B; SUV=005D2CADB7DD27195BF21BEFCBBE3709; pgv_pvi=8799300608; ssuid=6762987792; ld=Elllllllll2t033xlllllVhER71lllllTCKCjZllll9lllllVllll5@@@@@@@@@@; LSTMV=51%2C491; LCLKINT=18260; ABTEST=0|1557396157|v1; weixinIndexVisited=1; ppinf=5|1557989778|1559199378|dHJ1c3Q6MToxfGNsaWVudGlkOjQ6MjAxN3x1bmlxbmFtZTozNzolRTclODMlQkQlRTclODElQUIuJUU4JUJFJUI5JUU1JTlGJThFfGNydDoxMDoxNTU3OTg5Nzc4fHJlZm5pY2s6Mzc6JUU3JTgzJUJEJUU3JTgxJUFCLiVFOCVCRSVCOSVFNSU5RiU4RXx1c2VyaWQ6NDQ6bzl0Mmx1R3RUUlI5TjZ4TnlHbU1lM3luRnpUUUB3ZWl4aW4uc29odS5jb218; pprdig=pDwOzQhJnqC3Kr9aBUzytoY1poJ3QAl9cWXScYt2JCLOhSqZrBEDERHkOrt1190yzK_IKSdAPdUFyo5AOvwgG-XzKCvBh7JJs2Xhg2_LmZA_kp7MvDaiyfXumeWcNjtVRVbkbKYutAzfIPkg1sYOfjcc8L_VCyv4lnLLT8sd8Kc; sgid=29-40669169-AVzdCZL0PsNicLSKyFy900f0; SNUID=CBFF0A6FD8DD5F2BCFC93687D80ADC83; ppmdig=155805432800000015f265f013d784e1a66564ab6b844c13; IPLOC=CN5100; sct=42; JSESSIONID=aaa_Q0ixqZPPi_V9ND1Qw',
     24         'Host': 'weixin.sogou.com',
     25         'Upgrade-Insecure-Requests': '1',
     26         'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
     27     }
     28     session = Session()
     29     queue = RedisQueue()
     30     mysql = MySQL()
     31 
     32     def get_proxy(self):
     33         """
     34         从代理池获取代理
     35         :return:
     36         """
     37         try:
     38             response = requests.get(PROXY_POOL_URL)
     39             if response.status_code == 200:
     40                 print('Get Proxy', response.text)
     41                 return response.text
     42             return None
     43         except requests.ConnectionError:
     44             return None
     45 
     46     def start(self):
     47         """
     48         初始化工作
     49         :return:
     50         """
     51         # 全局更新Headers
     52         self.session.headers.update(self.headers)
     53         start_url = self.base_url + "?" + urlencode({'query': self.keyword, 'type': 2})
     54         weixin_request = WeixinRequest(url=start_url, callback=self.parse_index, need_proxy=True)
     55         # 调度第一个请求,添加到高度队列
     56         self.queue.add(weixin_request)
     57 
     58     def parse_index(self, response):
     59         """
     60         解析索引页面
     61         :param response: 响应
     62         :return: 新的响应
     63         """
     64         doc = pq(response.text)
     65         # 找class为news-box标签下的class为news-list标签下的li标签下的class为txt-box标签下的h3标签下的a标签
     66         items = doc('.news-box .news-list li .txt-box h3 a').items()
     67         for item in items:
     68             # 循环执行完后,再去执行下面的 if 语句,也就是当前页请求完后,再请求下一页
     69             url = item.attr('href')     # 获取文章的链接
     70             weixin_request = WeixinRequest(url=url, callback=self.parse_detail)    # parse_detail是Spider类中的方法,通过回调函数获取页面的详细内容
     71             yield weixin_request
     72         # 获取下一页的标签连接,该标签是a标签,有id属性。值是 sogou_next
     73         next = doc('#sogou_next').attr('href')
     74         if next:
     75             url = self.base_url + str(next)     # 拼接 url,请求这个 url 切换到下一页
     76             # 初始化 WeixinRequest,并传入下一页的连接,回调函数调用自身,这时进入到下一页的爬取
     77             weixin_request = WeixinRequest(url=url, callback=self.parse_index, need_proxy=True)
     78             yield weixin_request
     79 
     80     def parse_detail(self, response):
     81         """
     82         解析详情页
     83         :param response: 响应
     84         :return: 微信公众号文章
     85         """
     86         doc = pq(response.text)     # 将文本内容转化为 pyquery 对象
     87         # 将文章内容构造成字典,以便于保存到数据库
     88         data = {
     89             'title': doc('.rich_media_title').text(),
     90             'content': doc('.rich_media_content').text(),
     91             'date': doc('#publish_time').text(),
     92             'nickname': doc('#js_profile_qrcode > div > strong').text(),
     93             'wechat': doc('#js_profile_qrcode > div > p:nth-child(3) > span').text()
     94         }
     95         yield data
     96 
     97     def request(self, weixin_request):
     98         """
     99         执行请求
    100         :param weixin_request: 请求
    101         :return: 响应
    102         """
    103         try:
    104             if weixin_request.need_proxy:
    105                 proxy = self.get_proxy()
    106                 if proxy:
    107                     proxies = {
    108                         'http': 'http://' + proxy,
    109                         'https': 'https://' + proxy,
    110                     }
    111                     return self.session.send(weixin_request.prepare(),      # 调用的是 Request类的 prepare方法
    112                                              timeout=weixin_request.timeout, allow_redirects=False, proxies=proxies)
    113             return self.session.send(weixin_request.prepare(), timeout=weixin_request.timeout, allow_redirects=False)
    114         except (ConnectionError, ReadTimeout) as e:
    115             print(e.args)
    116             return False
    117 
    118     def error(self, weixin_request):
    119         """
    120         错误处理
    121         :param weixin_request: 请求
    122         :return:
    123         """
    124         weixin_request.fail_time = weixin_request.fail_time + 1
    125         print('Request Failed', weixin_request.fail_time, 'Times', weixin_request.url)
    126         if weixin_request.fail_time < MAX_FAILED_TIME:
    127             self.queue.add(weixin_request)      # 小于最大请求失败次数,重新加入数据库队列
    128 
    129     def schedule(self):
    130         """
    131         调度请求
    132         :return:
    133         """
    134         while not self.queue.empty():           # 数据库队列不为空
    135             weixin_request = self.queue.pop()   # 从数据库取出一个WeixinRequest对象,此时 weixin_request 是 WeixiRequest类的实例(对象)
    136             callback = weixin_request.callback  # 获取回调函数方法
    137             print('Schedule', weixin_request.rul)
    138             response = self.request(weixin_request)     # 调用Spider类的request方法,该方法返回的是响应
    139             if response and response.status_code in VALID_STATUSES:
    140                 results = list(callback(response))      # 调用回调函数,回调函数指向两个,分别是:parse_detail()和parse_index()
    141                 if results:
    142                     for result in results:
    143                         print('New Result', type(result))
    144                         if isinstance(result, WeixinRequest):   #
    145                             self.queue.add(result)      # 调用 RedisQueue 类的add方法添加到数据库队列中
    146                         if isinstance(result, dict):    # 如果返回对象是字典类型,就将文章保存到MySQL数据库中
    147                             self.mysql.insert('articles', result)   # 调用MySQL类中的insert方法
    148                 else:
    149                     self.error(weixin_request)  # 调用Spider类中的error函数
    150             else:
    151                 self.error(weixin_request)
    152 
    153     def run(self):
    154         """
    155         程序主入口
    156         :return:
    157         """
    158         self.start()
    159         self.schedule()
    160 
    161 
    162 if __name__ == '__main__':
    163     spider = Spider()
    164     spider.run()

    最后加入的一个run() 方法作为入口,启动时只需要执行Spider的run()方法即可。

    7、 MySQL存储
    调度模块完成后,还要定义一个MySQL类供存储数据。代码实现如下:

     1 import pymysql
     2 
     3 MYSQL_HOST = '192.168.54.50'
     4 MYSQL_PORT = 3508
     5 MYSQL_USER = 'root'
     6 MYSQL_PASSWORD = 'wyic123456'
     7 MYSQL_DATABASE = 'weixin'
     8 
     9 class MySQL():
    10     def __init__(self, host=MYSQL_HOST, username=MYSQL_USER, password=MYSQL_PASSWORD, port=MYSQL_PORT,
    11                  database=MYSQL_DATABASE):
    12         """
    13         MySQL初始化
    14         :param host:
    15         :param username:
    16         :param password:
    17         :param port:
    18         :param database:
    19         """
    20         try:
    21             self.db = pymysql.connect(host, username, password, database, charset='utf8', port=port)
    22             self.cursor = self.db.cursor()
    23         except pymysql.MySQLError as e:
    24             print(e.args)
    25 
    26     def insert(self, table, data):
    27         """
    28         插入数据
    29         :param table: 表名
    30         :param data: 数据
    31         :return:
    32         """
    33         keys = ', '.join(data.keys())
    34         values = ', '.join(['%s'] * len(data))
    35         sql_query = 'insert into %s (%s) values (%s)' % (table, keys, values)
    36         try:
    37             self.cursor.excute(sql_query, tuple(data.values()))
    38             self.db.commit()
    39         except pymysql.MySQLError as e:
    40             print(e.args)
    41             self.db.rollback()

    在MySQL类中,初始化方法初始化了MySQL的连接,需要提供MySQL的用户名、密码、端口、数据库名等信息。数据库名为 weixin,需
    要在数据库中提前创建。创建命令是:
    create database weixin charset utf8;

    insert()方法传入表名和字典即可动态构造SQL,SQL构造之后执行即可插入数据。另外还需要提前在数据库中建立一个数据表,表名是articles,建表的SQL命令是:
    create table articles (
    id int(11) NOT NULL,
    title varchar(255) NOT NULL,
    content text NOT NULL,
    date varchar(255),
    wechat varchar(255) NOT NULL,
    nickname varchar(255) NOT NULL
    ) default charset=utf8;
    alter table articles add primary key (`id`);


    到此,这个爬虫项目基本算是完成了,接下来就是运行这个项目。

    8、运行
    首先运行前面搭建的代理池,待代理池运行一会儿后,再运行Spider类中的主程序入口。代码运行成功,但爬取失败,不知是不是搜狗微信的反爬措施太强而造成的。

  • 相关阅读:
    Digital image processing In C#
    C#数字图像处理(摘录)
    C# P/Invoke中传递数组参数
    字符常用方法(c#)——(待扩展)
    java监控多个线程的实现
    jdbc访问数据库
    java与MSSQL2000连接
    java下的日期函数实现
    MyEclipse中防止代码格式化时出现换行的情况的设置
    java InputStream读取数据问题
  • 原文地址:https://www.cnblogs.com/Micro0623/p/10905193.html
Copyright © 2020-2023  润新知