c# C#编写网络爬虫时的并发控制和IP池管理

来源:这里教程网 时间:2026-02-21 17:41:50 作者:

为什么 HttpClient 不能全局复用又不能每次都 new

在 C# 爬虫里直接

new HttpClient()
十次,会快速耗尽本地端口(TIME_WAIT 状态堆积),而长期复用同一个
HttpClient
实例又可能因 DNS 缓存、连接池僵死或服务端连接关闭导致后续请求失败。正确做法是复用单个静态
HttpClient
实例,但必须配合
HttpClientHandler
的精细配置:

MaxConnectionsPerServer
设为合理值(如 10–50),避免单域名压垮对方或触发风控
启用
UseProxy = false
(除非你明确走代理)+
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
设置
Timeout
(建议 15–30 秒),防止某个请求卡死拖垮整个并发队列

如何用 SemaphoreSlim 控制并发请求数量

SemaphoreSlim
是轻量级、支持 async/await 的信号量,比
Task.Run + lock
更适合爬虫这种 I/O 密集型场景。它不阻塞线程,只限制同时进入临界区的协程数。

private static readonly SemaphoreSlim _throttle = new SemaphoreSlim(5); // 同时最多 5 个请求
<p>public async Task<string> FetchAsync(string url)
{
await _throttle.WaitAsync();
try
{
return await _httpClient.GetStringAsync(url);
}
finally
{
_throttle.Release();
}
}

注意:不要在

using
块里创建
SemaphoreSlim
,它需跨请求复用;释放必须放在
finally
,否则异常后信号量永远卡死。

IP 池不是“存一堆代理就完事”,关键在可用性验证和轮换策略

未经验证的代理列表基本不可用——超时、返回空、被目标站重定向到验证码页、甚至返回 403 伪装成成功。真实 IP 池管理必须包含:

启动时对每个代理执行预检:
HEAD
或轻量
GET
到一个稳定响应的测试地址(如
http://httpbin.org/ip
),记录响应时间与状态码
运行中动态降权:某代理连续 2 次超时或返回非 2xx,将其权重设为 0,10 分钟后尝试恢复 轮换不等于随机:优先选响应快、错误率低、未被当前目标站封禁的代理;可用
SortedSet<proxyitem></proxyitem>
Score
排序,每次取
First()

别把代理 IP 和端口拼成字符串存 List —— 定义

class ProxyItem { public string Address; public int Port; public double Score; }
,否则后期加字段、排序、过滤全得重写。

HttpClientHandler 的 Proxy 设置容易踩的坑

HttpClientHandler
赋值
Proxy
时,常见错误是传入
new WebProxy("http://127.0.0.1:8888")
却没关掉默认凭据:

UseDefaultCredentials = true
会让请求带上 Windows 登录凭据,多数 HTTP 代理不认,直接 407
漏设
BypassProxyOnLocal = true
,导致访问
localhost
或内网地址也走代理,超时失败
代理地址写成
"http://..."
,但实际要求不带协议(
"127.0.0.1:8888"
),不同代理中间件要求不一致

安全写法:

var handler = new HttpClientHandler
{
    Proxy = new WebProxy("127.0.0.1:8888"),
    UseProxy = true,
    UseDefaultCredentials = false,
    BypassProxyOnLocal = true
};

IP 池切换时,别反复 new

HttpClientHandler
—— 创建开销大,改用
handler.Proxy = new WebProxy(nextProxy)
动态赋值更高效。

相关推荐