Python 自建 IP 代理池

发布时间 2023-04-13 22:00:49作者: sinatJ

文章有点儿长,谨慎食用~

python 爬虫在爬取网页内容时,遭遇的最常见的反爬措施就是 ip 限制/封禁,对此最常见的解决方式就是设置 IP 代理池,每次请求时随机使用一个代理 IP 去访问资源。
网上有成熟的代理服务,但是小伙汁的爬虫需求多是非定期的自定义项目,使用付费代理并不划算,遂有了爬取免费代理并测试是否可用,进而构建一个可用代理 IP 池的想法。本项目亦可作为后续网络相关服务的子模块。

版本1:先通过 request 或者 selenium 进行爬取;
版本2(大概率是鸽了~):学习并使用 scrapy 进行爬取;

0 项目逻辑架构

经过在编码过程中不断的修修改改,重重构构,整体逻辑终于是有了一个相对解耦的模式,由于项目相比于大型项目来说还是 just like a toy,所以核心模块就是一个通用的 Spider 父类模块,定义了整体的爬虫逻辑,其余针对特定网页的实例都继承自该 Spider。先看图吧:

  • Spider 是基础爬虫类,定义了一些静态的属性和功能方法
  • spider x 是实例爬虫,每个实例爬虫需要根据自己网页的结构需要,重写 pre_parse()、parse()、get_all_proxies() 方法
  • Proxy Manager 是运行时的代理管理类(目前仅简单提供可用代理的临时存储功能)
  • 项目运行前,将多个对象爬虫实例化后配置在配置文件中,组成爬虫链。这样主函数执行时会串行加载每个爬虫实例并运行其爬取逻辑(之所以不用并行是想着后面的爬虫能利用前面爬虫验证过的代理,所以将那些没有反爬的爬虫实例尽量配在爬虫链的前面)

下面会详细的介绍每个模块的具体实现细节,再介绍之前,让我再 bb 几句吧。这个项目主要是出于个人兴趣,作为一个初入 python 爬虫领域的菜鸟,利用业余时间在拖拖拉拉中写完了这个项目,写的过程中也逐渐学习了一些 python 的高级语法,例如装饰器、自定义异常、多进程异步操作等,收获还是蛮多的。此外,由于免费代理资源本身并不是很稳定,指望通过免费代理资源来构建一个鲁棒的代理池还是有点困难的,所以这个项目更多的还是当学习使用。

食用提醒:

  1. 前置知识
  • 最好还是要对 python requests 库有点了解,包括 请求、jsonpath 解析网页资源 等
  • python 类的继承、多态等
  1. 可能的收获
  • python requests 的使用
  • python 类的使用
  • python 多进程、进程池
  • python 装饰器
  • python 自定义异常类并在程序中手动抛出并处理
  • python 进度条、输出格式个性化定制等
  • 一点点软件工程设计的思想

1 Spider 模块

这个模块一开始写得时候比较简单,但是在后续加入各种各样的实例爬虫后,为了解耦和鲁棒性,功能也在不断的完善,为了阅读方便,先上简单版本代码:

# _*_ coding : utf-8 _*_
"""
定义基础 Spider 类
"""
import sys
import time
import requests
# from wrappers import old_version_fun_wrapper, req_exceed_limit_wrapper, req_respose_none_wrapper
from tqdm import tqdm
from tools import check_proxy_icanhazip, check_proxy_900cha, get_free_proxy
from concurrent.futures import ProcessPoolExecutor, as_completed
import json
from config import *
from custom_exceptions import Request500Exception, TryWithSelfProxyLimitException


