• 基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入


    系列文章

    前言

    上周介绍了博客的模型设计,现在模型设计好了,要开始导入数据了。

    我们要把一个文件夹内的所有markdown文件导入,目录结构作为文章的分类,文件名作为文章的标题,同时把文件的创建、更新日期作为文章的发表时间。

    大概的思路就是先用.Net的标准库遍历目录,用第三方的markdown解析库处理文章内容,然后通过ORM写入数据库。

    PS:明天就是五一劳动节了,祝各位无产阶级劳动者节日快乐~

    相关技术

    • 文件IO相关API
    • 正则表达式
    • ORM:FreeSQL
    • markdown解析库:Markdig

    开始写代码

    我们首先从最关键的markdown内容解析、图片提取、标题处理说起。

    为了处理markdown内容,我搜了一下相关资料,发现.Net Core目前能用的只有Markdig这个库,由于还处在开发阶段,没有完整文档,只能边看github主页的一点点说明边自己结合例子来用。没办法,没别的好的选择,又懒得(菜)造轮子,只能将就了。

    Markdig官网地址:https://github.com/xoofx/markdig

    StarBlog.Migrate项目里新建一个Class:PostProcessor,我们要在这个class里实现markdown文件相关的处理逻辑。

    PostProcessor.cs的完整代码在这:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Migrate/PostProcessor.cs

    构造方法:

    private readonly Post _post;
    private readonly string _importPath;
    private readonly string _assetsPath;
    
    public PostProcessor(string importPath, string assetsPath, Post post) {
        _post = post;
        _assetsPath = assetsPath;
        _importPath = importPath;
    }
    

    其中

    • Post:我们上一篇里设计的文章模型
    • importPath:要导入的markdown文件夹路径
    • assetsPath:资源文件存放路径,用于存放markdown里的图片,本项目设置的路径是StarBlog.Web/wwwroot/media/blog

    文章摘要提取

    文章摘要提取,我做了简单的处理,把markdown内容渲染成文本,然后截取前n个字形成摘要,代码如下:

    public string GetSummary(int length) {
        return _post.Content == null
            ? string.Empty
            : Markdown.ToPlainText(_post.Content).Limit(length);
    }
    

    文章状态和标题处理

    之前在用本地markdown文件写博客的时候,出于个人习惯,我会在文件名里加上代表状态的前缀,例如未完成的文章是这样的:

    (未完成)StarBlog博客开发笔记(4):markdown博客批量导入
    

    或者已完成但未发布,会加上(未发布)

    等到发布之后,就把前缀去掉,所以在导入的时候,我要用正则表达式对这个前缀进行提取,让导入数据库的博客文章标题不要再带上前缀了。

    代码如下

    public (string, string) InflateStatusTitle() {
        const string pattern = @"^((.+))(.+)$";
        var status = _post.Status ?? "已发布";
        var title = _post.Title;
    	if (string.IsNullOrEmpty(title)) return (status, $"未命名文章{_post.CreationTime.ToLongDateString()}");
        var result = Regex.Match(title, pattern);
        if (!result.Success) return (status, title);
    
        status = result.Groups[1].Value;
        title = result.Groups[2].Value;
    
        _post.Status = status;
        _post.Title = title;
    
        if (!new[] { "已发表", "已发布" }.Contains(_post.Status)) {
            _post.IsPublish = false;
        }
    
        return (status, title);
    }
    

    逻辑很简单,判断标题是否为空(对文件名来说这不太可能,不过为了严谨一点还是做了),然后用正则匹配,匹配到了就把状态提取出来,没匹配到就默认"已发布"

    图片提取 & 替换

    markdown内容处理比较复杂的就是这部分了,所以我之前就把这部分单独拿出来写了一篇文章来介绍,所以本文就不再重复太多,详情可以看我前面的这篇文章:C#解析Markdown文档,实现替换图片链接操作

    然后回到我们的博客项目,这部分的代码如下

    public string MarkdownParse() {
        if (_post.Content == null) {
            return string.Empty;
        }
    
        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;
    
                if (linkInline.Url == null) continue;
                if (linkInline.Url.StartsWith("http")) continue;
    
                // 路径处理
                var imgPath = Path.Combine(_importPath, _post.Path, linkInline.Url);
                var imgFilename = Path.GetFileName(linkInline.Url);
                var destDir = Path.Combine(_assetsPath, _post.Id);
                if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
                var destPath = Path.Combine(destDir, imgFilename);
                if (File.Exists(destPath)) {
                    // 图片重名处理
                    var imgId = GuidUtils.GuidTo16String();
                    imgFilename = $"{Path.GetFileNameWithoutExtension(imgFilename)}-{imgId}.{Path.GetExtension(imgFilename)}";
                    destPath = Path.Combine(destDir, imgFilename);
                }
    
                // 替换图片链接
                linkInline.Url = imgFilename;
                // 复制图片
                File.Copy(imgPath, destPath);
    
                Console.WriteLine($"复制 {imgPath} 到 {destPath}");
            }
        }
    
    
        using var writer = new StringWriter();
        var render = new NormalizeRenderer(writer);
        render.Render(document);
        return writer.ToString();
    }
    

    实现的步骤大概是这样:

    • 用Markdig库的markdown解析功能
    • 把所有图片链接提取出来
    • 然后根据我们前面在构造方法中传入的importPath导入目录,去拼接图片的完整路径
    • 接着把图片复制到assetsPath里面
    • 最后把markdown中的图片地址替换为重新生成的图片文件名

    小结

    目前这个方案处理大部分markdown中的图片都没问题,但是仍存在一个问题!

    图片文件名带空格时无法识别!

    这个问题算是Markdig库的一个缺陷?吧,我尝试读了一下Markdig的代码想看看能不能fix一下,很遗憾我没读懂,所以暂时没有很好的办法,只能向官方提个issues了,这个库的更新很勤快,有希望让官方来修复这个问题。

    遍历目录

    前面说了关键的部分,现在来说一下比较简单的遍历目录文件,对文件IO用得很熟练的同学请跳过这部分~

    我用的是递归的方式来实现的,参考微软官方的一篇博客:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/file-system/how-to-iterate-through-a-directory-tree

    关键代码如下,完整代码在这:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Migrate/Program.cs

    void WalkDirectoryTree(DirectoryInfo root) {
        Console.WriteLine($"正在扫描文件夹:{root.FullName}");
    
        FileInfo[]? files = null;
        DirectoryInfo[]? subDirs = null;
    
        try {
            files = root.GetFiles("*.md");
        }
        catch (UnauthorizedAccessException e) {
            Console.WriteLine(e.Message);
        }
        catch (DirectoryNotFoundException e) {
            Console.WriteLine(e.Message);
        }
    
        if (files != null) {
            foreach (var fi in files) {
                Console.WriteLine(fi.FullName);
                // 处理文章的代码,省略
            }
        }
    
        subDirs = root.GetDirectories();
    
        foreach (var dirInfo in subDirs) {
            if (exclusionDirs.Contains(dirInfo.Name)) {
                continue;
            }
    
            if (dirInfo.Name.EndsWith(".assets")) {
                continue;
            }
    
            WalkDirectoryTree(dirInfo);
        }
    }
    

    用的这个方法叫做“前序遍历”,即先处理目录下的文件,然后再处理目录下的子目录。

    递归的方法写起来比较简单,但是有一个缺陷是如果目录结构嵌套太多的话,可能会堆栈溢出,可以考虑换用基于Stack<T>模式的遍历,不过作为博客的目录层级结构应该不会太多,所以我只用简单的~

    写入数据库

    本项目用到的ORM是FreeSQL,ORM操作在后续的网站开发中会有比较多的介绍,因此本文略过,文章数据写入数据库的代码很简单,可以直接看:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Migrate/Program.cs

    结束

    OK,博客批量导入就介绍了这么多,几个麻烦的地方处理好之后也没啥难度了,有了文章数据之后,才能方便接下来开始开发博客网站~

    大概就这些了,下篇文章见~

    同时所有项目代码已经上传GitHub,欢迎各位大佬Star/Fork!


    __EOF__

  • 本文作者: 程序设计实验室
  • 本文链接: https://www.cnblogs.com/deali/p/16211720.html
  • 关于博主: 公众号:程序设计实验室,欢迎交流~
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    c# Json转C#实体
    ARM pwn 入门 (4)
    小白看得懂的 Transformer (图解)
    GBase 8s静默安装
    Report Wizard(报表大师/Мастер отчетов)
    Emacs有什么优点,用Emacs写程序真的比IDE更方便吗?
    数据结构初阶之栈和队列--C语言实现
    数据结构与算法--并查集结构
    面试10000次依然会问的【线程池】,你还不会?
    “夭折“的AirPower将复活?iPhone开启长距离无线充电
  • 原文地址:https://www.cnblogs.com/deali/p/16211720.html