如何高效下载大量图片URL并打包为Zip文件?
高效实现方案
原代码的核心问题
- 每次循环重复打开/关闭Zip文件并进入
Update模式:该模式需要每次扫描整个Zip的目录结构,4万次操作会产生巨大的额外开销,是效率低下的主要原因。 - 串行下载:网络IO是主要瓶颈,串行下载无法充分利用带宽。
- 未合理复用
HttpClient:若_httpClient是每次循环创建,会导致连接池无法复用,增加网络握手开销。
优化方案
- 复用HttpClient:使用单例
HttpClient,利用其内置的连接池减少网络握手次数。 - 一次性打开ZipArchive:使用
Create模式打开Zip流并保持其全程打开,避免重复扫描Zip结构。 - 并行下载+串行写入Zip:通过并行下载提升网络利用率,同时用线程锁保证Zip写入的线程安全(因为
ZipArchive本身非线程安全)。 - 控制并发数:避免并发过高导致目标网站封禁或本地资源耗尽。
- 异常隔离:单个URL下载失败不中断整个流程,记录错误信息。
代码实现
using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Threading.Tasks; using System.Threading; class ImageDownloader { // 复用单例HttpClient,减少连接开销 private static readonly HttpClient _httpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(30) // 设置合理超时时间 }; public static async Task DownloadAndZipImages(string urlListPath, string zipOutputPath, int maxDegreeOfParallelism = 10) { // 1. 读取所有URL到内存(数十万条URL占用内存可忽略) var urls = new List<string>(); using (var reader = new StreamReader(urlListPath)) { string line; while ((line = await reader.ReadLineAsync()) != null) { if (!string.IsNullOrWhiteSpace(line)) { urls.Add(line.Trim()); } } } // 2. 一次性打开ZipArchive(Create模式,全程保持打开) using (var zipStream = new FileStream(zipOutputPath, FileMode.Create, FileAccess.Write, FileShare.None)) using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create)) { var zipLock = new object(); // 保证Zip写入操作的线程安全 var failedUrls = new List<string>(); // 3. 并行下载,控制并发数避免资源过载 await Parallel.ForEachAsync(urls, new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, async (url, token) => { try { // 生成唯一文件名,避免重复覆盖 var fileName = $"{Guid.NewGuid()}{Path.GetExtension(new Uri(url).AbsolutePath)}"; // 下载图片流 using (var responseStream = await _httpClient.GetStreamAsync(url, token)) { // 加锁写入Zip条目,保证线程安全 lock (zipLock) { var entry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal); // 可根据需求调整压缩级别 using (var entryStream = entry.Open()) { responseStream.CopyTo(entryStream); } } } } catch (Exception ex) { // 记录失败URL,不中断整体流程 lock (failedUrls) { failedUrls.Add($"{url} - {ex.Message}"); } } }); // 输出失败记录(可选) if (failedUrls.Count > 0) { await File.WriteAllLinesAsync("failed_urls.txt", failedUrls); Console.WriteLine($"下载完成,共{failedUrls.Count}个URL下载失败,详情见failed_urls.txt"); } else { Console.WriteLine("所有图片下载并打包完成"); } } } // 调用示例 static async Task Main(string[] args) { await DownloadAndZipImages("UrlList.txt", "images.zip", maxDegreeOfParallelism: 15); } }
额外优化建议
- 压缩级别调整:若不需要压缩,可将
CompressionLevel.Optimal改为CompressionLevel.NoCompression,进一步提升写入速度。 - 文件名策略:如果需要保留原文件名,可对重复文件名添加序号(如
image.jpg→image_1.jpg),避免Zip条目覆盖。 - 重试机制:若目标网站有反爬限制,可添加重试逻辑(如捕获特定异常后重试2-3次)。
内容的提问来源于stack exchange,提问作者AnMSLbR
相关产品推荐
相关产品推荐