class Spider:
    def __init__(self, *args, **kwargs):
        self.url = kwargs.get('url')
        self.headers = kwargs.get('headers')
        self.req_type = kwargs.get('req_type')		# get or post
        self.data = kwargs.get('data')				# for post
        self.proxies = kwargs.get('proxies')
        self.verify = kwargs.get('verify')
        self.day = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())).split(' ')[0].strip()	# time tag
        self.timeout = 3

        self.proxy_try_num = 0      	# 设置使用代理时的全局失败尝试次数
        self.response = None
        self.parse_urls = []			# 代理资源页入口
        self.all_proxies = []			# 爬取到的所有代理
        self.all_proxies_filter = []	# 验证后可用的所有代理

    def pre_parse(self):
        """
        识别当天代理信息资源页面
        """
        pass

    # @req_respose_none_wrapper	# 可暂时忽略
    def parse(self):
        """
        解析代理
        """
        pass

    def get_all_proxies(self):
        """
        获取所有 proxies
        """
        pass

    def update_attrs(self, *args, **kwargs):
        """
        update the spider object's attrs
        :param args:
        :param kwargs:
        :return:
        """
        self.url = self.url if kwargs.get('url') is None else kwargs.get('url')
        self.headers = self.headers if kwargs.get('headers') is None else kwargs.get('headers')
        self.req_type = self.req_type if kwargs.get('req_type') is None else kwargs.get('req_type')
        self.data = self.data if kwargs.get('data') is None else kwargs.get('data')
        self.proxies = self.proxies if kwargs.get('proxies') is None else kwargs.get('proxies')
        self.verify = self.verify if kwargs.get('verify') is None else kwargs.get('verify')
        self.timeout = self.timeout if kwargs.get('timeout') is None else kwargs.get('timeout')

    # @req_exceed_limit_wrapper		# 可暂时忽略
    def update_response(self, *args, **kwargs):
        """
        recontruct a url request, and update the spider's response attribute
        :param args:
        :param kwargs:
        :return:
        """
		if self.data is None:
			self.response = requests.get(self.url, headers=self.headers, timeout=self.timeout, proxies=self.proxies, verify=self.verify)
		else:
			self.response = requests.post(self.url, headers=self.headers, data=self.data, timeout=self.timeout, proxies=self.proxies, verify=self.verify)

        return self.response

    def filter_all_proxies_mp(self):
        """
        测试代理 ip 可用性
        多进程处理
        """
        self.all_proxies_filter = dict()

        # 进程池
        pool = ProcessPoolExecutor(max_workers=50)
        all_task = [pool.submit(check_proxy_900cha, proxy) for proxy in self.all_proxies]
        for future in tqdm(as_completed(all_task), total=len(all_task), file=sys.stdout, desc='[{}] checking proxies...'.format(self.__class__.__name__)):
            res, proxy = future.result()
            if res:
                self.all_proxies_filter['{}:{}'.format(proxy['ip'], proxy['port'])] = proxy

        self.all_proxies_filter = self.all_proxies_filter.values()
        return self.all_proxies_filter

    def save_to_txt(self, file_name, all_proxies, add_day_tag=True):
        """
        存文件
        """
        if not os.path.isdir(os.path.dirname(file_name)):
            os.makedirs(os.path.dirname(file_name))
        if add_day_tag:
            file_name = file_name.split('.')[0] + '_{}.'.format(self.day.replace('-', '_')) + file_name.split('.')[-1]
        with open(file_name, 'a+', encoding='utf-8') as f:
            for proxy in all_proxies:
                f.write(json.dumps(proxy, ensure_ascii=False) + '\n')

    def run(self):
        """
        General spider running logic:
            init -> face page url request -> (resource page collect) -> crawl all proxies -> check proxies' useability -> save
        :return:
        """
        # 1 爬取所有 proxies
        self.all_proxies = self.get_all_proxies()
        print('[{}] 爬取代理数:{}'.format(self.__class__.__name__, len(self.all_proxies)))
        # 2 过滤可用代理
        self.all_proxies_filter = self.filter_all_proxies_mp()
        print('[{}] 可用代理数:{}'.format(self.__class__.__name__, len(self.all_proxies_filter)))
        # 3 存储可用代理
        # 默认存储路径配置在 config 中,如果想要另存,在子爬虫中重构 run() 方法即可
        self.save_to_txt(os.path.join(useful_ip_file_path, useful_ip_file_name), self.all_proxies_filter)

        print('[{}] run successed.'.format(self.__class__.__name__))

        return list(self.all_proxies_filter)


if __name__ == '__main__':
    help(Spider)

上面这段代码即对应着项目中所有实例爬虫的通用运行逻辑,步骤如下:

  1. 初始化相关参数
  2. run() 实例化爬虫运行入口
  3. pre_parse() 请求代理资源页
  4. get_all_proxies() 爬取逻辑的入口函数,开始对当前实例爬虫进行代理资源爬取
  5. parse() 对代理详情页进行解析
  6. filter_all_proxies_mp() 验证爬取到的代理的可用性
  7. save_to_txt() 将可用代理存到文件中

