基于.NetCore开发博客项目 StarBlog - (17) 自动下载文章里的外部图片
系列文章
- 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客?
- 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目
- 基于.NetCore开发博客项目 StarBlog - (3) 模型设计
- 基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入
- 基于.NetCore开发博客项目 StarBlog - (5) 开始搭建Web项目
- 基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表
- 基于.NetCore开发博客项目 StarBlog - (7) 页面开发之文章详情页面
- 基于.NetCore开发博客项目 StarBlog - (8) 分类层级结构展示
- 基于.NetCore开发博客项目 StarBlog - (9) 图片批量导入
- 基于.NetCore开发博客项目 StarBlog - (10) 图片瀑布流
- 基于.NetCore开发博客项目 StarBlog - (11) 实现访问统计
- 基于.NetCore开发博客项目 StarBlog - (12) Razor页面动态编译
- 基于.NetCore开发博客项目 StarBlog - (13) 加入友情链接功能
- 基于.NetCore开发博客项目 StarBlog - (14) 实现主题切换功能
- 基于.NetCore开发博客项目 StarBlog - (15) 生成随机尺寸图片
- 基于.NetCore开发博客项目 StarBlog - (16) 一些新功能 (监控/统计/配置/初始化)
- 基于.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<CommonService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public CommonService(ILogger<CommonService> 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<CommonService>();
处理Markdown
下载图片的功能搞定了之后,我们继续来做markdown处理的部分
关于C#处理Markdown,之前已经有过多次探索了,可以说是轻车熟路了hhh~
附上之前关于Markdown处理的文章:
依然是用Markdig这个库(貌似.NetCore处理markdown上也没其他选择)
在PostService.cs中增加代码
/// <summary>
/// Markdown中外部图片下载
/// <para>如果Markdown中包含外部图片URL,则下载到本地且进行URL替换</para>
/// </summary>
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<Post> 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接口开发部分的文章里会详细介绍这个流程。
到这里就搞定啦~
参考资料
- 官方文档:https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-6.0
- c#:HttpClient使用详解:https://blog.csdn.net/u010476739/article/details/119782562
- C#中HttpClient的使用小结:https://zhuanlan.zhihu.com/p/89106847
基于.NetCore开发博客项目 StarBlog - (17) 自动下载文章里的外部图片的更多相关文章
- 基于.NetCore开发博客项目 StarBlog - (18) 实现本地Typora文章打包上传
前言 九月太忙,只更新了三篇文章,本来这个功能是从九月初就开始做的,结果一直拖到现在国庆假期才有时间完善并且写文章~ 之前我更新了几篇关于 Python 的文章,有朋友留言问是不是不更新 .Net 了 ...
- 基于.NetCore开发博客项目 StarBlog - (19) Markdown渲染方案探索
前言 笔者认为,一个博客网站,最核心的是阅读体验. 在开发StarBlog的过程中,最耗时的恰恰也是文章的展示部分功能. 最开始还没研究出来如何很好的使用后端渲染,所以只能先用Editor.md组件做 ...
- 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 ... 基于. ...
- 基于.NetCore开发博客项目 StarBlog - (3) 模型设计
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (5) 开始搭建Web项目
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (7) 页面开发之文章详情页面
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (8) 分类层级结构展示
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
随机推荐
- 秋招如何抱佛脚?2022最新大厂Java面试真题合集(附答案
2022秋招眼看着就要来了,但是离谱的是,很多同学最近才想起来还有秋招这回事,所以纷纷临时抱佛脚,问我有没有什么快速磨枪的方法, 我的回答是:有! 说起来,临阵磨枪没有比背八股文更靠谱的了,很多人对这 ...
- Flink中如何实现一个自定义MetricReporter
什么是 Metrics 在 flink 任务运行的过程中,用户通常想知道任务运行的一些基本指标,比如吞吐量.内存和 cpu 使用情况.checkpoint 稳定性等等.而通过 flink metric ...
- Simple, Fast Malicious Multiparty Private Set Intersection-解读
文本记录阅读该论文的笔记. 这是文章框架,来自视频. 介绍 本文主要解决恶意攻击下安全的多方PSI,主要用到两大技术OPPRF和OKVS,构造合谋和不合谋的协议. 基础知识 OPPRF 这部分在OPR ...
- ES6 - promise(3)
上一篇熟悉了promise的具体过程: promise的过程: 启动异步任务 => 返回promise对象 =>给promise对象绑定回调函数(甚至可以在异步任务结束后指定多个). 从p ...
- django框架11
内容概要 用户登录之后跳转到用户登录之前想要访问的页面 django操作cookie补充 django操作session django操作session补充 CBV添加装饰器 django中间件 自定 ...
- 【Java面试】Mybatis中#{}和${}的区别是什么?
一个工作2年的粉丝,被问到一个Mybatis里面的基础问题. 他跑过来调戏我,说Mic老师,你要是能把这个问题回答到一定高度,请我和一个月奶茶. 这个问题是: "Mybatis里面#{}和$ ...
- BUUCTF-菜刀666
菜刀666 这题和之前做过的流量题不同,对我还是有些难度.看了看大佬的wp才做出来的 wireshark打开流量包,一开始只是单纯过滤http,包很多,看花了眼,看了好多也没觉得有啥异常. 后面才知道 ...
- Vue 3.0 有哪些新特性值得我们提前了解
一.迎接 Vue 3.0 简介 Vue.js 作者兼核心开发者尤雨溪宣布 Vue 3.0 进入 Beta 阶段. 已合并所有计划内的 RFC 已实现所有被合并的 RFC Vue CLI 现在通过 ...
- 《The Tail At Scale》论文详解
简介 用户体验与软件的流畅程度是呈正相关的,所以对于软件服务提供方来说,保持服务耗时在用户能接受的范围内就是一件必要的事情.但是在大型分布式系统上保持一个稳定的耗时又是一个很大的挑战,这篇文章解析的是 ...
- NC17059 队列Q
NC17059 队列Q 题目 题目描述 ZZT 创造了一个队列 Q.这个队列包含了 N 个元素,队列中的第 i 个元素用 \(Q_i\) 表示.Q1 表示队头元素,\(Q_N\) 表示队尾元素.队列中 ...