https://www.toutiao.com/article/7470490693815861775

C#中使用webview2来批量下载网页中的图片
2025-02-12 20:07·办公小技巧
看到某些网页上的图片,是不是有一种想全部保存下来的想法了,一张一张来保存显示不是我们的风格,也没有效率,那么有没有一个工具可以批量把网页上显示的图片都保存下来,并且还能过滤掉小图片、广告图片了?

虽然有各种插件可以实现保存网页上的图片,但是还是想写一个适合自己用的,输入一个网页,然后把符合条件的图片保存到本地来,这里有两种方法,一种是把网页显示出来,另一种不显示网页,直接提取网页源码中的图片地址,在这里主要是讲一种怎么实现。

最终的效果

需要实现的功能
1、加载显示网页使用C#中的webview2来实现

2、过滤图片通过图片的宽度和高度来实现,只有宽度和高度都符合的图片才进行提取

3、可以实现图片的批量下载,并根据网页的标题保存到指定的文件夹中

4、大部分网页上的图片可以保存,有些网页图片可以在浏览器中查看,但是在软件中要下载时要么需要验证,要么需要Cookie,要么图片地址的格式很奇怪,就无法下载。

界面实现
如果想界面美观一些,可以使用antdui,vs2022中本身也没有webview2,需要negut来下载一下,网页在webview2中加载完成显示出来后,提取其中的图片保存到DataGridView中,方便后面进行批量下载。

界面设计

图片大小的过滤
在webview2中,可以使用ExecuteScriptAsync来执行js脚本,在脚本中就可以获取图片的宽和高,然后进行过滤,需要先定义一下图片过滤的参数和类

public static int imgW = 300;
public static int imgH = 200;
public static string imgSavePath = "D:\\ImgDown";
public static string Title = "";
public static string imgUrl = "";
// 定义图片信息的类
public class ImageInfo
{
    [JsonProperty("src")]
    public string Src { get; set; }

    [JsonProperty("width")]
    public int Width { get; set; }

    [JsonProperty("height")]
    public int Height { get; set; }
}
public class ImageDown
{
    public string url { get; set; }
    public int index { get; set; }
}

可以自行设计个界面来设置过滤图片的宽、高、默认保存路径等参数。

再新建一个类文件,就叫imghelp.cs吧,用来存放一些小东西,方便使用,如验证url的、获取扩展名的、正则提取标题的等。

/// <summary>
/// 获取扩展名
/// </summary>
/// <param name="contentType"></param>
/// <returns></returns>
public static string GetExtType(string contentType)
{
    switch (contentType)
    {
        case "image/jpeg":
            return "jpg";
        case "image/png":
            return "png";
        case "image/gif":
            return "gif";
        case "image/bmp":
            return "bmp";
        case "image/webp":
            return "webp";
        default:
            return string.Empty; // 未知类型
    }
}

/// <summary>
/// 正则表达式判断是否是一个网址
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static bool IsValidUrl(string input)
{
    // 正则表达式匹配常见的网址格式
    string pattern = @"^(https?|ftp):\/\/([^\s\/$.?#].[^\s]*)$";
    return Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase);
}

/// <summary>
/// 正则表达式:匹配中文字符
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static MatchCollection MatchChineseCharacters(string input)
{
    // 正则表达式:匹配中文字符
    string pattern = @"[\u4e00-\u9fa5]";
    return Regex.Matches(input, pattern);
}

/// <summary>
/// 提取标题,去除字符串中的符号、括号内内容,返回长度不超过10个字符的标题
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string GetTitle(string input)
{
    StringBuilder sb = new StringBuilder();
    bool inBracket = false; // 标记是否在括号内
    foreach (char c in input)
    {
        if (c == '(')
        {
            inBracket = true; // 进入括号内
        }
        else if (c == ')')
        {
            inBracket = false; // 离开括号内
        }
        else if (c == '【')
        {
            inBracket = true; // 进入括号内
        }
        else if (c == '】')
        {
            inBracket = false; // 离开括号内
        }
        else if (c == '(')
        {
            inBracket = true; // 进入括号内
        }
        else if (c == ')')
        {
            inBracket = false; // 离开括号内
        }
        else if (c == '[')
        {
            inBracket = true; // 进入括号内
        }
        else if (c == ']')
        {
            inBracket = false; // 离开括号内
        }
        else if (c == '-')
        {
            inBracket = true; // 进入括号内
        }
        else if (!inBracket)
        {
            sb.Append(c); // 仅在不在括号内时添加字符到结果中
        }
    }
    string pattern = @"[^\u4e00-\u9fa5]";
    string result = Regex.Replace(sb.ToString(), pattern, "");
    if (result.Length > 10)
    {
        result = result.Substring(0, 10);
    }
    return result;
}