为了便于理解代理资源页、代理详情页,下面以站大爷的网页结构为例进行说明:

代理资源页:可包含多个具体的代理资源集合

代理详情页:即前面每一条资源的详情页面

实际使用中,假设我们已有了一个实例爬虫 SpiderX,只需通过以下方式来启动:

spider_obj = SpiderX()
spider_obj.run()

插播一下:
上面我这里直接上了进程池的版本,本来一开始写得是串行验证,但是速度太慢了,所以果断换多进程并发。如果对 python 多进程不太熟悉,可以先停一会儿去这里看下相关知识,啪的一下很快的:https://www.cnblogs.com/zishu/p/17300868.html

1.1 Config

存储路径等参数放在 config.py 中:

# _*_ coding : utf-8 _*_

import os

RETRY_LIMIT = 4     # 爬取失败时的重试次数

# 存储文件地址
useful_ip_file_path = os.path.join('D:/FreeIPProxyGettingPro', 'proxies_spider_results')
# 存储文件名称
useful_ip_file_name = 'useful_proxies_spiding.txt'

1.2 验证代理可用性

当爬取到代理时,需要验证其可用性,对可用代理才将其保存。一般来说,较为简单的验证逻辑就是使用该代理对百度首页进行访问,然后根据返回结果验证代理可用性。但是在实际使用过程中,会出现各种问题,比如代理访问并没有隐藏掉源 ip、百度返回验证页面、无法访问但返回一个正常的说明网页(非百度首页)等。

所以这里采用的是使用代理对 IP 查询网站(https://ip.900cha.com/)进行访问,解析网页结果,判断网页显示的 ip 是否与所使用的代理 ip 一致:

代码逻辑 tools.py:

# _*_ coding : utf-8 _*_

import requests
from lxml import etree
import time


def check_proxy_900cha(proxy, timeout=3, realtimeout=False):
    """
    验证代理可用性
    :param proxy:
    :param timeout:
    :param realtimeout: 
    :return:
    """
	time.sleep(1)	# 防止频繁访问给服务器带来过大压力
    url = 'https://ip.900cha.com/'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
    }
    proxies = {
        'http': '{}:{}'.format(proxy['ip'], proxy['port']),
        'https': '{}:{}'.format(proxy['ip'], proxy['port'])
    }
    try:
        response = requests.get(url=url, headers=headers, proxies=proxies, timeout=timeout)
    except Exception as e:
        return False, None
    else:
        tree = etree.HTML(response.text)
        ret_ip = tree.xpath('//div[@class="col-md-8"]/h3/text()')[0].strip()
        if ret_ip == proxies['http'].split(':')[0]:
            if realtimeout:
                print(f'代理 {proxy["ip"]}:{proxy["port"]} 有效!')
            return True, proxy
        else:
            return False, None

2 实例爬虫

搜集网上的一些免费代理资源,限于篇幅,这里以 3 个结构典型案例来展示。

2.1 seo 代理

https://proxy.seofangfa.com/

可以说是最简单的一个代理页面了,入口页直接就放了 proxy 列表:

实例代码:

# _*_ coding : utf-8 _*_


from tools import *
from ProxiesSpider.spider import Spider			# 前面的 Spider 类
# from wrappers import req_respose_none_wrapper


class SpiderSeo(Spider):

    def __init__(self, *args, **kwargs):

        url = 'https://proxy.seofangfa.com/'
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
        }

        super().__init__(url=url, headers=headers)

    def pre_parse(self):
        self.parse_urls = [
            'https://proxy.seofangfa.com/'
        ]

    # @req_respose_none_wrapper	# 可暂时不管
    def parse(self):
        """
        解析代理
        """
        content = self.response.text
        tree = etree.HTML(content)
        proxies_obj = tree.xpath('//table[@class="table"]/tbody/tr')
        proxies = []
        for proxy_obj in proxies_obj:
            dic_ = {
                'ip': proxy_obj.xpath('./td[1]/text()')[0].strip(),
                'port': proxy_obj.xpath('./td[2]/text()')[0].strip(),
                'position': proxy_obj.xpath('./td[4]/text()')[0].strip(),
                'day': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[0],
            }
            proxies.append(dic_)
        return proxies

    def get_all_proxies(self):
        """
        获取所有 proxies
        """
        self.pre_parse()

        for parse_url in self.parse_urls:
            self.update_attrs(url=parse_url)
            self.update_response()

            proxies = self.parse()
            self.all_proxies += proxies

        return self.all_proxies


