人称外号大脸猫

Laravel 中封装一个支持动态代理池的通用 HTTP 请求类

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;

class ProxyHttpClient
{
    // 代理配置池
    protected array $proxies;
    
    // 当前使用的代理索引
    protected int $currentProxyIndex = -1;
    
    // 最大重试次数
    protected int $maxRetries;
    
    // 重试延迟(毫秒)
    protected int $retryDelay;

    public function __construct(
        array $proxies,
        int $maxRetries = 3,
        int $retryDelay = 1000
    ) {
        $this->proxies = $proxies;
        $this->maxRetries = $maxRetries;
        $this->retryDelay = $retryDelay;
    }

    /**
     * 发送 HTTP 请求
     *
     * @throws RequestException
     */
    public function request(
        string $method,
        string $url,
        array $options = []
    ) {
        $attempts = 0;

        while ($attempts < $this->maxRetries) {
            $proxy = $this->getNextProxy();

            try {
                return $this->buildClient($proxy)
                    ->{$method}($url, $options)
                    ->throw();

            } catch (RequestException $e) {
                Log::warning("Proxy request failed [{$proxy['name']}]: {$e->getMessage()}");
                
                // 标记失败代理(可选)
                $this->handleFailedProxy($proxy);
                
                $attempts++;
                usleep($this->retryDelay * 1000);
            }
        }

        throw new RequestException("All proxies failed after {$this->maxRetries} attempts");
    }

    /**
     * 获取下一个代理(随机策略)
     */
    protected function getNextProxy(): array
    {
        // 简单随机选择(可扩展为其他策略)
        return $this->proxies[array_rand($this->proxies)];
        
        // 或者使用轮询策略:
        // $this->currentProxyIndex = ($this->currentProxyIndex + 1) % count($this->proxies);
        // return $this->proxies[$this->currentProxyIndex];
    }

    /**
     * 构建 HTTP 客户端
     */
    protected function buildClient(array $proxy): PendingRequest
    {
        return Http::withOptions([
            'proxy' => $this->formatProxyUrl($proxy),
            'connect_timeout' => 10,
            'timeout' => 30,
        ])->withHeaders([
            'User-Agent' => 'Your-Custom-UA/1.0',
        ]);
    }

    /**
     * 格式化代理 URL
     */
    protected function formatProxyUrl(array $proxy): string
    {
        return sprintf(
            '%s://%s:%s@%s:%d',
            $proxy['protocol'] ?? 'http',
            $proxy['username'],
            $proxy['password'],
            $proxy['host'],
            $proxy['port']
        );
    }

    /**
     * 处理失败代理(可扩展)
     */
    protected function handleFailedProxy(array $proxy): void
    {
        // 示例:减少权重、标记不可用等
        // 可结合数据库记录失败次数
        Log::debug("Marking proxy as problematic: {$proxy['name']}");
    }

    /**
     * 快捷方法:GET 请求
     */
    public function get(string $url, array $query = [])
    {
        return $this->request('get', $url, ['query' => $query]);
    }

    /**
     * 快捷方法:POST 请求
     */
    public function post(string $url, array $data = [])
    {
        return $this->request('post', $url, ['form_params' => $data]);
    }
}

配置示例(config/proxies.php):

return [
    'proxies' => [
        [
            'name' => 'proxy1',
            'protocol' => 'http',
            'host' => '1.2.3.4',
            'port' => 8080,
            'username' => 'user1',
            'password' => 'pass1'
        ],
        [
            'name' => 'proxy2',
            'protocol' => 'https',
            'host' => '5.6.7.8',
            'port' => 3128,
            'username' => 'user2',
            'password' => 'pass2'
        ],
        // 更多代理配置...
    ]
];

使用示例:

// 注入配置
$proxies = config('proxies.proxies');

// 创建客户端实例
$client = new ProxyHttpClient($proxies);

try {
    // GET 请求
    $response = $client->get('https://api.example.com/data', ['page' => 1]);
    
    // POST 请求
    $response = $client->post('https://api.example.com/submit', ['key' => 'value']);
    
    // 直接使用 request 方法
    $response = $client->request('put', 'https://api.example.com/update', [
        'json' => ['field' => 'new_value']
    ]);
    
    $data = $response->json();
    
} catch (RequestException $e) {
    // 处理异常
}

选择策略增强:

protected function getNextProxy(): array
{
    // 示例:基于权重的随机选择
    $weights = array_column($this->proxies, 'weight');
    $total = array_sum($weights);
    $rand = mt_rand(1, $total);
    
    foreach ($this->proxies as $index => $proxy) {
        $total -= $weights[$index];
        if ($rand > $total) {
            return $proxy;
        }
    }
}

代理健康检查:

public function checkProxyHealth(array $proxy): bool
{
    try {
        $this->buildClient($proxy)
            ->get('https://www.google.com', ['timeout' => 5])
            ->throw();
        return true;
    } catch (\Exception $e) {
        return false;
    }
}

数据库集成示例:

class DatabaseProxyProvider
{
    public function getActiveProxies(): array
    {
        return Proxy::where('is_active', true)
            ->get()
            ->map(function ($proxy) {
                return [
                    'host' => $proxy->ip,
                    'port' => $proxy->port,
                    'username' => $proxy->username,
                    'password' => $proxy->password,
                    'weight' => $proxy->success_rate
                ];
            })->toArray();
    }
}

使用中间件记录统计信息:

protected function buildClient(array $proxy): PendingRequest
{
    return parent::buildClient($proxy)
        ->withMiddleware(function ($handler) use ($proxy) {
            return function ($request, $options) use ($handler, $proxy) {
                $start = microtime(true);
                
                return $handler($request, $options)->then(
                    function ($response) use ($proxy, $start) {
                        // 记录成功统计
                        $this->logProxyPerformance(
                            $proxy,
                            microtime(true) - $start
                        );
                        return $response;
                    },
                    function ($error) use ($proxy) {
                        // 记录失败统计
                        $this->logProxyFailure($proxy);
                        throw $error;
                    }
                );
            };
        });
}

注意事项:

  • 代理协议需要与实际代理类型匹配(HTTP/HTTPS/Socks5)

  • 建议添加代理IP的自动验证和失效剔除机制

  • 重要操作建议记录详细的请求日志

  • 根据目标网站的防爬策略调整请求频率

  • 考虑使用连接池保持长连接提升性能

  • 对敏感代理凭证建议使用加密存储

可以根据实际需求继续扩展以下功能:

  • 自动代理采购接口集成

  • 代理质量评分系统

  • 地域选择策略(根据目标网站选择对应地区的代理)

  • 请求频率限制

  • 障代理自动隔离和恢复机制

copyright ©2025 ahimu.com all rights reserved 皖ICP备19021547号-1