Chrome书签是Google Chrome浏览器中的一项功能,它允许用户保存他们经常访问或感兴趣的网页链接。用户可以将这些网页链接存储在书签栏、其他文件夹或使用关键词进行搜索。
以下是一个典型的Chrome书签HTML文件的示例:
<!DOCTYPE NETSCAPE-Bookmark-file-1> <!-- This is an automatically generated file. It will be read and overwritten. DO NOT EDIT! --> <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> <TITLE>Bookmarks</TITLE> <H1>Bookmarks</H1> <DL><p> <DT><H3 ADD_DATE="1700000000" LAST_MODIFIED="1700000000" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Bar</H3> <DL><p> <DT><A HREF="https://www.google.com" ADD_DATE="1700000001">Google</A> <DT><A HREF="https://www.example.com" ADD_DATE="1700000002">Example</A> <DT><H3 ADD_DATE="1700000003" LAST_MODIFIED="1700000003">Folder</H3> <DL><p> <DT><A HREF="https://www.example.org" ADD_DATE="1700000004">Example Org</A> </DL><p> </DL><p> </DL><p>
文档头部
<!DOCTYPE NETSCAPE-Bookmark-file-1>
:说明这是一个兼容Netscape的书签文件格式,许多浏览器都使用这种格式。<META>
标签:定义字符编码(一般是 UTF-8
)。<TITLE>
和 <H1>
:文档标题,通常是 "Bookmarks"。根层级 <DL>
标签
<DL>
:定义了书签目录的层级(类似于HTML的有序列表)。<DT>
:定义每个项目或文件夹。书签项 <A>
标签
HREF
:书签链接的目标网址。ADD_DATE
:书签添加的时间戳(Unix时间戳格式)。LAST_MODIFIED
:书签最后修改的时间戳。文件夹 <H3>
标签
<H3>
:文件夹名称。PERSONAL_TOOLBAR_FOLDER
:如果存在该属性,表示这是浏览器工具栏上的书签文件夹。嵌套 <DL>
<DL>
,用来存储该文件夹下的书签或子文件夹。数据结构可以根据自身的业务进行调整。
const bookmarksdata = [
{
key:'1',
label:'类目A',
children:[
{
menuName: '谷歌',
menuIcon: '', // Chrome 书签文件中并无图标信息
menuUrl: 'https://google.com',
menuCode: 'xx'
},
{
menuName: 'baidu',
menuIcon: '', // Chrome 书签文件中并无图标信息
menuUrl: 'https://baidu.com',
menuCode: 'xx'
}
]
},
{
key:'2',
label:'类目B',
children:[
{
menuName: '网址名称',
menuIcon: '', // Chrome 书签文件中并无图标信息
menuUrl: 'https://a.com',
menuCode: ''
}
]
}
]
生成函数:
// 生成 Chrome 书签 HTML 格式 function generateBookmarkHTML(bookmarks) { let html = `<!DOCTYPE NETSCAPE-Bookmark-file-1> <!-- This is an automatically generated file. It will be read and overwritten. DO NOT EDIT! --> <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> <TITLE>Bookmarks</TITLE> <H1>Bookmarks</H1> <DL><p>`; bookmarks.forEach(group => { html += ` <DT><H3>${group.label}</H3> <DL><p>`; group.children.forEach(item => { html += ` <DT><A HREF="${item.menuUrl}" ADD_DATE="${Date.now()}">${item.menuName}</A>`; }); html += ` </DL><p>`; }); html += ` </DL><p>`; return html; } // 导出为 HTML 文件 export function exportBookmarks(bookmarksData = []) { const bookmarkHTML = generateBookmarkHTML(bookmarksData); const blob = new Blob([bookmarkHTML], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bookmarks.html'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }
处理带有属性PERSONAL_TOOLBAR_FOLDER="true"
的H3标签进行忽略,判断书签栏的类目是网址还是单个网址,然后递归处理,生成想要的数据结构。
/** * 解析书签 html 文件 * @param {*} file * @returns */ function parseBookmarksHTML(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function (event) { try { const htmlContent = event.target.result; const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, 'text/html'); // 找到最外层 DL const rootDL = doc.querySelector('DL'); if (!rootDL) { throw new Error('未找到 <DL>,文件可能不是标准 Chrome 书签!'); } // 解析获得所有文件夹,并顺带收集"无文件夹"链接 const { noFolderLinks, folderList } = parseRootDL(rootDL); // 把"无文件夹"链接打包进一个文件夹结构 const result = []; if (noFolderLinks.length > 0) { result.push({ key: generateUUID(), label: '未分类', children: noFolderLinks, }); } // 把真正的文件夹们也放到最外层数组 result.push(...folderList); resolve(result); } catch (error) { reject(error); } }; reader.onerror = () => reject(new Error('文件读取失败')); reader.readAsText(file); }); } /** * 解析最外层 <DL>,把顶层没有文件夹的 <A> 放进 noFolderLinks, * 顶层有文件夹的 <H3> + <DL> 放进 folderList * * @param {HTMLElement} dlElement * @returns {{ noFolderLinks: Array, folderList: Array }} */ function parseRootDL(dlElement) { const dtElements = dlElement.querySelectorAll(':scope > DT'); const noFolderLinks = []; const folderList = []; dtElements.forEach((dt) => { const h3 = dt.querySelector(':scope > H3'); const aTag = dt.querySelector(':scope > A'); if (h3) { // 如果 h3 带有 PERSONAL_TOOLBAR_FOLDER="true" 就跳过"自己",但要继续递归解析它的子 <DL> if (h3.getAttribute('PERSONAL_TOOLBAR_FOLDER') === 'true') { const childDL = dt.querySelector(':scope > DL'); if (childDL) { // 子DL 里会返回一个数组,既可能含子文件夹({ key, label, children }) 也可能是书签({ menuName, ... }) const childItems = parseBookmarkDL(childDL); // 我们需要把这些子items分拆到 noFolderLinks 或 folderList // parseBookmarkDL 里返回的结构是: // - 如果是文件夹: { key, label, children } // - 如果是书签: { menuName, menuUrl, ... } childItems.forEach((item) => { if (item.menuUrl) { // 这是个链接 noFolderLinks.push(item); } else { // 这是个文件夹 folderList.push(item); } }); } // 然后跳过"自己"(不 push 任何东西) return; } // --- 如果不是 PERSONAL_TOOLBAR_FOLDER="true",正常处理文件夹 --- const folderName = h3.innerText.trim() || '未命名文件夹'; const childDL = dt.querySelector(':scope > DL'); folderList.push({ key: generateUUID(), label: folderName, children: childDL ? parseBookmarkDL(childDL) : [], }); } else if (aTag) { // 顶层直接的链接 noFolderLinks.push({ menuName: aTag.innerText.trim() || '未命名书签', menuIcon: '', menuUrl: aTag.href || '', menuCode: generateUUID(), }); } }); return { noFolderLinks, folderList }; } /** * 递归解析子文件夹/链接 * 这里和之前差不多,只是用于处理子文件夹 * * @param {HTMLElement} dlElement * @returns {Array} - 返回子层级的数组(文件夹、链接混合) */ function parseBookmarkDL(dlElement) { const results = []; const dtElements = dlElement.querySelectorAll(':scope > DT'); dtElements.forEach((dt) => { const h3 = dt.querySelector(':scope > H3'); const aTag = dt.querySelector(':scope > A'); if (h3) { // 如果是 "PERSONAL_TOOLBAR_FOLDER" 那就跳过"自己",把子DL 继续解析合并到当前 results if (h3.getAttribute('PERSONAL_TOOLBAR_FOLDER') === 'true') { const childDL = dt.querySelector(':scope > DL'); if (childDL) { const childItems = parseBookmarkDL(childDL); childItems.forEach((item) => results.push(item)); } return; } // 正常子文件夹处理 const folderName = h3.innerText.trim() || '未命名文件夹'; const childDL = dt.querySelector(':scope > DL'); results.push({ key: generateUUID(), label: folderName, children: childDL ? parseBookmarkDL(childDL) : [] }); } else if (aTag) { // 书签 const linkName = aTag.innerText.trim() || '未命名书签'; const linkUrl = aTag.href || ''; const iconData = aTag.getAttribute('ICON') || ''; // 如果取不到,默认空字符串 results.push({ menuName: linkName, menuIcon: iconData, menuUrl: linkUrl, menuCode: generateUUID() }); } }); return results; } /** 简易 UUID 生成 */ function generateUUID() { return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // 处理文件上传 async function handleFileUpload(event) { return new Promise(async (resolve, reject) => { try { const file = event.target.files[0]; if (!file) { throw new Error('请选择一个书签 HTML 文件'); } // 检查文件类型 if (!file.name.toLowerCase().endsWith('.html')) { throw new Error('请选择正确的 HTML 格式文件'); } // 检查文件大小(例如限制为 10MB) if (file.size > 10 * 1024 * 1024) { throw new Error('文件大小超过限制(最大10MB)'); } const bookmarks = await parseBookmarksHTML(file); resolve(bookmarks); } catch (error) { reject(error); } }); } // 创建文件上传按钮 export function importBookmarks(cb) { const uploadInput = document.createElement('input'); uploadInput.type = 'file'; uploadInput.accept = '.html'; uploadInput.addEventListener('change', async (e) => { try { const data = await handleFileUpload(e); if (cb) { cb(data); } else { console.warn('未提供回调函数处理导入结果'); } } catch (error) { console.error('导入书签失败:', error.message); // 这里可以添加UI提示,比如使用 alert 或其他提示组件 alert(error.message); } finally { uploadInput.remove(); } }); uploadInput.click(); }
使用方法,直接调用importBookmarks函数,并传入回调函数即可。
Chrome 浏览器和 Edge浏览器名称有所不同,Chrome书签叫书签栏,Edge浏览器叫收藏夹,区别在于导出的这一行:
<DT><H3 ADD_DATE="1700000000" LAST_MODIFIED="1700000000" PERSONAL_TOOLBAR_FOLDER="true">书签栏</H3>
<DT><H3 ADD_DATE="1700000000" LAST_MODIFIED="1700000000" PERSONAL_TOOLBAR_FOLDER="true">收藏夹栏</H3>
上面的generateBookmarkHTML 函数并没有加入这一行,如果加入这行,当把书签文件导入到 Chrome时会在根目录多生成两个文件夹来嵌套生成的文件夹: 已导入->书签栏,在Edge浏览器中则导入正常,如果有已存在的文件夹则进行合并数据,并不会在书签根级多生成一个文件夹。