【DV脚本】在单篇笔记里实现类 Notion Database 的轻量数据库

介绍

众所周知,在 Obsidian 里实现类似「数据库」的需求一直都是痛点。

虽然借助插件能实现,但往往得给每一项数据创建单独页面……很麻烦,完全不像 Notion 那样无痛而舒适。

很久以前我就有一个设想:
如果我有一篇笔记专门收集某个专题的内容,想要把它做成一个「小数据库」——不要求编辑,只求汇总展示,能行吗?

这个问题困扰了我半年之久,终于在最近,随着对 OB 和 Dataview 插件的学习,我实现了这个陈年需求。

展示

效果展示:

可以看到,通过一个笔记内的不同标题,就能自动生成一个「轻量数据库」进行呈现。

支持特性:

  1. 点击标题可以直接跳转到对应内容
  2. 只需要一篇笔记就可以创建数据库
  3. 支持多个属性的组合
  4. 根据元数据自适应生成表格列
  5. (大体上)实时更新,具体取决于 Dataview 的刷新间隔
  6. 简单易用:只需要三行代码加一个 JS 脚本文件即可实现

工作原理

笔记的书写

需要借助 Dataview 插件,以及固定的格式:

## 标题
[属性::值]  [属性2::值]  
  1. 元数据行紧挨着标题行写(标题层级不限制——几个井号都行)
  2. 可以有多个元数据,使用 [key::value] 的格式,一行里的多个属性之间要 隔开2个空格 :warning:
  3. 同一个属性(例如 标签 )可以在一个内联属性里用逗号写多个值,也可以一行里写多个同名属性(会自动合并)
    • 第一种: [标签:Obsidian,css]
    • 第二种:[标签:Obsidian] [标签:css]

关于内联属性
详见 Dataview 的文档:Adding Metadata - Dataview

数据库的呈现

首先,打开 Dataview 插件的设置,确保这几个选项都是打开的:

然后需要将这个 JS 文件放入你的笔记库,命名为 liteDatabase.js

const useList = false;
const curNote = dv.current();

if (!curNote){
    dv.span("当前文档未加载,请重新打开");
    return;
}

let tarFile = await app.vault.getAbstractFileByPath(curNote.file.path);

// 获取当前文件的 meta 数据
const curFileMeta = app.metadataCache.getFileCache(tarFile);
const headings = curFileMeta.headings;

const headingsList = headings.map( k => k.heading )

// 获取当前文章的文本内容
const curFilePath = curNote.file.path;
const curTFile = await app.vault.getFileByPath(curFilePath)
const content = await app.vault.cachedRead(curTFile);

if (!headings) {
  dv.span("当前文档缺少标题");
  return;
}

