用 Dataview 批量编辑笔记
引子
Obsidian 好像没有批量查找替换吧,没有吧,没有吧?我懒得深入研究……(真懒
有需求的时候就打开 VS Code,批量查找替换支持正则,很爽啊!
但是这样并不能满足所有需求,毕竟总会有更复杂的情况,比如给某些具有特定标记、特征的笔记进行一些编辑、修改、替换什么的。是的,我可以写个脚本,但是遍历所有文件,分析内容结构啥的,不说难吧,有点麻烦。
而 Dataview 是 Obsidian 下非常强大的查询插件,如果它能在查询之后进行编辑,那就完美了。
原理
Dataview 本身没有提供对笔记的编辑功能,甚至对于笔记内容的读取功能都没有。它能查询笔记中的各种特征,但不是笔记中的所有内容。这是基于资源开销的考虑,现在这样的设计只需要查询 Obsidian 提供的缓存就可以了。
所以这里需要用到几个 Obsidian 自身的 api:
app.vault.readRaw(笔记路径)
:这是读取笔记的原始内容,很多用户脚本中会使用,但是官方的 API 文档中并没推荐。异步函数,需要await
。- 官方推荐的方法有两种:
app.vault.read(文件对象)
和app.vault.readCache(文件对象)
。以下摘抄自官方文档:- If you only want to display the content to the user, then use
cachedRead()
to avoid reading the file from disk multiple times.
如果只想向用户显示内容,则使用cachedRead()
以避免多次从磁盘读取文件。 - If you want to read the content, change it, and then write it back to disk, then use
read()
to avoid potentially overwriting the file with a stale copy.
如果您想要读取内容、更改内容,然后将其写回磁盘,请使用read()
以避免可能用过时的副本覆盖文件。
- If you only want to display the content to the user, then use
- 如何获取参数中的文件对象,方法比较多,最容易发现的是
app.valut.getFiles()
方法,类似的还有app.valut.getFilesCache()
方法,但这些是一次读取所有文件,所以很多场景下并不适用。毕竟我们已经用 Dataview 去完成了查询和过滤的工作,所以这里没必要再一次读取所有文件并过滤。 app.vault.getAbstractFileByPath(文件路径)
可以获取文件对象,在读写 api 中都会用到- 用
app.vault.modify(文件对象, 新的内容)
方法对笔记进行修改,必须用完整笔记内容去替换掉旧的笔记。
实践
/* 首先用 Dataivew 查询所有你需要的页面 */
dv.pages()
/* 根据一些条件对页面进行筛选 */
.filter(p=>p.file.frontmatter.published && p.file.frontmatter.title)
/* 遍历并修改选出的页面 */
.forEach(async p=>{
/* 获取当前遍历到的文件对象 */
const noteTFile = app.vault.getAbstractFileByPath(p.file.path)
/* 读取笔记内容 */
const noteContent = await app.vault.read(noteTFile)
/* 根据需求对内容进行加工,获得新的内容 */
const uuidLine = `id: ${crypto.randomUUID()}\n`
const newContent = noteContent.replace(/(^-{3,}\n)([\s\S]*?-{3,}\n)/g, (m, p1, p2)=>p1+uuidLine+p2)
/* 将新内容写回 */
await app.vault.modify(noteTFile, newContent)
})
优化
Dataview 的代码会被自动执行,这导致每次显示到该代码块,这套自动编辑的动作都会被做一遍,这很可能产生意外。所以我会把上面的内容定义为一个函数,在代码块中插入一个按钮来触发这个动作,并且在执行时输出一些日志,以便观察执行情况。
这部分我在自己笔记中的 Dataview\Tools\UUID
视图中有完整的应用。