Google Chrome 書簽導出並生成 MHTML 文件


目的

因為某些原因需要將存放在 Google Chrome 內的書簽導出到本地,所幸 Google Chrome 提供了導出書簽的功能。

分析

首先在 Google Chrome 瀏覽器當中輸入 chrome://bookmarks 來到書簽管理頁面,找到最右側的三個點,選擇導出書簽,導出的文件是一個 HTML 文件,里面包含了所有書簽的層級結構等信息。
使用 Notepad++ 打開該文件之后可以看到里面的內容如下:
https://i.loli.net/2018/06/24/5b2eb28ad92da.png
粗略一看貌似沒什么問題,其實在里面的 <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.遞歸生成樹形模型

遞歸算法自己一直不太會寫,寫好這一個遞歸方法基本都花費了半天的時間 😛,后面打算惡補數學和算法這塊了。下面先上代碼再解釋原理:

/// <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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM