Metadata-Version: 2.2
Name: scrapy_proxy_ip_pool
Version: 1.0.0
Summary: 这是一个Scrapy中间件，用于管理代理IP池(支持使用Redis作为代理池)。
Home-page: https://github.com/Tlyer233/Scrapy-Proxy-IP-Pool
Author: 明廷盛
Author-email: 1594365335@qq.com
Classifier: Programming Language :: Python :: 3
Classifier: Framework :: Scrapy
Description-Content-Type: text/markdown
Requires-Dist: Twisted>=20.3.0
Requires-Dist: zope.interface>=5
Requires-Dist: scrapy>=2.12.0
Requires-Dist: redis>=5.2.1
Requires-Dist: loguru>=0.7.2
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: requires-dist
Dynamic: summary

# Scrapy Proxy IP Pool

> 这是一个Scrapy中间件，用于管理代理IP池。

* 🥳支持Redis:支持使用Redis的IP代理池(也可本地List作为代理池)
* 🥵最大限度榨干每个IP: 只有请求为指定异常or状态码(被封)时才会更换IP
* 🤌简单配置: ez三步即可使用
> 如果你不知道如何写"代理IP池"可以花几分钟看下(<10min)然后可以自己写,当然也可以用我写的现成代理池

## 安装

使用以下命令安装：

```bash
pip install scrapy_proxy_ip_pool
```

## 基本使用

### STEP1:在`setting.py`中添加该中间件

* 要求❗: 这里的权值一定要大于`RetryMiddleware`(重试中间件默认550)

```python
PROXY_POOL_SIZE = 1  # 代理池的大小(有封ip的网站建议开大一点)
DOWNLOADER_MIDDLEWARES = {
    'scrapy_proxy_ip_pool.proxy_pool_downloader_middleware.ProxyPoolDownloaderMiddleware': 551,
}
```

### STEP2:在你的spider中必须要编写`def get_proxy_ip(self):->str`方法

* 要求❗:名字只能是这个,返回值是一个代理IP.
* 说明: 代理池中间件会读取这个方法,从而获取代理IP地址.

```python
import requests
import scrapy


class IpSpider(scrapy.Spider):
    name = "ip"
    REDIS_KEY = name

    def parse(self, response, **kwargs):
        pass

    # 示例
    def get_proxy_ip(self):
        api_url = "你请求付费代理的地址"
        proxy_ip = requests.get(api_url).text
        username = "你的代理用户名"
        password = "你的代理密码"
        return f"http://{username}:{password}@{proxy_ip}/"  # 一次只需要返回一个ip
```
## 其他配置
### 一. 使用Redis作为 代理池
* `settings.py`中配置如下内容:
```python
PROXY_POOL_ENABLED = 'True'  # 使用Redis进行代理池的构建(默认为False)
REDIS_URL = "redis://127.0.0.1:6379/0"
```
* `spider`中添加`REDIS_KEY`变量指明存储在Redis中使用的键
```python
import scrapy

class IpSpider(scrapy.Spider):
    name = "ip"
    REDIS_KEY = name # 最终为 REDIS_KEY+":proxy_pool"
    ...
```
### 二. 指定异常和状态码
* `settings.py`中配置如下内容
* 说明: 如果遇到了这些异常, 就会更新IP; 如下默认配置的是常见可能被封异常和状态码
* 注意❗:这里依据网址而定, 如果不确定, 两个可以设置为空列表,后续根据报错改
```python
# 如下列举了常见的被封禁ip(或ip不可用)时的异常或状态码, 用户可以根据目标网站的情况在settings.py中自行配置
NEED_UPDATE_PROXY_EXCEPTIONS = [
    'twisted.internet.defer.TimeoutError',  # 请求超时未响应，可能目标服务器检测到代理IP异常导致故意延迟不响应
    'twisted.internet.error.TimeoutError',  # 底层网络连接超时，可能代理服务器IP被目标网站封锁导致无法建立连接
    'twisted.internet.error.ConnectError',  # 与代理服务器建立连接失败，可能代理IP已被防火墙封禁或服务器已下线
    'scrapy.core.downloader.handlers.http11.TunnelError',  # 代理服务器要求身份验证或目标网站封禁该代理IP，导致无法建立HTTPS隧道连接
]

NEED_UPDATE_PROXY_CODES = [
    503,  # 服务不可用，服务器可能正在主动拒绝来自该代理IP的请求（反爬机制触发）
    407,  # 代理身份验证失败，或代理服务提供商已封禁当前IP的访问权限
    403,  # 服务器明确拒绝访问，通常表示当前代理IP已被加入黑名单
    429,  # 请求频率超限，目标网站针对该代理IP实施了速率限制
]
```
## 关键代码
#### 一. 更新IP
* ⏱什么时候会被调用: 只有出现 `NEED_UPDATE_PROXY_EXCEPTIONS` 或 `NEED_UPDATE_PROXY_CODES` 中才会运行这个方法
```python
# 伪代码
def update_proxy(self, request: Request) -> Request:
    last_ip=本次(有问题)请求的代理ip
    if last_ip in 代理池:
        在代理池中更换该(有问题)的ip
    request.meta['proxy'] = 新ip
    return request
```
```python
def update_proxy(self, request: Request) -> Request:
    """
    使用"乐观锁"思想,更新过时IP,并返回携带新IP的request
    :param request: 未成功的网络请求
    :return: 返回携带新IP的request
    """
    # 解决request.meta['proxy']鉴权丢失问题
    last_proxy = request.meta['proxy'].split("//")[1]
    temp_proxy_list = self.client.smembers(self.redis_key) if self.proxy_pool_enabled else self.proxy_pool
    need_update = any(last_proxy in ip for ip in temp_proxy_list)  # 存在=>需要替换
    # 当前需要获取新的ip
    if need_update:
        # 替换"代理池"中的IP
        if self.proxy_pool_enabled:  # Redis
            self.client.srem(self.redis_key, last_proxy)
            self.client.sadd(self.redis_key, self.get_proxy_with_logging())
        else:  # 本地
            self.proxy_pool.remove(last_proxy)
            self.proxy_pool.append(self.get_proxy_with_logging())
    # 更换代理IP进行请求
    request.meta['proxy'] = self.get_proxy_from_pool()
    request.dont_filter = True  # 防止被过滤!!!
    return request
```