if __name__ == '__main__':

    spider_seo = SpiderSeo()
    spider_seo.run()

pre_parse() 用来获取代理资源页,但是 seo 没有,为了结构一致性,在其中直接填充详情页。

2.2 快代理

https://www.kuaidaili.com/free/inha/1/

快代理的网页结构介于 seo 和站大爷之间,其也没有代理资源页,但是其有两份代理资源(普通 & 高匿),所以同 seo 一样,直接在 pre_parse() 函数中填充即可。

# _*_ coding : utf-8 _*_

from ProxiesSpider.spider import Spider
from tools import *
import time
import sys
# from wrappers import req_respose_none_wrapper


class SpiderKuai(Spider):

    def __init__(self, *args, **kwargs):

        kwargs['url'] = 'https://www.kuaidaili.com/free/inha/1/'
        kwargs['headers'] = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
        }

        super().__init__(**kwargs)

    def pre_parse(self):
        self.parse_urls = [
            'https://www.kuaidaili.com/free/intr/',     # 国内普通代理
            'https://www.kuaidaili.com/free/inha/'      # 国内高匿代理
        ]
        return self.parse_urls

    # @req_respose_none_wrapper
    def parse(self):
        """
        解析代理
        """
        content = self.response.text
        tree = etree.HTML(content)
        proxies_obj = tree.xpath('//div[@id="list"]//tbody/tr')
        proxies = []
        for proxy_obj in proxies_obj:
            dic_ = {
                'ip': proxy_obj.xpath('./td[@data-title="IP"]/text()')[0].strip(),
                'port': proxy_obj.xpath('./td[@data-title="PORT"]/text()')[0].strip(),
                'type': proxy_obj.xpath('./td[@data-title="类型"]/text()')[0].strip(),
                'position': proxy_obj.xpath('./td[@data-title="位置"]/text()')[0].strip(),
                'day': proxy_obj.xpath('./td[@data-title="最后验证时间"]/text()')[0].strip().split(' ')[0]
            }
            if dic_['day'] != self.day:
                break
            proxies.append(dic_)
        return proxies

    def get_all_proxies(self):
        """
        获取所有 proxies
        """
        # 1 先获取所有待采集的 proxy list 页
        self.pre_parse()

        # 2 对每个 proxy 信息页的资源进行解析
        for parse_url in self.parse_urls:
            time.sleep(3)
            self.update_attrs(url=parse_url)
            self.update_response()

            # 3 获取资源页所有的 proxy
            count = 1
            pbar = tqdm(file=sys.stdout, desc='[{}] crawling all pages...'.format(self.__class__.__name__))
            while True:
                proxies = self.parse()
                if len(proxies) == 0:
                    break

                self.all_proxies += proxies

                next_page = '{}{}/'.format(parse_url, count+1)
                count += 1
                time.sleep(3)
                self.update_attrs(url=next_page)
                self.update_response()

                pbar.update(1)
            pbar.close()

        return self.all_proxies


if __name__ == '__main__':
    spider_kuai = SpiderKuai()
    spider_kuai.run()

基本结构和 seo 的逻辑一致,只是这里在代理详情页解析资源时要复杂些,因为是多页结构。
观察各页资源可以发现,快代理是将所有累计的免费代理都放在一起,并没有按天分区,所以这里要面对的问题有两个:

  • 翻页爬取
  • 翻页过程中对资源更新日期进行检测,一但资源声明周期超过当天,就停止继续爬取

所以我们这里直接采用一个 True 循环,在 parse() 中一旦遇到生成周期超过当天的资源后,就及时返回。这样在继续翻页并且下一页没有当天资源时,就会返回空列表,此时结束循环。

在测试过程中,我发现快代理的还是有着简单的反爬限制的:

  • 当连续访问多页内容时,会返回 -10,获取不到具体数据;
  • 一天内多次访问时,会封 IP;