const meta = content.matchAll(/#+ (.*)\n(\[.*\:.*\])/gm);

// 标题作为 key,元数据作为 value,做成 DV 表格
let tableHeads = ["标题"]

// 先存成一个 dict
let metaValues = []

if (meta) {
    for (let m of meta) {
        let title = m[1]

        // 检查 title 是否在 headings 内(避免把一些代码块内的内容也当作元数据)
        if (headingsList.indexOf(title) == -1) {
            continue
        }

        let metaList = m[2].split("  ")
        let metaDict = {}
        
        // 添加元数据的 key 到表头
        for (let i = 0; i < metaList.length; i++) {
            let keyValue = metaList[i].replace("[", "").replace("]", "").split("::")
            // console.log(metaList[i], keyValue)

            let key = keyValue[0].trim()
            if (tableHeads.indexOf(key) == -1) {
                tableHeads.push(key)
            }

            if (useList){
                // 1. 列表形式,重复属性会合并
                let value = [keyValue[1]]
                metaDict[key] = metaDict[key] ? metaDict[key].concat(value) : value
            } else {
                // 2. 文本形式,重复属性会使用最新的
                let value = keyValue[1]
                metaDict[key] = metaDict[key] ? metaDict[key] + `,${value}` : value
            }
        }

        let returnValuesList = []

        for (let i = 0; i < tableHeads.length; i++) {
            if (i == 0) {
                returnValuesList.push(`[[#${m[1]}]]`)
            } else {
                returnValuesList.push(metaDict[tableHeads[i]] ? metaDict[tableHeads[i]] : "")
            }
        }

        metaValues.push(returnValuesList)
    }
}

dv.table(
    tableHeads,
    metaValues.map( k => k )
)


你也可以直接在 Github Gist 中查看或下载这个代码文件:DVJS Code - Lite Database for Obsidian
(后续如果有更新会直接传到 Gist 里)

然后,在笔记中插入:


```dataviewjs
dv.view("liteDatabase")
```

在笔记书写符合规则的情况下,现在你应该就能看到轻量数据库的呈现了!

测试文本

如果暂时没写内容,你也可以直接复制这段内容进笔记查看效果:

## 传说之下
[英文名::Undertale]  [标签::感人,剧情向]

「你落入了地底,然后——」
关于友情的故事。

## 星际拓荒
[英文名::Outer Wilds]  [标签::探索,感人]

「嘿,尝试过掉进黑洞吗?」
关于探索的故事。

## 生化奇兵:无限
[标签::好玩]   [标签::感人,剧情向]  [英文名::BioShock Infinite]

「过去多少年,我依旧在你身边」
关于时间的故事。

拓展

因为用的是 JS 语言,所以理论上来讲形式和内容都有很大的拓展可能性

比如你也可以不用 Dataview 内联属性,就用自己定义的某种格式。
或者你想对获取到的元数据再进行二次处理……

修改 JS 文件(或者让 GPT 帮你修改)就可以按自己想要的方式进行定制化!

碎碎念时间

起源

一开始是今年一月份的时候,看 GameOff 的游戏开发比赛发现好多作品非常有趣,然后就一边记录一边想着「打标记」:

你可以看到,当时我就在尝试类似的「标题+下方的标记」这种方式来进行记录,但是苦于 没有办法把它们汇总到一起展示出来
(甚至开始怀念起了 Notion 的数据库 lol)

但是这个问题放到今天,只需要把当时还在探索的「标记」替换成 Dataview 的内联属性,就能得到:

——每个游戏是不是 Godot 开发的,有哪些值得注意的亮点,一目了然!

属性的定义

关于元数据定义形式,主要是参考了 Github Badges 的样式:

这样一排小徽标排在一起看着还挺好看的。
(应该也可以通过 css 来实现多彩的背景色)

另外就是……放在同一行也可以让正则表达式的筛选更简化一些。

最早其实也尝试过「标签」还有「内联代码」这两种形式。
但标签的话……会污染我的标签池,这种内容我还是希望就局限在「这个页面内」;内联代码倒是没啥问题,想用也可以用的。

至于多个 KeyValue 中间的 两个空格,这个可能是 Dataview 自己的限制?只有一个空格的情况下似乎会有一些识别问题,所以就强行规定两个空格了。
(但我自己都经常忘记打两个,嗨呀)

6 个赞

有点意思,感觉你这种写法,也可以用来写任务

其实我之前也搞过类似的,就是单文件写表格

缺点很多,不灵活,代码复杂,太个性化

所以自己折腾着用,但一般推荐的话还是推荐给每一条数据单独建一个页面,能实现更多的功能,比如直接在表格里编辑等等

1 个赞

也可以的!

然后单纯统计任务的话,用 dataview 提供的 tasks 会更方便一些:

嗯,可以结合着用~
一些不复杂的内容用单页面对我来说就够了,需要更多功能性的时候再转用专门的插件实现 ::

(我去,是 calm 佬!

其实 Dataview 提供了一个读取 CSV 文件的接口 dv.io.csv。看起来更适合数据库的需求,它可以使用 Excel 进行编辑。事实上还可以通过 Dataview 调用 Obsidian 接口创建一个可编辑视图。

const data = await dv.io.csv(“xxx.csv”);

哈哈哈虽然说是「数据库」但也不想真的去编辑 csv 啦!
还是希望在 Obsidian 内记笔记的基础上有个快速呈现的方式就够了~

其实我觉得ob也完全可以实现数据库功能,诚然,如果通过复杂的语法在markdown中插入代码显然是不现实的,但谁说markdown就一定得在markdown中实现?完全可以在外部实现,markdown做个引用即可。

比如markdown新增语法 [[@课程表 ]] 那么就代表引用了名为课程表的数据库,这个数据库存储在专门的目录,数据库名必须全局唯一,原因是这样不受文件移动的影响,格式嘛,建议用JSON,如果markdown无法满足,或许只有JSON更好用了,如果要自定义语法一定要易用。然后引用过来后就可以可视化操作了。敲入@后还可以选择已有的表格引用,如果敲入新的名字就是创建。

肯定会有人反对,这已经不是markdown了,我觉得这没有关系,首先要明白,你是数据的主人,而软件不是,总说自己掌控数据,又怪软件提供了高级功能,这不是相互矛盾吗?如果想完全markdown可以不用这个高级功能嘛,用普通表格就是了。数据是你的,怎么用是自己的事,而和软件无关,软件给了你选择,你非用高级功能,然后又说不markdown,这能怪谁呢?

那,如果有一天我需要把markdown用于其他用途,这些标签不支持咋办?我建议官方可以提供一键转表格功能,转为表格后,原数据库还在,哪天你想转换回来重新引用即可。当然,最好也提供查看未引用表格功能,然后用户确认后可删除。

有人会说,那我备份文件或移动文件有影响吗?我觉得可以把表格数据放到.obsidian文件夹中,这样同一个仓库内移动文件不受影响,备份只需要复制仓库文件夹即可。

大家觉得这个想法怎么样?

canvas就是json,这个想法本身是合理的,就是实现上的难度
不过我觉得需要有一点注意,那就是数据库文件里的内容可以在ob全局搜索里找到

请问这个怎么实现呀 :smiling_face_with_tear:

dataviewjs代码

你对 Dataview 插件了解多少,我看看从哪儿开始讲起hhh

最简单来说,三个步骤:

  1. 安装并启用 Dataview 插件,在设置里检查这项是打开的:

  2. 在笔记里新建一个文本文件,命名成:liteDatabase.js
    具体路径没关系,放在你笔记库的文件夹里就OK。

举个例子,你的笔记库如果在 D://Note/ ,你可以创建一个 D://Note/Scripts 文件夹然后把这个 js 文件扔进去。

然后把我帖子里的那个 JS 内容复制粘贴进去。

  1. 在你需要生成数据库的笔记里,粘贴这段内容:
```dataviewjs
dv.view("liteDatabase")
```

## 传说之下
[英文名::Undertale]  [标签::感人,剧情向]

「你落入了地底,然后——」
关于友情的故事。

## 星际拓荒
[英文名::Outer Wilds]  [标签::探索,感人]

「嘿,尝试过掉进黑洞吗?」
关于探索的故事。

正常来说这样就能看到效果。
如果有啥问题可以再附上截图具体来问~

感谢感谢,我已经成功实现了这样的效果。不过有一个需求请问能实现吗,目前的数据库实现是通过【标题+dataview内联样式】实现,有没有完全使用【dataview内联样式实现的】办法呢?

具体情景是:我有一个(或者多个)放书单的md文件,里面有很多书的信息(书名、作者、描述、状态等),我希望可以另外有一个md文件作为数据库用表格的方式呈现这些信息。(【标题+dataview内联样式】当然能实现,但这样需要所有书名都设置为标题,在书籍很多的时候就不太方便)

比如,书单文件的内容可以是:

[书名::book1]
[作者::AAA]
[描述::aaa]
[状态::未读]
***
[书名::book2]
[作者::BBB]
[描述::bbb]
[状态::已读]

然后另一个数据库文件就能呈现出书籍表格。

其实就是上面展示的那种:

我不分享代码,思路的话就是数据处理,这些输出就是个object,会写代码的可以自己console.log慢慢调试,不分享的原因有三:

  1. 我的态度在上面已经很明显了,这个太复杂,自用就乱折腾,不推荐给别人
  2. 因为是自用,代码写的没有通用性,给别人还要改,我没时间
  3. dv新版要出来了,如果新版支持编辑的话,代码得完全重构,虽然我不一定重构,这玩意我不想折腾了,老老实实找个表格软件做就完事了

另外还有一点,我这么用,是因为插件自带plugin-id,其他场景虽然也能用,但是逻辑上就少了点流畅

1 个赞

尝试了下,出现效果了,就是必须按照特定格式,先不用了

嗯,可以等有需要的时候再用~

另外格式啥的也可以让 AI 帮你重新写一下,理论上可以变得很自由,不用固定格式 √