如何用HttpClient结合ProgressBar下载文件?遇无响应问题求助
问题解决与需求实现
一、按钮无响应的原因与修复
当前代码存在多个导致无响应/无输出的问题,逐一修复:
1. URL缺少协议头
你的图片URLmysite.com/sites/default/files/1231105.gif未指定http://或https://,WebClient无法识别合法请求地址,需补全协议:
list.Add("https://mysite.com/sites/default/files/1231105.gif"); list.Add("https://mysite.com/sites/default/files/1231040.gif");
2. 线程不安全的集合操作
Parallel.ForEach是多线程执行,直接对List<Website>调用Add会引发线程冲突,导致集合损坏或数据丢失。替换为线程安全的ConcurrentBag<Website>:
// 替换原List<Website> list = new List<Website>(); ConcurrentBag<Website> bag = new ConcurrentBag<Website>(); // 在Parallel.ForEach中替换list.Add(results); bag.Add(results);
3. 图片用DownloadString错误
图片是二进制数据,DownloadString会按文本编码解析,导致数据损坏且无法正确下载。改为使用DownloadData或直接DownloadFile保存到本地。
4. 进度报告的线程安全问题
原代码中progressReport是共享实例,多线程下同时修改会导致进度数据混乱。应在每个循环内创建新的ProgressReport实例,避免冲突。
二、修改为图片下载并保存到本地
调整SiteDownload类,改为下载图片到本地指定目录,同时更新相关逻辑:
using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Threading; using System.Collections.Concurrent; using System.IO; public class SiteDownload { // 图片保存目录,可按需修改 private static readonly string SaveDirectory = Path.Combine(Environment.CurrentDirectory, "DownloadedImages"); static SiteDownload() { // 创建保存目录(不存在则创建) if (!Directory.Exists(SaveDirectory)) { Directory.CreateDirectory(SaveDirectory); } } public static List<string> Sites() { List<string> list = new List<string>(); // 补全协议头 list.Add("https://mysite.com/sites/default/files/1231105.gif"); list.Add("https://mysite.com/sites/default/files/1231040.gif"); return list; } public static async Task<List<Website>> ParallelDownload(IProgress<ProgressReport> progress, CancellationTokenSource cancellationTokenSource) { List<string> sites = Sites(); ConcurrentBag<Website> bag = new ConcurrentBag<Website>(); ParallelOptions parallelOptions = new ParallelOptions(); parallelOptions.MaxDegreeOfParallelism = 8; parallelOptions.CancellationToken = cancellationTokenSource.Token; await Task.Run(() => { try { Parallel.ForEach<string>(sites, parallelOptions, (site) => { Website results = DownloadImage(site); bag.Add(results); // 创建新的进度报告实例,避免多线程冲突 var progressReport = new ProgressReport { SitesDownloaded = bag.ToList(), PercentageComplete = (bag.Count * 100) / sites.Count }; progress.Report(progressReport); parallelOptions.CancellationToken.ThrowIfCancellationRequested(); }); } catch (OperationCanceledException) { // 取消操作无需重新抛出,上层会处理 } }); return bag.ToList(); } private static Website DownloadImage(string url) { Website website = new Website(); website.Url = url; // 提取文件名 string fileName = Path.GetFileName(url); string savePath = Path.Combine(SaveDirectory, fileName); using (WebClient client = new WebClient()) { // 添加User-Agent请求头 client.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0 Chrome"); // 下载并保存到本地 client.DownloadFile(url, savePath); website.LocalSavePath = savePath; } return website; } public class Website { public string Url { get; set; } // 新增本地保存路径字段 public string LocalSavePath { get; set; } } public class ProgressReport { public int PercentageComplete { get; set; } public List<Website> SitesDownloaded { get; set; } } }
同时更新Form1中的PrintResults方法,适配新的Website类:
void PrintResults(List<Website> results) { richTextBox1.Text = string.Empty; foreach (var item in results) richTextBox1.Text += $"已下载:{item.Url}{Environment.NewLine}保存路径:{item.LocalSavePath}{Environment.NewLine}{Environment.NewLine}"; }
三、使用HttpClient添加请求头的方法
如果要替换WebClient为HttpClient(推荐长期复用HttpClient实例),可按以下方式设置请求头:
1. 设置全局默认请求头
创建复用的HttpClient实例,初始化时添加默认请求头:
// 全局复用的HttpClient实例,不要每次创建新的 private static readonly HttpClient _httpClient = new HttpClient(); static SiteDownload() { // 添加默认User-Agent _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0 Chrome"); // 创建保存目录 if (!Directory.Exists(SaveDirectory)) { Directory.CreateDirectory(SaveDirectory); } }
2. 单请求添加请求头(如需单独设置)
在具体请求中添加:
using (var request = new HttpRequestMessage(HttpMethod.Get, url)) { // 单独添加请求头(全局已设置可省略) request.Headers.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0 Chrome"); using (var response = await _httpClient.SendAsync(request, cancellationToken)) { response.EnsureSuccessStatusCode(); using (var stream = await response.Content.ReadAsStreamAsync()) using (var fileStream = new FileStream(savePath, FileMode.Create)) { await stream.CopyToAsync(fileStream); } } }
其他注意事项
- 确保程序有写入
SaveDirectory的权限,避免权限不足导致保存失败。 - 若目标网站有反爬机制,除了User-Agent,可能还需添加Referer等其他请求头。
- IO密集型场景(下载)下,更推荐使用
Task.WhenAll代替Parallel.ForEach,避免线程池资源浪费:
// 替换ParallelDownload中的Task.Run+Parallel.ForEach var tasks = sites.Select(site => DownloadImageAsync(site, cancellationTokenSource.Token)); var results = await Task.WhenAll(tasks); // 单独实现进度统计逻辑
内容的提问来源于stack exchange,提问作者Daniel Lip
相关产品推荐
相关产品推荐

