基於.NetCore開發部落格專案 StarBlog - (4) markdown部落格批量匯入

程式設計實驗室發表於2022-04-30

系列文章

前言

上週介紹了部落格的模型設計,現在模型設計好了,要開始匯入資料了。

我們要把一個資料夾內的所有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!

相關文章