目的 因為某些原因需要將存放在 Google Chrome 內的書簽導出到本地,所幸 Google Chrome 提供了導出書簽的功能。 分析 首先在 Google Chrome 瀏覽器當中輸入 來到書簽管理頁面,找到最右側的三個點,選擇導出書簽,導出的文件是一個 HTML 文件,裡面包含了所有書簽 ...
目的
因為某些原因需要將存放在 Google Chrome 內的書簽導出到本地,所幸 Google Chrome 提供了導出書簽的功能。
分析
首先在 Google Chrome 瀏覽器當中輸入 chrome://bookmarks
來到書簽管理頁面,找到最右側的三個點,選擇導出書簽,導出的文件是一個 HTML 文件,裡面包含了所有書簽的層級結構等信息。
使用 Notepad++ 打開該文件之後可以看到裡面的內容如下:
粗略一看貌似沒什麼問題,其實在裡面的 <DT>
與 <P>
都缺少了閉合標簽,所以在解析的時候需要將其去除掉。去除掉之後的 HTML 文件結構大概像這樣:
<DL>
<H3>文件夾標題</H3>
<DL>
<H3>子文件夾標題</H3>
<A HREF="書簽地址">子文件夾書簽1</A>
<A HREF="書簽地址">子文件夾書簽2</A>
</DL>
<A HREF="書簽地址">書簽1</A>
<A HREF="書簽地址">書簽2</A>
</DL>
可以很明顯看到這裡是有一個層級關係的,所以我們可以通過遞歸來生成一個樹形模型,生成之後,再遍歷這個模型來根據這個樹形結構來創建 MHTML 文件,並且進行歸類。
實現
操作 HTML 文件在 .Net 下有一個很方便的第三方庫,名字叫做 HtmlAgilityPack
,通過這個庫我們可以很方便地操作 HTML 文檔,就跟 DOM 一樣方便,而且它支持 XPath 選取。
項目地址:http://html-agility-pack.net/
GitHub 地址:https://github.com/zzzprojects/html-agility-pack
Nuget 地址:https://www.nuget.org/packages/HtmlAgilityPack/
通過 Nuget 安裝該包到項目當中,引入 HtmlAgilityPack
命名空間,就可以開始編寫代碼了。
1.編寫 HtmlResolver 解析器
建立一個 HtmlResolver 類,該類用於解析 Chrome 導出的書簽:
public class HtmlResolver
{
private HtmlDocument _htmlDocument = new HtmlDocument();
/// <summary>
/// 初始化 HTML 解析器
/// </summary>
/// <param name="htmlPath">Google Chrome 導出的書簽 HTML 路徑</param>
public HtmlResolver(string htmlPath)
{
using (FileStream htmlFileStream = File.Open(htmlPath, FileMode.Open))
{
using (StreamReader htmlReader = new StreamReader(htmlFileStream))
{
// 移除干擾標簽
string htmlStr = htmlReader.ReadToEnd();
htmlStr = htmlStr.Replace(@"<DT>", string.Empty).Replace(@"<p>", string.Empty);
// 載入 HTML
_htmlDocument.LoadHtml(htmlStr);
}
}
}
}
在對象初始化的時候要求提供 Google Chrome 導出的書簽 HTML 文件路徑,並且讀入 HTML 文件數據的時候移除掉之前所說的 <DT>
與 <P>
標簽,方便後面 HtmlAgilityPack
進行解析,移除之後,HtmlDocument
通過 HTML String 初始化。
2.創建書簽模型
當我們遞歸完成之後需要將數據存儲在書簽模型當中,方便後面生成 MHTML 文件的時候使用。
/// <summary>
/// 書簽模型
/// </summary>
public class BookMarkModel
{
/// <summary>
/// 初始化書簽模型
/// </summary>
/// <param name="name">書簽名稱</param>
/// <param name="path">書簽路徑</param>
/// <param name="url">綁定的 URL</param>
/// <param name="childNodes">子節點集合</param>
public BookMarkModel(string name, string path, string url = null, List<BookMarkModel> childNodes = null)
{
Name = name;
Url = url;
ChildNodes = childNodes;
Path = path;
}
/// <summary>
/// 書簽名稱
/// </summary>
public string Name { get; set; }
/// <summary>
/// 綁定的 URL
/// </summary>
public string Url { get; set; }
/// <summary>
/// 書簽路徑
/// </summary>
public string Path { get; set; }
/// <summary>
/// 子節點集合,如果沒有則為 NULL
/// </summary>
public List<BookMarkModel> ChildNodes { get; set; }
}
該模型是一個典型的樹形結構,之後我們就開始遞歸生成書簽模型了。
3.遞歸生成樹形模型
遞歸演算法自己一直不太會寫,寫好這一個遞歸方法基本都花費了半天的時間 :P,後面打算惡補數學和演算法這塊了。下麵先上代碼再解釋原理:
/// <summary>
/// 遞歸生成書簽模型
/// </summary>
/// <param name="node">父級節點</param>
/// <param name="parentPath">父級節點 Path</param>
private List<BookMarkModel> RecursionGenerate(HtmlNode node, string parentPath)
{
List<BookMarkModel> bookMarkModels = new List<BookMarkModel>();
// 獲取所有文件夾標題與其下屬節點,以便遞歸查詢其子節點
var bookMarkFolderTitles = node.SelectNodes("h3")?.Cast<HtmlNode>().ToList();
var bookMarkFolder = node.SelectNodes("dl")?.Cast<HtmlNode>().ToList();
var htmlBookMarks = node.SelectNodes("a");
// 如果文件夾不存在則直接將所有具體書簽返回
if (bookMarkFolderTitles == null || bookMarkFolder == null)
{
return GenerateBookMarkModels(htmlBookMarks, parentPath);
}
// 遞歸構建書簽模型
for (int i = 0; i < bookMarkFolderTitles.Count; i++)
{
BookMarkModel bookMark = new BookMarkModel(bookMarkFolderTitles[i].InnerText, $@"{parentPath}\{bookMarkFolderTitles[i].InnerText}.mhtml");
bookMark.ChildNodes = RecursionGenerate(bookMarkFolder[i], bookMark.Path);
List<BookMarkModel> bookmarks = GenerateBookMarkModels(htmlBookMarks, parentPath);
if (bookmarks != null) bookMark.ChildNodes?.AddRange(bookmarks);
bookMarkModels.Add(bookMark);
}
return bookMarkModels;
}
首先說說 RecursionGenerate(HtmlNode node,string parentPath)
方法,這個方法接收一個節點參數,這個節點就是需要遍歷的節點,而 parentPath
則是用於生成路徑的,在每次構建書簽模型的時候都會根據父級路徑來生成新的路徑。
如果要獲取某個節點下麵的子節點,肯定要拿到該節點下屬的所有節點,可以參考上面的大概結構,一般一個書簽文件夾下麵都會有一個或多個子文件夾,也有可能會有部分書簽與這些子文件夾同級。
所以,我們先提取出當前節點的文件夾名稱,也就是 <H3>
標簽裡面的內容,然後只要有一個 <H3>
標簽,那他肯定有一個對應的 <DL>
標簽表示包裹著它的子節點內容。如果某個節點它的內部沒有子文件夾的話,那直接抓取其內部的具體書簽,並返回出來。
如果某個節點擁有子文件夾的話,遍歷其內部,並且再次調用 RecursionGenerate
方法,將其內部節點添加到這個節點的 Childern 當中。
註意,這裡在迴圈內部還再次進行了獲取具體書簽的操作,因為有的時候某個節點內部也是擁有具體書簽項的,所以這裡才會有 List<T>.AddRange(IEnumerable<T> list)
操作。
具體書簽生成:
/// <summary>
/// 將 A 標簽的集合轉換為 BookMarkModel 集合
/// </summary>
/// <param name="nodes">A 標簽節點集合</param>
/// <returns>轉換完成的 Node 集合</returns>
private List<BookMarkModel> GenerateBookMarkModels(HtmlNodeCollection nodes, string parentPath)
{
if (nodes == null) return null;
List<BookMarkModel> bookmarks = new List<BookMarkModel>();
foreach (var node in nodes)
{
bookmarks.Add(new BookMarkModel(node.InnerText, $@"{parentPath}\{node.InnerText}.mhtml", node.Attributes["href"].Value));
}
return bookmarks;
}
具體書簽的生成就很簡單了,直接構建即可,這裡會在其末尾添加 .mhtml 尾碼。
4.根據生成的書簽模型來產生 MHTML 文件
這裡可以參考以下實現:
https://code.msdn.microsoft.com/windowsdesktop/Creating-a-MHTML-MIME-HTML-61cf5dd1
目前程式還沒有實現這一個功能,因為使用 CDO 的方法不太方便,而且生成的 MHT 文件樣式丟失嚴重,並不像 Google Chrome 保存的 mht 文件那樣完整。
後續再來填坑。
結尾
項目地址:http://git.myzony.com/Zony/GoogleBookmarkExportTool