• 基于.NetCore开发博客项目 StarBlog - (17) 自动下载文章里的外部图片


    系列文章

    前言

    好久没更新博客了,上个月底更新了一篇关于StarBlog博客开发的文章之后,就因为线下培训、诗词大会之类的杂七杂八的事浪费了很多时间,有段时间一直在忙这些事情都没空写代码……

    PS:我在诗词大会上分享了这首诗:读白居易的《禽虫十二章》

    然后最近买了杨中科大佬新出的《AspNetCore技术内幕》,看得津津有味,花了一个多星期的时间,把书里的内容大致看了一遍,DDD(领域驱动设计)我早就想学了,不过一直没找到好的入门资料,大佬的这本书就很不错,很好懂,尽管如此,DDD还是一个相对复杂的方法,需要通过不断的实践来掌握。

    虽然最近做了这么多事,但同时工作也很忙,有个项目需要在九月前上线,本来我打算来实践一下DDD的,不过写着写着发现还是把握不住,只好先用我之前的DjangoStarter框架,后面再慢慢把我的StarBlog博客用DDD思想进行改造~

    对了,这么久没更新博客的原因,还有一点是我在使用过程中对目前的管理后台非常不满(使用Vue2+ElementUI开发),用户体验极差,所以我同时在构思用何种技术对管理后台前端项目进行重构,目前有几个备选项:

    • blazor(使用C#开发前端,很酷)
    • react(相对其他的来说,我最喜欢的前端技术栈)
    • 仍然vue,但重写现有架构(工作量较小)

    还没拿定主意,在重构完成之前,只能先捏着鼻子用现有的管理后台,同时大概率也不会在现有的前端项目中增加新功能了。

    回到正题

    OK,说回本文的内容。在博客的使用过程中,有时候我会从其他网站复制一些markdown片段,或者是从我在其他平台的博客上复制markdown内容(博客园、掘金之类的),这时候复制过来的markdown内容里面可能会有一些图片,如果不做处理,可能会产生某些问题,如因图片防盗链功能导致网络图片在StarBlog博客中无法显示、网站运营商关闭导致图片丢失等,对于数据,还是牢牢掌握在自己的手中比较放心。

    于是,我就做了这个功能:将markdown文章中的网络图片下载下来,并且替换markdown中的链接

    原理很简单,扫描markdown,把图片链接拿出来下载,同时把图片链接替换成StarBlog上的地址。下面一步步介绍如何在代码中实现。

    下载图片

    首先是下载图片的功能,C#中访问网络,可以使用HttpClient这个标准库

    最简单的用法是这样:

    var client = new HttpClient();
    await client.GetAsync("图片地址");
    

    不过官方文档中并不推荐这种用法,最佳实践是一个程序中只维护一个HttpClient的对象

    在AspNetCore中,我们可以利用依赖注入IHttpClientFactory来管理HttpClient对象。

    Program.cs中注册服务

    builder.Services.AddHttpClient();
    

    在需要的地方注入IHttpClientFactory,比如在本项目中,我们新建一个CommonService.cs来放下载文件的代码,考虑到这个功能以后别的地方也可能用到,所以做成通用的,不和PostService耦合在一起。

    代码如下:

    public class CommonService {
      private readonly ILogger _logger;
      private readonly IHttpClientFactory _httpClientFactory;
    
      public CommonService(ILogger logger, IHttpClientFactory httpClientFactory) {
        _logger = logger;
        _httpClientFactory = httpClientFactory;
      }
      
      public async Task<string?> DownloadFileAsync(string url, string savePath) {
        var httpClient = _httpClientFactory.CreateClient();
        try {
          var resp = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
    
          // 生成随机文件名
          var fileName = GuidUtils.GuidTo16String() + Path.GetExtension(url);
          var filePath = Path.Combine(savePath, WebUtility.UrlEncode(fileName));
          await using var fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write);
          await resp.Content.CopyToAsync(fs);
    
          return fileName;
        }
        catch (Exception ex) {
          _logger.LogError("下载文件出错,信息:{Error}", ex);
          return null;
        }
      }
    }
    

    分析一下部分代码:

    • 第13行代码使用HttpClient的GetAsync方法下载数据,添加了个HttpCompletionOption.ResponseHeadersRead参数,这样我们不必等全部信息加载到内存中后再进行流读取之类的操作,而是在请求头返回的时候就可以进入下一步处理。避免因为要下载的文件太大而导致OutOfMemoryException,这对下载文件的程序来说很重要!
    • 第16行,使用封装好的Guid工具生成16位的GUID,直接用Guid.NewGuid().ToString()也行,这是32位的。
    • 第18-19行,将Http响应内容写入文件流

    搞定,下载文件代码比较简单,涉及到IO操作这种容易出错的地方,细节要处理好,才能保证程序的稳定性。

    PS:别忘了注册服务!

    builder.Services.AddSingleton();
    

    处理Markdown

    下载图片的功能搞定了之后,我们继续来做markdown处理的部分

    关于C#处理Markdown,之前已经有过多次探索了,可以说是轻车熟路了hhh~

    附上之前关于Markdown处理的文章:

    依然是用Markdig这个库(貌似.NetCore处理markdown上也没其他选择)

    PostService.cs中增加代码

    /// 
    /// Markdown中外部图片下载
    /// 如果Markdown中包含外部图片URL,则下载到本地且进行URL替换
    /// 
    private async Task<string> MdExternalUrlDownloadAsync(Post post) {
      if (post.Content == null) return string.Empty;
    
      // 得先初始化目录
      InitPostMediaDir(post);
    
      var document = Markdown.Parse(post.Content);
      foreach (var node in document.AsEnumerable()) {
        if (node is not ParagraphBlock {Inline: { }} paragraphBlock) continue;
        foreach (var inline in paragraphBlock.Inline) {
          if (inline is not LinkInline {IsImage: true} linkInline) continue;
    
          var imgUrl = linkInline.Url;
          
          // 跳过空链接
          if (imgUrl == null) continue;
          // 跳过本站地址的图片
          if (imgUrl.StartsWith(Host)) continue;
    
          // 下载图片
          _logger.LogDebug("文章:{Title},下载图片:{Url}", post.Title, imgUrl);
          var savePath = Path.Combine(_environment.WebRootPath, "media", "blog", post.Id!);
          var fileName = await _commonService.DownloadFileAsync(imgUrl, savePath);
          linkInline.Url = fileName;
        }
      }
    
      await using var writer = new StringWriter();
      var render = new NormalizeRenderer(writer);
      render.Render(document);
      return writer.ToString();
    }
    

    代码说明:

    • 第9行的初始化目录就是检查这篇文章有没有对应的目录,没有就先创建,很简单就不贴代码了。可以在github项目里看到完整代码
    • 第12行开始的两层循环通过遍历markdown文档树,把图片链接找出来
    • 第22行检查图片是站外还是站内的,站内图片不用下载

    这样就完成了markdown里站外图片的下载和链接替换~

    修改文章保存逻辑

    接下来修改一下文章的保存逻辑

    还是在这个PostService.cs里,保存和新增文章共享一个方法:InsertOrUpdateAsync

    直接上代码

    public async Task InsertOrUpdateAsync(Post post) {
      // 是新文章的话,先保存到数据库
      if (await _postRepo.Where(a => a.Id == post.Id).CountAsync() == 0) {
        post = await _postRepo.InsertAsync(post);
      }
    
      // 检查文章中的外部图片,下载并进行替换
      post.Content = await MdExternalUrlDownloadAsync(post);
      // 修改文章时,将markdown中的图片地址替换成相对路径再保存
      post.Content = MdImageLinkConvert(post, false);
    
      // 处理完内容再更新一次
      await _postRepo.UpdateAsync(post);
      return post;
    }
    

    代码说明:

    • 新文章的话,会先保存一次,作为草稿。
    • 先下载外部图片,再替换本地图片链接(关于图片链接替换的,可以参考本系列第4篇文章,上面有链接)
    • 完成这些之后再保存,注意这时文章还是草稿状态,需要通过另一个方法将文章的IsPublish属性设置为true,不过与本文关系不大,这里先不贴代码,后续在RESTFul接口开发部分的文章里会详细介绍这个流程。

    到这里就搞定啦~

    参考资料


    __EOF__

  • 本文作者: 程序设计实验室
  • 本文链接: https://www.cnblogs.com/deali/p/16586437.html
  • 关于博主: 公众号:程序设计实验室,欢迎交流~
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    微信视频通话使用虚拟摄像头
    大神带你玩转异步编程,理论与实践齐飞,敢说是目前最全的讲解了
    赫夫曼树
    【工作技术栈】【源码解读】一次springboot注入bean失败问题的排查过程
    『手撕Vue-CLI』完善提示信息
    【C语言】详解 memset() 函数用法
    【1day】金和协同管理平台c6系统任意文件读取漏洞学习
    【408数据结构与算法】—顺序表的插入、删除和查找(四)
    408-2014
    Day25 Python的文件操作和异常处理
  • 原文地址:https://www.cnblogs.com/deali/p/16586437.html