浅析浏览器书签的导入和导出

浏览器有个实用的功能,但是可能用的频率不高,就是书签/收藏的导入和导出,因为现在一般浏览器都有云同步功能,所以这个功能存在感不强。

浏览器书签是可以跨不同的浏览器导入的,所以意味着导出的文件肯定是有一个规范的,我简单搜了一下没有搜到,可能是各家约定俗成的规范,并没有一个正式的标准。

通用的数据交换格式有很多,比如xml、json、yaml,json应该是使用最广泛的,因为易于解析和存储,尺寸也不大,所以很适合浏览器书签的导出,但是,实际上现代浏览器导出的书签文件是html文件。原因不详,也没有搜到相关信息,我猜测原因可能是html文件相对于json来说,普通用户更为熟悉,其次,html文件可以直接使用浏览器打开,当然,json文件也可以使用浏览器打开,但是可能直接点击的时候默认是用文本编辑器打开的,另外它们在浏览器的呈现方式也不一样,html显示的是一个普通的带有一堆超链接的页面,就是一个有点丑的网页,而json打开有点类似源码,不太友好,因为一般用户导出书签就是为了在另一个浏览器导入,所以屏蔽细节并没有什么问题。

html和xml是类似的,所以解析和传输也很简单,接下来看一下实例:

基本结构如上,每个文件夹下都有个书签,导出的书签源码如下:



简单分析一下:

1.标签字母都是大写

2.DOCTYPE声明和普通HTML页面不同

3.使用DL和DT来组织书签,DL代表一个文件夹的内容列表,DT代表一个内容,可能是书签也可能是文件夹,文件夹的话会有一个H3标签来表示书签的名字,书签的话就是直接跟一个A标签,DL标签后都跟了一个小写的p标签,有部分标签没有闭合

4.H1标签之前的都和书签内容没有什么关系

5.文件夹名称H3标签和超链接A标签都有ADD_DATELAST_MODIFIED来保存时间信息,该属性不存在也不影响

6.文件夹名称H3标签的属性PERSONAL_TOOLBAR_FOLDER来表示该文件夹下的内容是否显示到浏览器的工具栏,否则会默认放到浏览器的其他文件夹里,但也不一定,有的浏览器会有自己的行为

7.网页的标题icon会转换为base64格式放到ICON属性上,这个属性不存在也不影响

html其实就是普通字符串,所以可以手动生成,常见于一些导航网站和网址收藏工具的导出功能,如五花八门导航(http://lxqnsys.com/d),有一个需要注意的地方,就是html字符串必须格式化带换行和缩进,下图这种压缩过的是不行的:

生成方式也很简单,书签是树结构,所以递归循环拼接即可。

先看一下书签数据的格式,忽略时间和icon:

let bookmarks = [
    {
        name: '',// 文件夹或书签名字
        toolbar: true,// 是否显示到工具栏
        folder: true,// 是否是文件夹
        children: [
            {
                name: '',
                folder: true,
                children: []
            },
            {
                name: '',// 书签名称
                url: ''// 书签url
            }
        ]
    }
]

使用ES6的话可以直接使用模板字符串``来带换行的拼接,很方便:

function createBookmarksStr (bookmarks) {
    let str = `
        <!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>
    `
    let loop = (root) => {
        let str = ''
        root.forEach((item) => {
            if (item.folder) {
                str += `
                    <DT>
                        <H3 ${item.toolbar ? `PERSONAL_TOOLBAR_FOLDER="true"` : ''}>${item.name}</H3>
                        <DL>
                            <p>
                `
                str += loop(item.children)
                str += `
                    </DL>
                    <p>
                `
            } else {
                str += `
                    <DT><A HREF="${item.url}">${item.name}</A>
                `
            }
        })
        return str
    }
    str += loop(bookmarks)
    str += `
        </DL>
        <p>
    `
    return str
}

ES6之前的就需要显式的拼接上换行符:

function createBookmarksStr (bookmarks) {
    var str = '<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<!-- This is an automatically generated file.It will be read and overwritten.DO NOT EDIT! -->\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL>\n\t<p>\n\t\t<DT>\n\t\t\t<H3 ADD_DATE=\"1568796074\" LAST_MODIFIED=\"1601707819\" PERSONAL_TOOLBAR_FOLDER=\"true\">\u4E66\u7B7E\u680F</H3>\n\t\t\t<DL>\n\t\t\t\t<p>\n\t\t\t\t\t'
    var loop = function (root) {
        var str = ''
        root.forEach(function (item) {
            if (item.folder) {
                str += '<DT>\n\t\t\t\t\t\t<H3 '+ (item.toolbar ? 'PERSONAL_TOOLBAR_FOLDER="true"' : '') +'>'+item.name+'</H3>\n\t\t\t\t\t\t<DL>\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t'
                str += loop(item.children)
                str += '</DL>\n\t\t\t\t\t\t<p>\n\t\t\t'
            } else {
                str += '<DT><A HREF=\"'+item.url+'\">'+item.name+'</A>\n\t\t\t\t\t\t\t\t'
            }
        })
        return str
    }
    str += loop(bookmarks)
    str += '</DL>\n\t\t\t<p>\n</DL>\n<p>'
    return str
}

看完了如何生成,接下来看一下如何解析,解析和拼接类似,也是通过深度优先进行遍历,只是会有一些特征判断。字符串如何转化成一棵树,最简单的肯定是先转换为DOM元素,然后再通过操作DOM的api来进行遍历,有一些库可以用来做这件事,不过这里直接用的是iframe

function getBookmarksStrRootNode (str) {
    // 创建iframe
    let iframe = document.createElement('iframe')
    document.body.appendChild(iframe)
    iframe.style.display = 'none'
    // 添加书签dom字符串
    iframe.contentWindow.document.documentElement.innerHTML = str
    // 获取书签树根节点
    return iframe.contentWindow.document.querySelector('dl')
}
function analysisBookmarksStr(str) {
    let root = getBookmarksStrRootNode(str)
}

看一下转换的结果:

书签DOM字符串:

转换后的DOM节点:

获取到书签树的根节点,接下来递归遍历即可:

function walkBookmarksTree (root) {
    let result = []
    // 深度优先遍历
    let walk = (node, list) => {
        let els = node.children
        if (els && els.length > 0) {
            for (let i = 0; i < els.length; i++) {
                let item = els[i]
                // p标签或h3标签直接跳过
                if (item.tagName === 'P' || item.tagName === 'H3') {
                    continue
                }
                // 文件夹不用创建元素
                if (item.tagName === 'DL') {
                    walk(els[i], list)
                } else {// DT节点
                    let child = null
                    // 判断是否是文件夹
                    let children = item.children
                    let isDir = false
                    for(let j = 0; j < children.length; j++) {
                        if (children[j].tagName === 'H3' || children[j].tagName === 'DL') {
                            isDir = true
                        }
                    }
                    // 文件夹
                    if (isDir) {
                        child = {
                            name: item.tagName === 'DT' ? item.querySelector('h3') ? item.querySelector('h3').innerText : '' : '',
                            folder: true,
                            children: []
                        }
                        walk(els[i], child.children)
                    } else {// 书签
                        let _item = item.querySelector('a')
                        child = {
                            name: _item.innerText,
                            url: _item.href
                        }
                    }
                    list.push(child)
                }
            }
        }
    }
    walk(root, result)
    return result
}
function analysisBookmarksStr(str) {
    let root = getBookmarksStrRootNode(str)
    let result = walkBookmarksTree(root)
}

最后解析的结果:

搞定收工,赶紧去试试吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容