其中针对第一种,只需要在多个连续请求之间 sleep() 一下即可。而对第二种限制,由于我们的爬虫正式逻辑是一天访问一次,所以正式运行时逻辑上不会存在封禁 ip 的情况,所以可以不做处理(当然后面也可以利用已获取的代理对快代理网站进行访问)。

\(插播一下\)

这里还有个知识点,就是 python 进度条组件 tqdm 的使用,由于 tqdm 默认的输出模式和 print 是不一样的,会导致 tqdm 输出和 print 输出排版交混的问题,所以这里需要在 tqdm 中指定 file=sys.stdout,这样就不会出现上述问题。

2.3 站大爷

相对来说结构最为完善的网页,包含代理资源页、代理详情页。所以 pre_parse() 函数中需要先对代理资源页进行解析,获取当天的代理详情页链接,再进二级页面进行爬取。

# _*_ coding : utf-8 _*_

from lxml import etree
from ProxiesSpider.spider import Spider
import time
import sys
# from wrappers import req_respose_none_wrapper


class SpiderZdaye(Spider):
    """
    zdaye 自有证书,需要设置 verify=False
    """
    def __init__(self, *args, **kwargs):

        url = 'https://www.zdaye.com/dayProxy.html'
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
        }

        # zdaye 封禁代理比较频繁,需要使用代理去访问资源
        super().__init__(url=url, headers=headers, verify=False)

    def pre_parse(self):
        """
        代理资源页解析
        :return:
        """
        self.update_response()

		# 该网页需对 request 结果指定 utf-8 编码
        self.response.encoding = 'utf-8'
        content = self.response.text
        tree = etree.HTML(content)
        proxy_page_info_obj = tree.xpath('//div[@class="thread_content"]/h3/a')
        for ppio in proxy_page_info_obj:
            title = ppio.xpath('./text()')[0].strip().split(' ')[0]
            parse_day = title.split('日')[0].replace('年', '-').replace('月', '-')
            if [int(x) for x in parse_day.split('-')] == [int(x) for x in self.day.split('-')]:
                self.parse_urls.append(ppio.xpath('./@href')[0])
            else:
                break
        return self.parse_urls

    # @req_respose_none_wrapper
    def parse(self):
        """
        解析代理
        """
        self.response.encoding = 'utf-8'
        content = self.response.text
        tree = etree.HTML(content)
        proxies_obj = tree.xpath('//table[@id="ipc"]/tbody/tr')
        proxies = []
        for proxy_obj in proxies_obj:
            dic_ = {
                'ip': proxy_obj.xpath('./td[1]/text()')[0].strip().replace('"', '').strip(),
                'port': proxy_obj.xpath('./td[2]/text()')[0].strip().replace('"', '').strip(),
                'type': proxy_obj.xpath('./td[3]/text()')[0].strip(),
                'position': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[0],
                'isp': proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')[1] if len(proxy_obj.xpath('./td[5]/text()')[0].strip().split(' ')) > 1 else None,
                'day': self.day
            }
            proxies.append(dic_)
        return proxies

    def get_all_proxies(self):
        """
        获取所有 proxies
        """
        # 1 先获取所有代理详情页的 url
        self.pre_parse()

        # 2 对每个 proxy 信息页的资源进行解析
        for parse_url in self.parse_urls:
            parse_url = 'https://www.zdaye.com' + parse_url

            self.update_attrs(url=parse_url)
            self.update_response()

            # 3 获取详情页所有的 proxy
            while True:
                time.sleep(3)   # 间隔爬取
                proxies = self.parse()
                if len(proxies) == 0:
                    break

                self.all_proxies += proxies

                next_tag = etree.HTML(self.response.text).xpath('//a[@title="下一页"]/@href')
                if len(next_tag) == 0:
                    break
                else:
                    next_page = 'https://www.zdaye.com' + etree.HTML(self.response.text).xpath('//a[@title="下一页"]/@href')[0]
                    self.update_attrs(url=next_page)
                    self.update_response()

        return self.all_proxies


if __name__ == '__main__':
    spider_zdy = SpiderZdaye()
    spider_zdy.run()

站大爷的网站需要注意的主要有两点:

  • 设置 verify=False 来关闭 SSL 验证;
  • 该网站的反爬措施还蛮严厉的,上述代码中尽管对请求进行了简单的 sleep,但是只要多调试几次,还是会被封;

