mirror of
https://github.com/NanmiCoder/MediaCrawler.git
synced 2025-11-25 03:15:17 +08:00
fix: httpx proxy format error
feat: add a ip proxy provider
This commit is contained in:
@@ -23,7 +23,7 @@ ENABLE_IP_PROXY = False
|
||||
IP_PROXY_POOL_COUNT = 2
|
||||
|
||||
# 代理IP提供商名称
|
||||
IP_PROXY_PROVIDER_NAME = "kuaidaili"
|
||||
IP_PROXY_PROVIDER_NAME = "kuaidaili" # kuaidaili | wandouhttp
|
||||
|
||||
# 设置为True不会打开浏览器(无头浏览器)
|
||||
# 设置False会打开一个浏览器
|
||||
|
||||
BIN
docs/static/images/wd_http_img.png
vendored
Normal file
BIN
docs/static/images/wd_http_img.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 369 KiB |
BIN
docs/static/images/wd_http_img_1.png
vendored
Normal file
BIN
docs/static/images/wd_http_img_1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 345 KiB |
BIN
docs/static/images/wd_http_img_2.png
vendored
Normal file
BIN
docs/static/images/wd_http_img_2.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
BIN
docs/static/images/wd_http_img_4.png
vendored
Normal file
BIN
docs/static/images/wd_http_img_4.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
44
docs/代理使用.md
44
docs/代理使用.md
@@ -5,45 +5,11 @@
|
||||
|
||||

|
||||
|
||||
## 准备代理 IP 信息
|
||||
点击 <a href="https://www.kuaidaili.com/?ref=ldwkjqipvz6c">快代理</a> 官网注册并实名认证(国内使用代理 IP 必须要实名,懂的都懂)
|
||||
|
||||
## 获取 IP 代理的密钥信息
|
||||
从 <a href="https://www.kuaidaili.com/?ref=ldwkjqipvz6c">快代理</a> 官网获取免费试用,如下图所示
|
||||

|
||||
## 选择一个代理IP提供商
|
||||
|
||||
注意:选择私密代理
|
||||

|
||||
|
||||
选择开通试用
|
||||

|
||||
|
||||
初始化一个快代理的示例,如下代码所示,需要4个参数
|
||||
|
||||
```python
|
||||
|
||||
def new_kuai_daili_proxy() -> KuaiDaiLiProxy:
|
||||
"""
|
||||
构造快代理HTTP实例
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return KuaiDaiLiProxy(
|
||||
kdl_secret_id=os.getenv("kdl_secret_id", "你的快代理secert_id"),
|
||||
kdl_signature=os.getenv("kdl_signature", "你的快代理签名"),
|
||||
kdl_user_name=os.getenv("kdl_user_name", "你的快代理用户名"),
|
||||
kdl_user_pwd=os.getenv("kdl_user_pwd", "你的快代理密码"),
|
||||
)
|
||||
|
||||
```
|
||||
在试用的订单中可以看到这四个参数,如下图所示
|
||||
|
||||
`kdl_user_name`、`kdl_user_pwd`
|
||||

|
||||
|
||||
`kdl_secret_id`、`kdl_signature`
|
||||

|
||||
|
||||
## 将配置文件中的`ENABLE_IP_PROXY`置为 `True`
|
||||
> `IP_PROXY_POOL_COUNT` 池子中 IP 的数量
|
||||
### 快代理
|
||||
[快代理使用文档](快代理使用文档.md)
|
||||
|
||||
### 豌豆HTTP文档查看
|
||||
[豌豆HTTP使用文档](豌豆HTTP使用文档.md)
|
||||
41
docs/快代理使用文档.md
Normal file
41
docs/快代理使用文档.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## 快代理使用文档(支持个人和企业用户)
|
||||
|
||||
## 准备代理 IP 信息
|
||||
点击 <a href="https://www.kuaidaili.com/?ref=ldwkjqipvz6c">快代理</a> 官网注册并实名认证(国内使用代理 IP 必须要实名,懂的都懂)
|
||||
|
||||
## 获取 IP 代理的密钥信息
|
||||
从 <a href="https://www.kuaidaili.com/?ref=ldwkjqipvz6c">快代理</a> 官网获取免费试用,如下图所示
|
||||

|
||||
|
||||
注意:选择私密代理
|
||||

|
||||
|
||||
选择开通试用
|
||||