打开一个网址
这里打开的网址需要带http或https,不然无法使用,可以使用IsValidUrl来验证一下是否符合要求,然后等着网页打开就可以了,处理一下按钮和表。

           //打开一个新网址
            DG_show.Rows.Clear();
            btn_down.Enabled= false; 
            string url = txt_url.Text.Trim().ToString();
            if (ImgHelp.IsValidUrl(url))
            {
                alert1.Text = "打开网页,图片获取中...";
                web2.Source = new Uri(url);
            }
            else
            {
                MessageBox.Show("输入的似乎不是一个正确的网址,需要包含http部分!", "错误提醒!", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
那怎么知道网页已经加载完成了呢,咱们看到网页显示出来不算数,得通过这个事件来判断

private void web2_NavigationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e)
{
    if (e.IsSuccess)
    {
        //开始采集图片
        CaptureImages();
    }
}
如果网页加载成功了,完全显示出来了,就开始通过CaptureImages提取图片吧
/// <summary>
/// 网页加载完成后,采集图片
/// </summary>
private async void CaptureImages()
{
    await web2.EnsureCoreWebView2Async(); // 确保WebView2已初始化并加载核心组件。
    string script = @"
        var images = document.querySelectorAll('img');
        var imageData = [];
        images.forEach(img => {
            imageData.push({
                src: img.src,
                width: img.width,
                height: img.height
            });
        });
        imageData;
    ";
    var result = await web2.CoreWebView2.ExecuteScriptAsync(script);

    // 解析返回的JSON字符串
    var imageData = JsonConvert.DeserializeObject<List<ImageInfo>>(result);

    // 输出图片地址
    DG_show.Rows.Clear();
    foreach (var image in imageData)
    {
        if (image.Width >= imgW && image.Height >= imgH)
        {
            DG_show.Rows.Add(image.Src, image.Width, image.Height);
        }

    }
    int num = imageData.Count;
    int num1 = DG_show.Rows.Count;
    alert1.Text = $"获取完成,本次共获取到{num}张图片,过滤后还剩余{num1}张图片!";
    Title =ImgHelp.GetTitle(web2.CoreWebView2.DocumentTitle);
    this.Text = Title;
    btn_down.Enabled = true;
}

其中最关键的是script这地方,能不能提取到图片的地址全靠它了,提取完成后判断一下,符合条件的放到表格中,不符合的直接不管了。

下载图片
下载图片本以为很简单,但实现起来很是头大,并不能直接把webview2中显示的图片直接保存出来,是不是很心塞,网上有例子说通过ExecuteScriptAsync来执行脚本,可以把显示的图片重画一下,然后保存,但是试了好久都不成功,直接放弃了。

然后就是通过HttpClient来保存图片了,现在好些图片要么需要模拟自己是浏览器打开的,要么需要带有cookie验证,反正能直接打开的很少,还有一些图片路径很反人类,也读取不出来,所以不要看图片显示出来了,能保存出来的也只有一部分

保存图片当然要使用多线程了,不然一个一个的下载也没效率,使用tasks来实现多线程,并等待所有的线程下载完成。

//下载图片
btn_down.Enabled = false;
int num = DG_show.Rows.Count;
if (num < 1)
{
    MessageBox.Show("表格中没有需要下载的图片内容!", "错误提醒!", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    return;
}

////启用线程池,进行多线程下载
string url = "";
Task[] tasks = new Task[num];
for (int i = 0; i < num; i++)
{
    ImageDown imgdown = new ImageDown();
    url = DG_show.Rows[i].Cells["图片地址"].Value.ToString();
    imgdown.url = url;
    imgdown.index = i;

    tasks[i] = Task.Run(() => DownloadImageAsync(imgdown));
}

await Task.WhenAll(tasks);
MessageBox.Show(“图片已经下载完成,默认保存在D盘根目录下!”, “完成提醒!”, MessageBoxButtons.OK, MessageBoxIcon.Warning);
btn_down.Enabled = true;
DownloadImageAsync下载图片,需要注意的是文件名的生成,如果想要使用图片原来的名称,有些文件名很奇怪,会导致保存失败,这里获取一下扩展名,然后直接按在表格中的顺序来命名图片了。


private async Task DownloadImageAsync(ImageDown imageDown)
{
    string imageUrl = imageDown.url;
    int index = imageDown.index;
    string img_path = imgSavePath.Trim() + "\\" + Title.Trim();
    if (!Directory.Exists(img_path))
    {
        Directory.CreateDirectory(img_path);
    }
    try
    {
        if (imageUrl.StartsWith("data:image"))
        {

        }
        else
        {
            // 图片下载,增加头文件
            using (HttpClient client = new HttpClient())
            {
                client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
                client.DefaultRequestHeaders.Add("Accept", "image/webp,image/apng,image/*,*/*;q=0.8");
                client.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.9");
                //获取扩展名
                var headResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, imageUrl));
                string contentType = headResponse.Content.Headers.ContentType.MediaType;
                string ext =ImgHelp.GetExtType(contentType);

                byte[] imageData = await client.GetByteArrayAsync(imageUrl);

                HttpResponseMessage response = await client.GetAsync(imageUrl);
                response.EnsureSuccessStatusCode(); // 确保请求成功
                //string fileName = Path.GetFileName(new Uri(imageUrl).LocalPath).Trim();
                string fileName =index.ToString()+"."+ext;
                string savePath = Path.Combine(img_path, fileName).Trim();
                using (FileStream fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write))
                {
                    await response.Content.CopyToAsync(fileStream);
                }
                updateGridView(index, Color.GreenYellow); //更新DataGridView
            }
        }
    }
    catch (Exception ex)
    {
        updateGridView(index, Color.Red);//更新DataGridView
        Console.WriteLine(ex.Message.ToString());
    }
}

在线程中更新表格的UI,也就是下载成功的加个绿色,失败的加个红色

private void updateGridView(int i, Color c)
{
    if (DG_show.InvokeRequired)
    {
        DG_show.BeginInvoke(new Action<int, Color>(updateGridView), i, c);
    }
    else
    {
        DG_show.Rows[i].DefaultCellStyle.BackColor = c;
    }
}

完成
到此,基本上就已经完成了,可以输入几个网址试一下运行的效果。

这种方法保存图片有个缺点,就是有些网页的显示速度比较慢,还有同一个网址在第二次打开时,无法获取到图片的地址,这算是个bug吧,应该是这一句导致的,但去掉又不合适。

await web2.EnsureCoreWebView2Async(); // 确保WebView2已初始化并加载核心组件。

文档更新时间: 2025-02-13 09:05   作者:admin