AI书签管理工具开发全记录(十八):书签导入导出
文章目录
- AI书签管理工具开发全记录(十八):书签导入导出
- 1.前言 📝
- 2.书签结构分析 📖
- 3.书签示例 📑
- 4.书签文件结构定义描述 🔣
- 4.1. 整体文档结构
- 4.2. 核心元素类型
- 4.3. 层级关系
- 4.4. 嵌套规则
- 4.5. 属性规范
- 4.6. 特殊结构
- 4.7. 文档终止
- 5. 导出 🚀
- 5.1 定义导出方法
- 5.2 前端调用
- 6. 导入 📥
- 6.1 添加书签处理方法
- 6.2 添加后端处理方法
AI书签管理工具开发全记录(十八):书签导入导出
1.前言 📝
在上一篇文章中,我们完成了sun-panel同步功能的实现。本篇文章将聚焦于书签的导入导出功能。
2.书签结构分析 📖
这里我们以chrome书签格式作为参考标准。
我们可以在chrome://bookmarks/
中点击导出书签
3.书签示例 📑
4.书签文件结构定义描述 🔣
4.1. 整体文档结构
- 文档类型声明:
<!DOCTYPE NETSCAPE-Bookmark-file-1>
- 元数据:
- 注释说明
- 字符集声明:
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
- 标题:
<TITLE>Bookmarks</TITLE>
- 主容器:所有书签内容包裹在顶级
<DL><p>
标签中
4.2. 核心元素类型
-
文件夹 (Folder):
- 定义标签:
<DT><H3>
- 关键属性:
ADD_DATE
:创建时间戳(Unix时间戳)LAST_MODIFIED
:最后修改时间戳(可选)PERSONAL_TOOLBAR_FOLDER
:标记是否为书签栏(布尔值,可选)
- 子容器:后跟
<DL><p>
包裹下级内容
- 定义标签:
-
书签项 (Bookmark):
- 定义标签:
<DT><A>
- 关键属性:
HREF
:URL地址(必需)ADD_DATE
:添加时间戳(Unix时间戳,必需)
- 定义标签:
4.3. 层级关系
<DL><p> <!-- 容器开始 --><DT><H3 属性>文件夹名称</H3> <!-- 文件夹定义 --><DL><p> <!-- 文件夹子容器开始 --><DT><A 属性>书签名</A> <!-- 书签项 --><DT><H3 属性>子文件夹</H3> <!-- 嵌套文件夹 --><DL><p>...</DL><p> <!-- 嵌套文件夹子容器 --></DL><p> <!-- 文件夹子容器结束 -->
</DL><p> <!-- 容器结束 -->
4.4. 嵌套规则
- 任意层级可包含混合内容(文件夹 + 书签项)
- 文件夹必须包含子容器
<DL><p>
(即使为空) - 最大嵌套深度无限制(支持无限层级)
- 同级元素按顺序排列,不依赖特殊分组标签
4.5. 属性规范
- 时间戳格式:所有时间戳均为 Unix 时间戳(10位整数)
- 保留属性:
- 文件夹:
ADD_DATE
,LAST_MODIFIED
,PERSONAL_TOOLBAR_FOLDER
- 书签项:
HREF
,ADD_DATE
- 文件夹:
4.6. 特殊结构
- 空文件夹:包含完整的
<DT><H3>
和空<DL><p>
标签对 - 根级混合内容:顶级容器可直接包含书签项(不强制要求文件夹包裹)
- 文本节点:仅作为元素内容(如书签名/文件夹名),无独立标签
4.7. 文档终止
- 以闭合标签
</DL><p>
结束所有层级 - 无额外文档结构结束标签(如
</HTML>
)
5. 导出 🚀
5.1 定义导出方法
// internal/api/api.go// ExportBookmarks godoc
// @Summary 导出书签
// @Description 导出书签为HTML格式
// @Tags bookmarks
// @Accept json
// @Produce text/html
// @Param request body models.ExportBookmarkRequest true "导出参数"
// @Success 200 {string} string "HTML格式的书签"
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/bookmarks/export [post]
func (s *Server) exportBookmarks(c *gin.Context) {var req models.ExportBookmarkRequestif err := c.ShouldBindJSON(&req); err != nil {c.JSON(400, gin.H{"error": "无效的请求数据"})return}// 构建查询query := s.db.Model(&models.Bookmark{}).Preload("Category")// 按分类过滤if req.CategoryID != 0 {query = query.Where("category_id = ?", req.CategoryID)}// 关键词搜索if req.Keyword != "" {query = query.Where("title LIKE ? OR url LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")}// 获取书签var bookmarks []models.Bookmarkif err := query.Find(&bookmarks).Error; err != nil {c.JSON(500, gin.H{"error": "获取书签失败"})return}// 生成Chrome书签HTMLnow := time.Now().Unix()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>AI书签</H3><DL><p>`// 按分类分组书签categoryBookmarks := make(map[string][]models.Bookmark)for _, b := range bookmarks {categoryName := b.Category.Nameif categoryName == "" {categoryName = "未分类"}categoryBookmarks[categoryName] = append(categoryBookmarks[categoryName], b)}// 生成HTMLfor category, bookmarks := range categoryBookmarks {html += fmt.Sprintf("\n <DT><H3>%s</H3>\n <DL><p>", category)for _, b := range bookmarks {html += fmt.Sprintf("\n <DT><A HREF=\"%s\" ADD_DATE=\"%d\">%s</A>", b.URL, now, b.Title)if b.Description != "" {html += fmt.Sprintf("\n <DD>%s</DD>", b.Description)}}html += "\n </DL><p>"}html += "\n </DL><p>\n</DL><p>"// 设置响应头c.Header("Content-Type", "text/html; charset=utf-8")c.Header("Content-Disposition", "attachment; filename=bookmarks.html")c.String(200, html)
}
5.2 前端调用
添加导出按钮
<el-button type="success" @click="handleExport">导出书签</el-button>
定义导出方法
// 导出书签
const handleExport = async () => {try {loading.value = trueconst response = await exportBookmarks({category_id: selectedCategory.value || undefined,keyword: searchKeyword.value || undefined})// 生成带时间戳的文件名const now = new Date()const timestamp = now.getFullYear() +String(now.getMonth() + 1).padStart(2, '0') +String(now.getDate()).padStart(2, '0') +String(now.getHours()).padStart(2, '0') +String(now.getMinutes()).padStart(2, '0')const filename = `bookmarks_${timestamp}.html`// 创建下载链接const url = window.URL.createObjectURL(new Blob([response]))const link = document.createElement('a')link.href = urllink.download = filenamedocument.body.appendChild(link)link.click()document.body.removeChild(link)window.URL.revokeObjectURL(url)ElMessage.success('书签导出成功')} catch (error) {console.error('导出书签失败:', error)ElMessage.error('导出书签失败')} finally {loading.value = false}
}
6. 导入 📥
6.1 添加书签处理方法
添加书签处理方法,处理书签节点。
// 处理文件选择
const handleFileChange = async (file) => {if (!file) returntry {// 使用FileReader读取文件内容const content = await new Promise((resolve, reject) => {const reader = new FileReader()reader.onload = (e) => resolve(e.target.result)reader.onerror = (e) => reject(e)reader.readAsText(file.raw)})console.log('文件内容:', content)// 创建DOM解析器const parser = new DOMParser()const doc = parser.parseFromString(content, 'text/html')console.log('解析后的DOM:', doc)const bookmarks = []let currentCategory = '未分类'// 递归处理节点const processNode = (node) => {if (!node) return// 处理H3标签(分类)if (node.tagName === 'H3') {currentCategory = node.textContent.trim()console.log('找到分类:', currentCategory)}// 处理A标签(书签)if (node.tagName === 'A') {const bookmark = {title: node.textContent.trim(),url: node.getAttribute('href'),category: currentCategory,description: '',icon: node.getAttribute('icon') || ''}console.log('创建书签:', bookmark)bookmarks.push(bookmark)}// 递归处理子节点for (const child of node.children) {processNode(child)}}// 从根节点开始处理const rootDL = doc.querySelector('DL')if (rootDL) {processNode(rootDL)}console.log('解析完成,书签数量:', bookmarks.length)// 调用导入APIconst response = await importBookmarks({ bookmarks })console.log('导入响应:', response)if (response.stats) {ElMessage.success(`导入完成:成功 ${response.stats.success} 个,重复 ${response.stats.duplicate} 个,失败 ${response.stats.failed} 个`)// 刷新列表fetchBookmarks()} else {ElMessage.error(response.msg || '导入失败')}} catch (error) {console.error('解析文件失败:', error)ElMessage.error('解析文件失败')}
}
6.2 添加后端处理方法
不存在的分类则自动创建分类。
// ImportBookmarks godoc
// @Summary 导入书签
// @Description 批量导入书签
// @Tags bookmarks
// @Accept json
// @Produce json
// @Param request body models.ImportBookmarkRequest true "书签导入信息"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/bookmarks/import [post]
func (s *Server) importBookmarks(c *gin.Context) {var req models.ImportBookmarkRequestif err := c.ShouldBindJSON(&req); err != nil {c.JSON(400, gin.H{"error": "无效的请求数据"})return}// 用于统计导入结果stats := map[string]int{"total": len(req.Bookmarks),"success": 0,"duplicate": 0,"failed": 0,}// 获取所有现有书签用于去重var existingBookmarks []models.Bookmarkif err := s.db.Find(&existingBookmarks).Error; err != nil {c.JSON(500, gin.H{"error": "获取现有书签失败"})return}// 创建URL映射用于快速查找urlMap := make(map[string]bool)for _, b := range existingBookmarks {urlMap[b.URL] = true}// 获取所有分类var categories []models.Categoryif err := s.db.Find(&categories).Error; err != nil {c.JSON(500, gin.H{"error": "获取分类失败"})return}// 创建分类名称到ID的映射categoryMap := make(map[string]uint)for _, cat := range categories {categoryMap[cat.Name] = cat.ID}// 处理每个书签for _, b := range req.Bookmarks {// 检查URL是否已存在if urlMap[b.URL] {stats["duplicate"]++continue}// 获取或创建分类categoryID := categoryMap[b.Category]if categoryID == 0 && b.Category != "" {// 创建新分类newCategory := models.Category{Name: b.Category,Description: "从导入的书签创建",}if err := s.db.Create(&newCategory).Error; err != nil {stats["failed"]++continue}categoryID = newCategory.IDcategoryMap[b.Category] = categoryID}// 创建书签bookmark := models.Bookmark{Title: b.Title,URL: b.URL,Description: b.Description,CategoryID: categoryID,}if err := s.db.Create(&bookmark).Error; err != nil {stats["failed"]++continue}stats["success"]++urlMap[b.URL] = true}c.JSON(200, gin.H{"message": "导入完成","stats": stats,})
}
往期系列
- Ai书签管理工具开发全记录(一):项目总览与技术蓝图
- Ai书签管理工具开发全记录(二):项目基础框架搭建
- AI书签管理工具开发全记录(三):配置及数据系统设计
- AI书签管理工具开发全记录(四):日志系统设计与实现
- AI书签管理工具开发全记录(五):后端服务搭建与API实现
- AI书签管理工具开发全记录(六):前端管理基础框框搭建 Vue3+Element Plus
- AI书签管理工具开发全记录(七):页面编写与接口对接
- AI书签管理工具开发全记录(八):Ai创建书签功能实现
- AI书签管理工具开发全记录(九):用户端页面集成与展示
- AI书签管理工具开发全记录(十):命令行中结合ai高效添加书签
- AI书签管理工具开发全记录(十一):MCP集成
- AI书签管理工具开发全记录(十二):MCP集成查询
- AI书签管理工具开发全记录(十三):TUI基本框架搭建
- AI书签管理工具开发全记录(十四):TUI基本界面完善
- AI书签管理工具开发全记录(十五):TUI基本逻辑实现与数据展示
- AI书签管理工具开发全记录(十六):Sun-Panel接口分析
- AI书签管理工具开发全记录(十七):Sun-Panel书签同步实现
相关文章:

AI书签管理工具开发全记录(十八):书签导入导出
文章目录 AI书签管理工具开发全记录(十八):书签导入导出1.前言 📝2.书签结构分析 📖3.书签示例 📑4.书签文件结构定义描述 🔣4.1. 整体文档结构4.2. 核心元素类型4.3. 层级关系4.…...
codeforces C. Cool Partition
目录 题目简述: 思路: 总代码: https://codeforces.com/contest/2117/problem/C 题目简述: 给定一个整数数组,现要求你对数组进行分割,但需满足条件:前一个子数组中的值必须在后一个子数组中…...
TMC2226超静音步进电机驱动控制模块
目前已经使用TMC2226量产超过20K,发现在静音方面做的还是很不错。 一、TMC2226管脚定义说明 二、原理图及下载地址 一、TMC2226管脚定义说明 引脚编号类型功能OB11电机线圈 B 输出 1BRB2线圈 B 的检测电阻连接端。将检测电阻靠近该引脚连接到地。使用内部检测电阻时,将此引…...

docker容器互联
1.docker可以通过网路访问 2.docker允许映射容器内应用的服务端口到本地宿主主机 3.互联机制实现多个容器间通过容器名来快速访问 一 、端口映射实现容器访问 1.从外部访问容器应用 我们先把之前的删掉吧(如果不删的话,容器就提不起来,因…...

安宝特案例丨寻医不再长途跋涉?Vuzix再次以AR技术智能驱动远程医疗
加拿大领先科技公司TeleVU基于Vuzix智能眼镜打造远程医疗生态系统,彻底革新患者护理模式。 安宝特合作伙伴TeleVU成立30余年,沉淀医疗技术、计算机科学与人工智能经验,聚焦医疗保健领域,提供AR、AI、IoT解决方案。 该方案使医疗…...

Modbus转Ethernet IP深度解析:磨粉设备效率跃升的底层技术密码
在建材矿粉磨系统中,开疆智能Modbus转Ethernet IP网关KJ-EIP-101的应用案例是一个重要的技术革新。这个转换过程涉及到两种主要的通信协议:Modbus和Ethernet IP。Modbus是一种串行通信协议,广泛应用于工业控制系统中。它简单、易于部署和维护…...
Python打卡训练营学习记录Day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
八、【ESP32开发全栈指南:UDP客户端】
1. 环境准备 安装ESP-IDF v4.4 (官方指南)确保Python 3.7 和Git已安装 2. 创建项目 idf.py create-project udp_client cd udp_client3. 完整优化代码 (main/main.c) #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h&…...

在MobaXterm 打开图形工具firefox
目录 1.安装 X 服务器软件 2.服务器端配置 3.客户端配置 4.安装并打开 Firefox 1.安装 X 服务器软件 Centos系统 # CentOS/RHEL 7 及之前(YUM) sudo yum install xorg-x11-server-Xorg xorg-x11-xinit xorg-x11-utils mesa-libEGL mesa-libGL mesa-…...

旋量理论:刚体运动的几何描述与机器人应用
旋量理论为描述刚体在三维空间中的运动提供了强大而优雅的数学框架。与传统的欧拉角或方向余弦矩阵相比,旋量理论通过螺旋运动的概念统一了旋转和平移,在机器人学、计算机图形学和多体动力学领域具有显著优势。这种描述不仅几何直观,而且计算…...
SE(Secure Element)加密芯片与MCU协同工作的典型流程
以下是SE(Secure Element)加密芯片与MCU协同工作的典型流程,综合安全认证、数据保护及防篡改机制: 一、基础认证流程(参数保护方案) 密钥预置 SE芯片与MCU分别预置相同的3DES密钥(Key1、Key2…...

运动控制--BLDC电机
一、电机的分类 按照供电电源 1.直流电机 1.1 有刷直流电机(BDC) 通过电刷与换向器实现电流方向切换,典型应用于电动工具、玩具等 1.2 无刷直流电机(BLDC) 电子换向替代机械电刷,具有高可靠性,常用于无人机、高端家电…...

Redis专题-实战篇一-基于Session和Redis实现登录业务
GitHub项目地址:https://github.com/whltaoin/redisLearningProject_hm-dianping 基于Session实现登录业务功能提交版本码:e34399f 基于Redis实现登录业务提交版本码:60bf740 一、导入黑马点评后端项目 项目架构图 1. 前期阶段2. 后续阶段导…...

【前端实战】如何让用户回到上次阅读的位置?
目录 【前端实战】如何让用户回到上次阅读的位置? 一、总体思路 1、核心目标 2、涉及到的技术 二、实现方案详解 1、基础方法:监听滚动,记录 scrollTop(不推荐) 2、Intersection Observer 插入探针元素 3、基…...

dvwa11——XSS(Reflected)
LOW 分析源码:无过滤 和上一关一样,这一关在输入框内输入,成功回显 <script>alert(relee);</script> MEDIUM 分析源码,是把<script>替换成了空格,但没有禁用大写 改大写即可,注意函数…...
关于疲劳分析的各种方法
疲劳寿命预测方法很多。按疲劳裂纹形成寿命预测的基本假定和控制参数,可分为名义应力法、局部应力一应变法、能量法、场强法等。 1名义应力法 名义应力法是以结构的名义应力为试验和寿命估算的基础,采用雨流法取出一个个相互独立、互不相关的应力循环&…...
数据库优化实战指南:提升性能的黄金法则
在现代软件系统中,数据库性能直接影响应用的响应速度和用户体验。面对数据量激增、访问压力增大,数据库性能瓶颈经常成为项目痛点。如何科学有效地优化数据库,提升查询效率和系统稳定性,是每位开发与运维人员必备的技能。 本文结…...

【Axure高保真原型】图片列表添加和删除图片
今天和大家分享图片列表添加和删除图片的原型模板,效果包括: 点击图片列表的加号可以显示图片选择器,选择里面的图片; 选择图片后点击添加按钮,可以将该图片添加到图片列表; 鼠标移入图片列表的图片&…...

XXE漏洞知识
目录 1.XXE简介与危害 XML概念 XML与HTML的区别 1.pom.xml 主要作用 2.web.xml 3.mybatis 2.XXE概念与危害 案例:文件读取(需要Apache >5.4版本) 案例:内网探测(鸡肋) 案例:执行命…...

mq安装新版-3.13.7的安装
一、下载包,上传到服务器 https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.13.7/rabbitmq-server-generic-unix-3.13.7.tar.xz 二、 erlang直接安装 rpm -ivh erlang-26.2.4-1.el8.x86_64.rpm不需要配置环境变量,直接就安装了。 erl…...
第21节 Node.js 多进程
Node.js本身是以单线程的模式运行的,但它使用的是事件驱动来处理并发,这样有助于我们在多核 cpu 的系统上创建多个子进程,从而提高性能。 每个子进程总是带有三个流对象:child.stdin, child.stdout和child.stderr。他们可能会共享…...

DL00871-基于深度学习YOLOv11的盲人障碍物目标检测含完整数据集
基于深度学习YOLOv11的盲人障碍物目标检测:开启盲人出行新纪元 在全球范围内,盲人及视觉障碍者的出行问题一直是社会关注的重点。尽管技术不断进步,许多城市的无障碍设施依然未能满足盲人出行的实际需求。尤其是在复杂的城市环境中ÿ…...

华硕电脑,全新的超频方式,无需进入BIOS
想要追求更佳性能释放 或探索更多可玩性的小伙伴, 可能会需要为你的电脑超频。 但我们常用的不论是BIOS里的超频, 还是Armoury Crate奥创智控中心超频, 每次调节都要重启,有点麻烦。 TurboV Core 全新的超频方案来了 4不规…...

安全领域新突破:可视化让隐患无处遁形
在安全领域,隐患就像暗处的 “幽灵”,随时可能引发严重事故。传统安全排查手段,常常难以将它们一网打尽。你是否好奇,究竟是什么神奇力量,能让这些潜藏的隐患无所遁形?没错,就是可视化技术。它如…...
触发DMA传输错误中断问题排查
在STM32项目中,集成BLE模块后触发DMA传输错误中断(DMA2_Stream1_IRQHandler进入错误流程),但单独运行BLE模块时正常,表明问题可能源于原有线程与BLE模块的交互冲突。以下是逐步排查与解决方案: 一、问题根源…...

Vue.js教学第二十一章:vue实战项目二,个人博客搭建
基于 Vue 的个人博客网站搭建 摘要: 随着前端技术的不断发展,Vue 作为一种轻量级、高效的前端框架,为个人博客网站的搭建提供了极大的便利。本文详细介绍了基于 Vue 搭建个人博客网站的全过程,包括项目背景、技术选型、项目架构设计、功能模块实现、性能优化与测试等方面。…...

[KCTF]CORE CrackMe v2.0
这个Reverse比较古老,已经有20多年了,但难度确实不小。 先查壳 upx压缩壳,0.72,废弃版本,工具无法解压。 反正不用IDA进行调试,直接x32dbg中,dump内存,保存后拖入IDA。 这里说一下…...
Redis——Cluster配置
目录 分片 一、分片的本质与核心价值 二、分片实现方案对比 三、分片算法详解 1. 范围分片(顺序分片) 2. 哈希分片 3. 虚拟槽分片(Redis Cluster 方案) 四、Redis Cluster 分片实践要点 五、经典问题解析 C…...

Ubuntu 安装 Mysql 数据库
首先更新apt-get工具,执行命令如下: apt-get upgrade安装Mysql,执行如下命令: apt-get install mysql-server 开启Mysql 服务,执行命令如下: service mysql start并确认是否成功开启mysql,执行命令如下&am…...
结合PDE反应扩散方程与物理信息神经网络(PINN)进行稀疏数据预测的技术方案
以下是一个结合PDE反应扩散方程与物理信息神经网络(PINN)进行稀疏数据预测的技术方案,包含完整数学推导、PyTorch/TensorFlow双框架实现代码及对比实验分析。 基于PINN的反应扩散方程稀疏数据预测与大规模数据泛化能力研究 1. 问题定义与数学模型 1.1 反应扩散方程 考虑标…...