|
||||
|
||||
初始化一个快代理的示例,如下代码所示,需要4个参数
|
||||
|
||||
```python
|
||||
# 文件地址: proxy/providers/kuai_daili_proxy.py
|
||||
# -*- coding: utf-8 -*-
|
||||
def new_kuai_daili_proxy() -> KuaiDaiLiProxy:
|
||||
"""
|
||||
构造快代理HTTP实例
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return KuaiDaiLiProxy(
|
||||
kdl_secret_id=os.getenv("kdl_secret_id", "你的快代理secert_id"),
|
||||
kdl_signature=os.getenv("kdl_signature", "你的快代理签名"),
|
||||
kdl_user_name=os.getenv("kdl_user_name", "你的快代理用户名"),
|
||||
kdl_user_pwd=os.getenv("kdl_user_pwd", "你的快代理密码"),
|
||||
)
|
||||
|
||||
```
|
||||
在试用的订单中可以看到这四个参数,如下图所示
|
||||
|
||||
`kdl_user_name`、`kdl_user_pwd`
|
||||

|
||||
|
||||
`kdl_secret_id`、`kdl_signature`
|
||||

|
||||
38
docs/豌豆HTTP使用文档.md
Normal file
38
docs/豌豆HTTP使用文档.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## 豌豆HTTP代理使用文档 (只支持企业用户)
|
||||
|
||||
## 准备代理 IP 信息
|
||||
点击 <a href="https://h.wandouip.com?invite_code=rtnifi">豌豆HTTP代理</a> 官网注册并实名认证(国内使用代理 IP 必须要实名,懂的都懂)
|
||||
|
||||
## 获取 IP 代理的密钥信息 appkey
|
||||
从 <a href="https://h.wandouip.com?invite_code=rtnifi">豌豆HTTP代理</a> 官网获取免费试用,如下图所示
|
||||

|
||||
|
||||
选择自己需要的套餐
|
||||

