系列文章
- 基于.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頁面動态編譯
- ...
前言
上周介紹了部落格的模型設計,現在模型設計好了,要開始導入資料了。
我們要把一個檔案夾内的所有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
-
:要導入的markdown檔案夾路徑importPath
-
:資源檔案存放路徑,用于存放markdown裡的圖檔,本項目設定的路徑是assetsPath
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!
- 部落格後端+前台項目位址:https://github.com/Deali-Axy/StarBlog
- 管理背景前端項目位址:https://github.com/Deali-Axy/StarBlog-Admin
微信公衆号:「程式設計實驗室」
專注于網際網路熱門新技術探索與團隊靈活開發實踐,包括架構設計、機器學習與資料分析算法、移動端開發、Linux、Web前後端開發等,歡迎一起探讨技術,分享學習實踐經驗。