本文目录导读:

这是一个非常好的问题,答案是:实用脚本当然能,但“批量高并发”本身是一个需要谨慎设计的特性,而不是脚本的天然属性。
一个普通的单线程脚本(比如用 for 循环挨个处理)不能实现高并发。 通过特定的编程技巧、使用合适的语言和库,你可以把脚本改造成具备高并发能力的工具。
下面来拆解一下这个问题,并给出实用建议。
核心概念:并行、并发与I/O
首先分清两个概念:
- 并发 (Concurrency):逻辑上同时处理多个任务(比如在一个时间片内快速切换处理A、B、C),单核CPU也可以实现。
- 并行 (Parallelism):物理上同时处理多个任务(需要多核CPU)。
- I/O密集型 vs. CPU密集型:
- I/O密集型:脚本大部分时间在等待网络响应、磁盘读写、数据库查询,爬虫、批量下载文件、API请求。
- CPU密集型:脚本大部分时间在计算、处理数据,视频转码、图像处理、加密解密。
绝大多数“实用脚本”是I/O密集型的(下载、上传、发请求、读写文件),对于I/O密集型任务,高并发效果非常显著,对于CPU密集型任务,高并发效果有限,主要靠多进程并行。
不同语言的批量高并发能力对比
| 语言/工具 | 高并发方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| Python | asyncio (协程), aiohttp, httpx |
网络爬虫、API调用、WebSocket通信 | 代码优雅,生态好(爬虫、数据处理库丰富) | GIL限制CPU密集型并行;asyncio学习曲线稍陡 |
| Node.js | 事件循环 + async/await + Promise.all |
I/O密集型任务(如高并发HTTP请求) | 天生异步,非阻塞I/O,性能极高,代码简洁 | 单线程,CPU密集型任务表现差 |
| Go | goroutine + channel |
微服务、API网关、系统工具、高并发服务器 | 天生支持高并发,内存占用极低,性能优异 | 语法稍显另类,生态不如Python/JS丰富 |
| Bash | xargs -P , & + wait |
批量执行命令、文件操作 | 极其简单,零依赖 | 并发控制粗糙,无错误处理,功能单一 |
| PowerShell | ForEach-Object -Parallel |
Windows环境下的批量操作 | 系统内置,与Windows集成好 | 跨平台弱,复杂任务性能不如Python/Go |
如何让你的脚本实现批量高并发?(实用代码示例)
Python (最推荐):使用 asyncio 和 aiohttp
这是写爬虫和API批量调用最主流的方式。
import asyncio
import aiohttp
import time
# 假设这是你要并发访问的100个URL
urls = [f"http://example.com/api/item/{i}" for i in range(100)]
async def fetch_one(session, url):
"""异步地获取一个URL"""
try:
async with session.get(url) as response:
# 模拟处理响应(比如解析JSON)
data = await response.text()
# print(f"成功获取: {url}")
return data
except Exception as e:
print(f"失败: {url}, 错误: {e}")
return None
async def main():
"""主协程,控制并发"""
connector = aiohttp.TCPConnector(limit=50) # 限制最大并发连接数为50
async with aiohttp.ClientSession(connector=connector) as session:
# 创建一个任务列表
tasks = [fetch_one(session, url) for url in urls]
# 使用 asyncio.gather 并发执行所有任务
results = await asyncio.gather(*tasks)
print(f"总共成功获取 {len([r for r in results if r])} 个结果")
if __name__ == "__main__":
start = time.time()
asyncio.run(main())
print(f"耗时: {time.time() - start:.2f}秒")
关键点:
asyncio.gather:把多个协程打包并发执行。TCPConnector(limit=50):防止一次性开太多连接导致被目标服务器封禁或耗尽本地端口。
Node.js:使用 Promise.all
对于熟悉JavaScript的前端或全栈开发者,这是最自然的写法。
// 需要安装: npm install axios
const axios = require('axios');
const urls = Array.from({ length: 100 }, (_, i) => `http://example.com/api/item/${i}`);
async function fetchOne(url) {
try {
const response = await axios.get(url);
// console.log(`成功获取: ${url}`);
return response.data;
} catch (error) {
console.error(`失败: ${url}, 错误: ${error.message}`);
return null;
}
}
async function main() {
// 同时发起所有请求
const promises = urls.map(url => fetchOne(url));
const results = await Promise.all(promises);
console.log(`总共成功获取 ${results.filter(r => r).length} 个结果`);
}
main();
关键点:
Promise.all:接收一个Promise数组,并发执行。- 注意:上面的代码会瞬间发起100个请求,可能耗尽本机资源或被目标服务器拒绝,更好的做法是使用批处理或
p-limit库来控制并发数。
// 控制并发数到20个
const pLimit = require('p-limit');
const limit = pLimit(20);
const limitedPromises = urls.map(url => limit(() => fetchOne(url)));
const results = await Promise.all(limitedPromises);
Go:goroutine + WaitGroup (最彻底的高并发)
Go从语言层面解决了高并发问题。
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
func fetchOne(url string, wg *sync.WaitGroup) {
defer wg.Done() // 确保任务完成后通知WaitGroup
resp, err := http.Get(url)
if err != nil {
fmt.Printf("失败: %s, 错误: %v\n", url, err)
return
}
defer resp.Body.Close()
// 读取并丢弃body (或处理)
_, _ = io.ReadAll(resp.Body)
// fmt.Printf("成功获取: %s\n", url)
}
func main() {
urls := make([]string, 100)
for i := 0; i < 100; i++ {
urls[i] = fmt.Sprintf("http://example.com/api/item/%d", i)
}
start := time.Now()
var wg sync.WaitGroup
// 使用channel作为信号量控制并发数 (这里限制为20)
sem := make(chan struct{}, 20)
for _, url := range urls {
wg.Add(1)
sem <- struct{}{} // 获取一个令牌,如果满20个会阻塞
go func(u string) {
defer func() { <-sem }() // 释放令牌
fetchOne(u, &wg)
}(url)
}
wg.Wait() // 等待所有goroutine完成
fmt.Printf("耗时: %v\n", time.Since(start))
}
关键点:
sync.WaitGroup:等待所有goroutine完成。channel作为信号量:优雅地控制并发数(Throttling)。
Bash:一个非常实用的“懒人”方案
如果你只是需要在Linux环境下快速执行一个命令,比如批量下载或检查网站状态。
#!/bin/bash
# 从urls.txt读取URL,并发20个任务下载
cat urls.txt | xargs -P 20 -I {} wget -q {}
echo "所有下载任务已启动完毕"
# 或者并发检查HTTP状态码
cat urls.txt | xargs -P 50 -I {} curl -s -o /dev/null -w "{} %{http_code}\n"
关键注意事项(避坑指南)
- 控制并发数(Concurrency Limit):永远不要无限制地并发(比如同时发10万个请求),这会导致:
- 目标服务器封禁你的IP。
- 本地机器资源耗尽(端口、内存、文件描述符)。
- 网络拥塞,使用信号量(Semaphore)、连接池、生产者-消费者队列来控制。
- 错误处理与重试:高并发下网络错误是常态,脚本必须能优雅地处理超时、连接拒绝、状态码错误等,并实现指数退避重试。
- 幂等性(Idempotency):如果任务失败了需要重试,确保多次执行同一任务的结果是一样的(比如多次写入同一行数据不会出问题)。
- 限流(Rate Limiting):很多API有每秒请求次数限制(例如100 req/s),你需要用令牌桶或滑动窗口算法来遵守这个规则。
- 资源管理:务必正确关闭网络连接、文件句柄,在Python中用
async with,在Go中用defer resp.Body.Close()。
你需要什么?
- 如果你写爬虫、批量调用API:强烈推荐Python +
asyncio+aiohttp(或httpx),代码可读性好,生态强大,调试方便。 - 如果你需要极致的网络I/O性能,且团队熟悉JS:Node.js 是好选择。
- 如果你需要一个轻量、高性能的系统工具(比如API网关、中间件):Go 是最佳答案。
- 如果你只是想快速完成一次性的bash文件批量操作:Bash +
xargs -P是最简单粗暴且有效的方法。
一句话:实用脚本当然能批量高并发,但你必须主动去实现它(使用异步、协程、多进程、控制并发数、处理错误),而不是指望脚本默认就有这个能力。