|
||||
|
||||
|
||||
初始化一个豌豆HTTP代理的示例,如下代码所示,需要1个参数: app_key
|
||||
|
||||
```python
|
||||
# 文件地址: proxy/providers/wandou_http_proxy.py
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
def new_wandou_http_proxy() -> WanDouHttpProxy:
|
||||
"""
|
||||
构造豌豆HTTP实例
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return WanDouHttpProxy(
|
||||
app_key=os.getenv(
|
||||
"wandou_app_key", "你的豌豆HTTP app_key"
|
||||
), # 通过环境变量的方式获取豌豆HTTP app_key
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
在个人中心的`开放接口`找到 `app_key`,如下图所示
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
# @Time : 2024/4/5 10:13
|
||||
# @Desc :
|
||||
from .jishu_http_proxy import new_jisu_http_proxy
|
||||
from .kuaidl_proxy import new_kuai_daili_proxy
|
||||
from .kuaidl_proxy import new_kuai_daili_proxy
|
||||
from .wandou_http_proxy import new_wandou_http_proxy
|
||||
110
proxy/providers/wandou_http_proxy.py
Normal file
110
proxy/providers/wandou_http_proxy.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则:
|
||||
# 1. 不得用于任何商业用途。
|
||||
# 2. 使用时应遵守目标平台的使用条款和robots.txt规则。
|
||||
# 3. 不得进行大规模爬取或对平台造成运营干扰。
|
||||
# 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。
|
||||
# 5. 不得用于任何非法或不当的用途。
|
||||
#
|
||||
# 详细许可条款请参阅项目根目录下的LICENSE文件。
|
||||
# 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author : relakkes@gmail.com
|
||||
# @Time : 2025/7/31
|
||||
# @Desc : 豌豆HTTP 代理IP实现
|
||||
import os
|
||||
from typing import Dict, List
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from proxy import IpCache, IpGetError, ProxyProvider
|
||||
from proxy.types import IpInfoModel
|
||||
from tools import utils
|
||||
|
||||
|
||||
class WanDouHttpProxy(ProxyProvider):
|
||||
|
||||
def __init__(self, app_key: str, num: int = 100):
|
||||
"""
|
||||
豌豆HTTP 代理IP实现
|
||||
:param app_key: 开放的app_key,可以通过用户中心获取
|
||||
:param num: 单次提取IP数量,最大100
|
||||
"""
|
||||
self.proxy_brand_name = "WANDOUHTTP"
|
||||
self.api_path = "https://api.wandouapp.com/"
|
||||
self.params = {
|
||||
"app_key": app_key,
|
||||
"num": num,
|
||||
}
|
||||
self.ip_cache = IpCache()
|
||||
|
||||
async def get_proxy(self, num: int) -> List[IpInfoModel]:
|
||||
"""
|
||||
:param num:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# 优先从缓存中拿 IP
|
||||
ip_cache_list = self.ip_cache.load_all_ip(
|
||||
proxy_brand_name=self.proxy_brand_name
|
||||
)
|
||||
if len(ip_cache_list) >= num:
|
||||
return ip_cache_list[:num]
|
||||
|
||||
# 如果缓存中的数量不够,从IP代理商获取补上,再存入缓存中
|
||||
need_get_count = num - len(ip_cache_list)
|
||||
self.params.update({"num": min(need_get_count, 100)}) # 最大100
|
||||
ip_infos = []
|
||||
async with httpx.AsyncClient() as client:
|
||||
url = self.api_path + "?" + urlencode(self.params)
|
||||
utils.logger.info(f"[WanDouHttpProxy.get_proxy] get ip proxy url:{url}")
|
||||
response = await client.get(
|
||||
url,
|
||||
headers={
|
||||
"User-Agent": "MediaCrawler https://github.com/NanmiCoder/MediaCrawler",
|
||||
},
|
||||
)
|
||||
res_dict: Dict = response.json()
|
||||
if res_dict.get("code") == 200:
|
||||
data: List[Dict] = res_dict.get("data", [])
|
||||
current_ts = utils.get_unix_timestamp()
|
||||
for ip_item in data:
|
||||
ip_info_model = IpInfoModel(
|
||||
ip=ip_item.get("ip"),
|
||||
port=ip_item.get("port"),
|
||||
user="", # 豌豆HTTP不需要用户名密码认证
|
||||
password="",
|
||||
expired_time_ts=utils.get_unix_time_from_time_str(
|
||||
ip_item.get("expire_time")
|
||||
),
|
||||
)
|
||||
ip_key = f"WANDOUHTTP_{ip_info_model.ip}_{ip_info_model.port}"
|
||||
ip_value = ip_info_model.model_dump_json()
|
||||
ip_infos.append(ip_info_model)
|
||||
self.ip_cache.set_ip(
|
||||
ip_key, ip_value, ex=ip_info_model.expired_time_ts - current_ts
|
||||
)
|
||||
else:
|
||||
error_msg = res_dict.get("msg", "unknown error")
|
||||
# 处理具体错误码
|
||||
error_code = res_dict.get("code")
|
||||
if error_code == 10001:
|
||||
error_msg = "通用错误,具体错误信息查看msg内容"
|
||||
elif error_code == 10048:
|
||||
error_msg = "没有可用套餐"
|
||||
raise IpGetError(f"{error_msg} (code: {error_code})")
|
||||
return ip_cache_list + ip_infos
|
||||
|
||||
|
||||
def new_wandou_http_proxy() -> WanDouHttpProxy:
|
||||
"""
|
||||
构造豌豆HTTP实例
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return WanDouHttpProxy(
|
||||
app_key=os.getenv(
|
||||
"wandou_app_key", "你的豌豆HTTP app_key"
|
||||
), # 通过环境变量的方式获取豌豆HTTP app_key
|
||||
)
|
||||
@@ -19,7 +19,10 @@ import httpx
|
||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||
|
||||
import config
|
||||
from proxy.providers import new_jisu_http_proxy, new_kuai_daili_proxy
|
||||
from proxy.providers import (
|
||||
new_kuai_daili_proxy,
|
||||
new_wandou_http_proxy,
|
||||
)
|
||||
from tools import utils
|
||||
|
||||
from .base_proxy import ProxyProvider
|
||||
@@ -28,7 +31,9 @@ from .types import IpInfoModel, ProviderNameEnum
|
||||
|
||||
class ProxyIpPool:
|
||||
|
||||
def __init__(self, ip_pool_count: int, enable_validate_ip: bool, ip_provider: ProxyProvider) -> None:
|
||||
def __init__(
|
||||
self, ip_pool_count: int, enable_validate_ip: bool, ip_provider: ProxyProvider
|
||||
) -> None:
|
||||
"""
|
||||
|
||||
Args:
|
||||
@@ -56,19 +61,26 @@ class ProxyIpPool:
|
||||
:param proxy:
|
||||
:return:
|
||||
"""
|
||||
utils.logger.info(f"[ProxyIpPool._is_valid_proxy] testing {proxy.ip} is it valid ")
|
||||
utils.logger.info(
|
||||
f"[ProxyIpPool._is_valid_proxy] testing {proxy.ip} is it valid "
|
||||
)
|
||||
try:
|
||||
httpx_proxy = {
|
||||
f"{proxy.protocol}": f"http://{proxy.user}:{proxy.password}@{proxy.ip}:{proxy.port}",
|
||||
}
|
||||
async with httpx.AsyncClient(proxy=httpx_proxy) as client:
|
||||
# httpx 0.28.1 需要直接传入代理URL字符串,而不是字典
|
||||
if proxy.user and proxy.password:
|
||||
proxy_url = f"http://{proxy.user}:{proxy.password}@{proxy.ip}:{proxy.port}"
|
||||
else:
|
||||
proxy_url = f"http://{proxy.ip}:{proxy.port}"
|
||||
|
||||
async with httpx.AsyncClient(proxy=proxy_url) as client:
|
||||
response = await client.get(self.valid_ip_url)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
utils.logger.info(f"[ProxyIpPool._is_valid_proxy] testing {proxy.ip} err: {e}")
|
||||
utils.logger.info(
|
||||
f"[ProxyIpPool._is_valid_proxy] testing {proxy.ip} err: {e}"
|
||||
)
|
||||
raise e
|
||||
|
||||
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
|
||||
@@ -84,7 +96,9 @@ class ProxyIpPool:
|
||||
self.proxy_list.remove(proxy) # 取出来一个IP就应该移出掉
|
||||
if self.enable_validate_ip:
|
||||
if not await self._is_valid_proxy(proxy):
|
||||
raise Exception("[ProxyIpPool.get_proxy] current ip invalid and again get it")
|
||||
raise Exception(
|
||||
"[ProxyIpPool.get_proxy] current ip invalid and again get it"
|
||||
)
|
||||
return proxy
|
||||
|
||||
async def _reload_proxies(self):
|
||||
@@ -97,8 +111,8 @@ class ProxyIpPool:
|
||||
|
||||
|
||||
IpProxyProvider: Dict[str, ProxyProvider] = {
|
||||
ProviderNameEnum.JISHU_HTTP_PROVIDER.value: new_jisu_http_proxy(),
|
||||
ProviderNameEnum.KUAI_DAILI_PROVIDER.value: new_kuai_daili_proxy(),
|
||||
ProviderNameEnum.WANDOU_HTTP_PROVIDER.value: new_wandou_http_proxy(),
|
||||
}
|
||||
|
||||
|
||||
@@ -118,5 +132,5 @@ async def create_ip_pool(ip_pool_count: int, enable_validate_ip: bool) -> ProxyI
|
||||
return pool
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则:
|
||||
# 1. 不得用于任何商业用途。
|
||||
# 2. 使用时应遵守目标平台的使用条款和robots.txt规则。
|
||||
# 3. 不得进行大规模爬取或对平台造成运营干扰。
|
||||
# 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。
|
||||
# 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则:
|
||||
# 1. 不得用于任何商业用途。
|
||||
# 2. 使用时应遵守目标平台的使用条款和robots.txt规则。
|
||||
# 3. 不得进行大规模爬取或对平台造成运营干扰。
|
||||
# 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。
|
||||
# 5. 不得用于任何非法或不当的用途。
|
||||
#
|
||||
# 详细许可条款请参阅项目根目录下的LICENSE文件。
|
||||
# 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。
|
||||
#
|
||||
# 详细许可条款请参阅项目根目录下的LICENSE文件。
|
||||
# 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。
|
||||
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -20,12 +20,13 @@ from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProviderNameEnum(Enum):
|
||||
JISHU_HTTP_PROVIDER: str = "jishuhttp"
|
||||
KUAI_DAILI_PROVIDER: str = "kuaidaili"
|
||||
WANDOU_HTTP_PROVIDER: str = "wandouhttp"
|
||||
|
||||
|
||||
class IpInfoModel(BaseModel):
|
||||
"""Unified IP model"""
|
||||
|
||||
ip: str = Field(title="ip")
|
||||
port: int = Field(title="端口")
|
||||
user: str = Field(title="IP代理认证的用户名")
|
||||
|
||||
@@ -172,7 +172,7 @@ def match_interact_info_count(count_str: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def format_proxy_info(ip_proxy_info) -> Tuple[Optional[Dict], Optional[Dict]]:
|
||||
def format_proxy_info(ip_proxy_info) -> Tuple[Optional[Dict], Optional[str]]:
|
||||
"""format proxy info for playwright and httpx"""
|
||||
# fix circular import issue
|
||||
from proxy.proxy_ip_pool import IpInfoModel
|
||||
@@ -183,9 +183,11 @@ def format_proxy_info(ip_proxy_info) -> Tuple[Optional[Dict], Optional[Dict]]:
|
||||
"username": ip_proxy_info.user,
|
||||
"password": ip_proxy_info.password,
|
||||
}
|
||||
httpx_proxy = {
|
||||
f"{ip_proxy_info.protocol}": f"http://{ip_proxy_info.user}:{ip_proxy_info.password}@{ip_proxy_info.ip}:{ip_proxy_info.port}"
|
||||
}
|
||||
# httpx 0.28.1 需要直接传入代理URL字符串,而不是字典
|
||||
if ip_proxy_info.user and ip_proxy_info.password:
|
||||
httpx_proxy = f"http://{ip_proxy_info.user}:{ip_proxy_info.password}@{ip_proxy_info.ip}:{ip_proxy_info.port}"
|
||||
else:
|
||||
httpx_proxy = f"http://{ip_proxy_info.ip}:{ip_proxy_info.port}"
|
||||
return playwright_proxy, httpx_proxy
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user