关于反爬,后面会讲解如何使用已爬取的代理来请求,并且站大爷这个网站还挺难搞的,后面会说到的。

3 爬虫链 & 使用代理绕过反爬

其实写到这里,如果要求不高的话,上面的功能已经可以做基本使用了,只需要挨个运行或者直接写一个主函数依次实例化并运行即可。但是上面已经说到了有些网站会有反爬限制,如果不使用代理,这个实例爬虫基本上就废掉了,所以下面我将多个实例爬虫排排队,串行执行,并在此过程中更新已有可用代理,并编写代理请求接口函数随机的获取一个可用代理,用来应对被限制的情况。

3.1 加载已有代理资源

看过 Spider 类代码的应该有印象,该项目将爬取并验证后的代理以字典的形式存到 txt 文件中,形式如下:

{"ip": "222.190.208.49", "port": "8089", "position": "江苏省泰州市", "isp": "电信", "day": "2023-04-13"}
{"ip": "36.137.106.110", "port": "7890", "position": "北京市", "isp": "移动", "day": "2023-04-13"}
{"ip": "182.241.132.30", "port": "80", "position": "云南省红河州", "isp": "电信", "day": "2023-04-13"}

并且多个实例爬虫是采用追加的形式向同一份文件追加写入的,那么我们自然可以简单的通过加载文件的方式来获取已有资源:

# 项目文件结构
-- proxies_spider_results:
		-- useful_proxies_2023-04-11.txt
		-- useful_proxies_2023-04-12.txt
-- ProxiesSpider:
		-- seo_spider.py
		-- kuai_spider.py
		-- zdaye_spider.py
-- main.py
-- tools.py
import sys
import json
import random
import os
from config import useful_ip_file_path

here = os.path.dirname(__file__)

def get_latest_proxy_file(file_path):
    """
    获取当前路径下的最新文件内容
    :param file_path:
    :return:
    """
    file_latest = sorted(os.listdir(file_path))[-1]
    with open(os.path.join(file_path, file_latest), 'r', encoding='utf=8') as f:
        all_free_proxies = [json.loads(s.strip()) for s in f.readlines()]

    return all_free_proxies

def get_free_proxy():
    all_free_proxies = get_latest_proxy_file(useful_ip_file_path)
    for i in tqdm(range(len(all_free_proxies)), file=sys.stdout, desc='choosing a useful proxy...'):
        index = random.randint(0, len(all_free_proxies)-1)
        proxy = all_free_proxies[index]
        useful, proxy = check_proxy_900cha(proxy)
        if useful:
            return proxy
    print('无可用 proxy ~')
    return None
	
if __name__ == '__main__':
    print(get_free_proxy())

这样,通过 get_free_proxy()函数,就可以很容易的从已有代理中随机挑选一个可用的代理。

3.2 使用代理

有了上面的代理获取函数,在遇到反爬时,我们只需要调用一下,如果能返回一个可用的代理,那么就可以拿着这个代理去重新请求网页资源。

这里我以 kuai spider 为例,我们可以设置超时时间为 0.01 s,来模拟访问失败的情况,然后 catch 这个 Exception 并重新使用代理访问正确的网页:

# _*_ coding : utf-8 _*_
# @Time : 2023/3/13 14:19
# @Author : jiang
# @File : kuai_proxy_spider
# Project : FreeIPProxyGettingPro

# 将上级目录加载进来
import sys
import os
sys.path.append(os.path.dirname(__name__))
from ProxiesSpider.spider import Spider
from lxml import etree
import time
import sys
from wrappers import req_respose_none_wrapper
from tqdm import tqdm
from tools import get_free_proxy


