[C#]Google Chrome 书签导出并生成 MHTML 文件
目的
因为某些原因需要将存放在 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 文件那样完整。
后续再来填坑。