You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 13:50:19