class SpiderKuai(Spider):

    def __init__(self, *args, **kwargs):

        kwargs['url'] = 'https://www.kuaidaili.com/free/inha/1/'
        kwargs['headers'] = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
        }

        super().__init__(**kwargs)

    def pre_parse(self):
        self.parse_urls = [
            'https://www.kuaidaili.com/free/intr/',     # 国内普通代理
            'https://www.kuaidaili.com/free/inha/'      # 国内高匿代理
        ]
        return self.parse_urls

    @req_respose_none_wrapper
    def parse(self):
        """
        解析代理
        """
        content = self.response.text
        tree = etree.HTML(content)
        proxies_obj = tree.xpath('//div[@id="list"]//tbody/tr')
        proxies = []
        for proxy_obj in proxies_obj:
            dic_ = {
                'ip': proxy_obj.xpath('./td[@data-title="IP"]/text()')[0].strip(),
                'port': proxy_obj.xpath('./td[@data-title="PORT"]/text()')[0].strip(),
                'type': proxy_obj.xpath('./td[@data-title="类型"]/text()')[0].strip(),
                'position': proxy_obj.xpath('./td[@data-title="位置"]/text()')[0].strip(),
                'day': proxy_obj.xpath('./td[@data-title="最后验证时间"]/text()')[0].strip().split(' ')[0]
            }
            if dic_['day'] != self.day:
                break
            proxies.append(dic_)
        return proxies

    def get_all_proxies(self):
        """
        获取所有 proxies
        """
        # 1 先获取所有待采集的 proxy list 页
        self.pre_parse()

        # 2 对每个 proxy 信息页的资源进行解析
        for parse_url in self.parse_urls:
            time.sleep(3)
            self.update_attrs(url=parse_url)
            self.update_response()

            # 3 获取资源页所有的 proxy
            count = 1
            pbar = tqdm(file=sys.stdout, desc='[{}] crawling all pages...'.format(self.__class__.__name__))
            while True:
                proxies = self.parse()
                if len(proxies) == 0:
                    break

                self.all_proxies += proxies

                next_page = '{}{}/'.format(parse_url, count+1)
                count += 1
                time.sleep(3)
                self.update_attrs(url=next_page)
                self.update_response()

                pbar.update(1)
            pbar.close()

        return self.all_proxies


if __name__ == '__main__':
    spider_kuai = SpiderKuai()
    spider_kuai.timeout = 0.01
    try:
        spider_kuai.run()
    except Exception as e:
        print(e)

        # 使用代理
        proxy = get_free_proxy()
        print('使用代理:', proxy)
        
        if proxy is None:
            proxies = None
        else:
            proxies = {
                'http': '{}:{}'.format(proxy['ip'], proxy['port']),
                'https': '{}:{}'.format(proxy['ip'], proxy['port']),
            }
        spider_kuai.timeout = 3
        spider_kuai.run()

执行结果:

~\python_spider\FreeIPProxyGettingPro_TMP> kuai_proxy_spider.py

HTTPSConnectionPool(host='www.kuaidaili.com', port=443): Max retries exceeded with url: /free/intr/ (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x00000183962DFB50>, 'Connection to www.kuaidaili.com timed out. (connect timeout=0.01)'))
choosing a useful proxy...:   6%|█████▊                                                                                             | 1/17 [00:06<01:38,  6.13s/it]
使用代理: {'ip': '182.241.132.30', 'port': '80', 'position': '云南省红河州', 'isp': '电信', 'day': '2023-04-13'}
[SpiderKuai] crawling all pages...: 2it [00:07,  3.72s/it]
[SpiderKuai] crawling all pages...: 2it [00:07,  3.69s/it]
[SpiderKuai] 爬取代理数:42
[SpiderKuai] checking proxies...: 100%|████████████████████████████████████████████████████████████████████████████████████████████| 42/42 [00:07<00:00,  5.48it/s]
[SpiderKuai] 可用代理数:0
[SpiderKuai] run successed.

可以看到,基本逻辑没问题。

4 鲁棒性

上一节介绍了如何使用爬取到的代理访问资源页,但是处理方式还是不够优雅。

我们可以想象一下,真实的爬虫运行出错场景是怎样的,首先肯定一开始是正常请求,结果报错了,此时我们应该暂停一下再次访问(确保不是网络本身的问题),如果还是不行就通过 get_free_proxy()获取一个代理来进行请求,当然也不是无限次请求,我们暂且设置这种尝试次数不超过三次,之后如果还是报错,则停止这个爬虫。

4.1 request 请求出错处理逻辑

好了,有了这个基础的逻辑,我们就可以愉快的写代码了。首先,为了解耦,我们肯定是不会在每个实例爬虫中像上一节中那样进行 try catch 的,别忘了 Spider 这个类,对 url 进行 requset 请求操作被封装在了 update_response() 这个函数中,所以我们可以直接对该函数进行 try catch。

未完待续:在后面,会有,不会鸽~