兼容性信息
  • 文章中脚本已适配 cosma 2.4.0 版本【2023.12.27】
  • cosma v2.1.0 版本更新后,原生支持 Obsidian 标题形式的双向链接,但优先识别 id 字段。本脚本执行过程会向输出文件增加 id 元数据,所以脚本在新版本 cosma 下仍能正确实现预期的功能。更多信息详见:Cosma | Changelog (arthurperret.fr)

本文包含的项目:

Readme Card

挖坑

由于最近使用 Obsidian 写文章,页面之间的链接越来越丰富,关系图谱越来越复杂。我在想能不能有一种方法可以把 Obsidian 的这张关系图导出放在网站上呢?经历一番探索之后,我发现 Obsidian 官方是推出过关系图生成网页的功能的:Obsidian Publish,但它是付费服务。

终于,我在 Obsidian 的论坛找到了我想要的:Share the graph view and notes as HTML page for free with obsidian2cosma - Share & showcase - Obsidian Forum。在这篇帖子中,作者利用一款可以生成这种关系图的开源 APP Cosma 进行生成。Cosma 可以生成一个 HTML 网页,在包含关系图的同时提供对各个结点的交互,让用户在多种视图下审阅知识库的内容。

为了让文章中的双向链接能被 Cosma 识别,帖子作者 Kévin Polisano 还自己编写了一个 Python 命令行工具,将 Obsidian 知识库中的文章的 Front-matter、以及双向链接的格式进行替换,以使得 Cosma 能够识别文章。这一点就让我看到了 Hexo 关系图谱实现的可能性,于是耗时一个星期的挖坑开始了…

效果

文章在 Obsidian 中显示的关系视图是这样的:

image.png

在 Hexo 侧边栏是这样的:

image.png

主关系图网页是这样的:

image.png

包含了各种标签筛选、视图以及文章查看等功能。

思路

整个过程分为几个步骤,我的愿景是脚本全自动进行。在 Hexo 进行本地部署或上传至服务器时进行以下操作:

  1. 使用脚本转换 Hexo 中 _posts 目录下的文章为 Cosma 可以识别的文章
  2. 运行两次 Cosma,根据不同的配置文件生成两个不一样的 cosmoscope.html(这就是我说的那个关键的包含关系图谱网页)
  3. 将其中一个 cosmoscope.html 用脚本进行修剪,用做侧边栏的展示
  4. 将处理好的 cosmoscope.html 粘贴到 Hexo 博客 source 目录下某指定文件夹中
  5. 删除转换后的文件,以免 Obsidian 仓库混淆
  6. 脚本进行剩余的本地部署或发布操作

操作

转换 Obsidian 文章为 Cosma 可识别的文章

这一步是关键一步。只要能导出 HTML 关系图谱网页那么一切都好办了。

前面提到过 Kévin Polisano 写了一个脚本实现了这样的功能,但是限于这个脚本的诞生条件(Cosma 有未修复的 Bug)以及创作环境(作者是法国人且常用 Linux/IOS,脚本有很多兼容性 Bug),导致它的脚本跑不起来。

在我仔细研读源代码后决定将其重写代码以让它正确工作。项目地址如下:

Readme Card

欢迎大家来捧场 🎉🎉!喜欢可以点个 star ⭐

这个脚本最主要特点:Windows/Linux/MacOS 适配Cosma/Zettlr 兼容适合 Hexo 类型文章管理语言支持(更适合中国宝宝体质)

我在原脚本的基础上修复了一些 Bug 也增加了一些功能以适配我的需求:

修复与增强
  • 重写了 YAML 解析器,使之可以解析更多格式的 YAML,摆脱特定关键字的依赖。
  • 重写了创建 ID 的逻辑。
  • 功能:更改 front-matter 中的关键字名称。
  • 修复了在 Windows 上复制源文件创建日期时出现的错误。
  • 提高在 Windows 上工作的效率。
  • 为频繁使用脚本提供交互增强。
  • 支持使用 UTF-8 字符集,中文标题文章也能用。
  • 更新 README.md 并增加其中文版本。
  • 其他:ID 冲突检测、程序运行时间计算等
    —— 摘自 uuanqin/obsidian2cosma(github.com) 中文文档

使用 Cosma 生成关系图谱 HTML

在使用了上述脚本后,输出的 Markdown 文件会在指定的文件夹中,这里假设为 data/。接下来我们使用 Cosma 进行读取和转换。

Cosma 的安装与使用请详详细细阅读官网:Cosma | About Cosma (arthurperret.fr)

我的设想是使用两个不同的 Cosma 配置文件生成两种 cosmoscope.html:一个用做侧边栏的基础展示,不需要那么多花里胡哨的功能,视图更清晰一些即可,这里我称之 cosmoscope_trim.html;另一个用做正式的交互网页,一般原封不动即可。

cosmoscope.html 进行修剪与美化

在上一步中,配置文件只能调一些基本结点样式而已,不能关闭一些功能。这就会导致 cosmoscope_trim.html 文件很大,在侧边栏中显示不全。因此,这里我通过浏览器 F12 确定删减的标签与更改的样式,在将这个操作新写成另一个 Python 脚本。

image.png

此外,我对这个网页增加了一个按钮,让读者可以进入到主关系视图网页。项目来自:Css buttom (codepen.io)

脚本分享:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""
此脚本将根据 Cosma CLI 2.4.0 生成的关系图谱网页进行修剪。
作者:uuanqin([email protected]
"""

import os
import re

from bs4 import BeautifulSoup
import sys
from pathlib import Path

def trim(target_html):
"""
修剪 HTML文件使之能在侧边栏展出
:return:
"""

soup = BeautifulSoup(open(target_html,"r",encoding="utf-8"), 'html.parser')

# 删除多余元素
soup.html.body.aside.decompose()
soup.html.body.main.decompose()
soup.html.body.button.decompose() # 2.4.0
soup.html.body.button.decompose() # 2.4.0
for div in soup.html.body.find_all("div", {'class': 'graph-controls'}):
div.decompose()


# 修改css,使得图片占据全画面
style_str = soup.html.style.string
style_str = re.sub(r"(width: )calc\(100vw - 30rem\)(;)",r"\1 100vw\2",style_str,count=1,flags=re.DOTALL)
soup.html.style.string = style_str

# 项目地址 https://codepen.io/jqueryalmeida/pen/ZxmzYe
# CSS部分
css_string = """
.btn-b {
position: absolute;
bottom: 9%;
right: 2%;
cursor: pointer;
font-size: 10px;
text-align: left;
background: #a188fc;
background: linear-gradient(to right, #0f4eb4 1%, #07a1e9 100%);
color: #ddffff;
padding: 0rem 0rem 0rem 0rem;
border-radius: 10rem 50rem 50rem 10rem;
-webkit-transition: all 0.7s;
-moz-transition: all 0.7s;
transition: all 0.7s;
display: flex;
align-items: center;
justify-content: space-between;
}
.btn-b:after {
content: "";
position: absolute;
right: -1px;
margin-left: 1em;
width: 35px;
height: 35px;
border-radius: 50%;
box-shadow: 0px 2px 8px 0px rgba(15, 78, 180, 0.22);
background: #3bc1ff url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgUHJvIDYuNC4yIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlIChDb21tZXJjaWFsIExpY2Vuc2UpIENvcHlyaWdodCAyMDIzIEZvbnRpY29ucywgSW5jLiAtLT48cGF0aCBkPSJNMzQ0IDBINDg4YzEzLjMgMCAyNCAxMC43IDI0IDI0VjE2OGMwIDkuNy01LjggMTguNS0xNC44IDIyLjJzLTE5LjMgMS43LTI2LjItNS4ybC0zOS0zOS04NyA4N2MtOS40IDkuNC0yNC42IDkuNC0zMy45IDBsLTMyLTMyYy05LjQtOS40LTkuNC0yNC42IDAtMzMuOWw4Ny04N0wzMjcgNDFjLTYuOS02LjktOC45LTE3LjItNS4yLTI2LjJTMzM0LjMgMCAzNDQgMHpNMTY4IDUxMkgyNGMtMTMuMyAwLTI0LTEwLjctMjQtMjRWMzQ0YzAtOS43IDUuOC0xOC41IDE0LjgtMjIuMnMxOS4zLTEuNyAyNi4yIDUuMmwzOSAzOSA4Ny04N2M5LjQtOS40IDI0LjYtOS40IDMzLjkgMGwzMiAzMmM5LjQgOS40IDkuNCAyNC42IDAgMzMuOWwtODcgODcgMzkgMzljNi45IDYuOSA4LjkgMTcuMiA1LjIgMjYuMnMtMTIuNSAxNC44LTIyLjIgMTQuOHoiLz48L3N2Zz4=) no-repeat center;
background-size: 50%;
}
"""
new_style_tag = soup.new_tag("style")
new_style_tag.string = css_string
soup.html.head.append(new_style_tag)
# HTML部分
new_div_tag = soup.new_tag("div")
new_div_tag["class"] = "btn-b"
# 链接
new_a_tag = soup.new_tag("a")
new_a_tag["href"] = "/DO_NOT_render/cosmoscope/cosmoscope.html"
new_a_tag["target"] = "_blank"
new_a_tag.append(new_div_tag)
soup.html.body.div.insert_after(new_a_tag)


# 重新写回
p = Path(target_html)
output_html = os.path.dirname(target_html)+"/"+p.stem+'_trim.html'

with open(output_html,"w",encoding="utf-8") as f:
f.write(soup.prettify())

if __name__ == "__main__":
# target_html = "cosma_dir/cosmoscope.html"
trim(sys.argv[1])

Hexo 侧边栏配置

实现无缝侧边栏参考我这篇教程:Butterfly 不改动主题源码实现自定义侧边栏

widge.yml:

1
2
3
4
5
6
7
8
9
- class_name: seamless-card
id_name:
name:
icon:
order:
html: |
<div style="height: 250px">
<iframe src="/DO_NOT_render/cosmoscope/cosmoscope_trim.html" frameborder="no" style="width: 100%; height: 100%"></iframe>
</div>

可以看到 iframe 指向 cosmoscope_trim.html

Hexo 部署自动化

上文 思路,中提到的自动化步骤,通过了 npm script 及其钩子实现。

以前的文章中我提到过钩子的用法:

分享我目前的 npm script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"scripts": {
"build": "hexo generate",
"clean": "hexo clean",
"deploy": "hexo deploy",
"server": "hexo server",
"prepublish": "npm run check-obsidian && npm run check-markdown-filename",
"publish": "hexo clean && hexo g && npm run gen-cosma && hexo d",
"postpublish": "hexo algolia && npm run flush-cdn",
"prestart": "npm run check-obsidian && npm run check-markdown-filename",
"start": "hexo clean && hexo g && npm run gen-cosma && hexo s",
"flush-cdn": "node my_scripts/auto_flush_cdn.js",
"check-obsidian": "node my_scripts/check_obsidian.js",
"check-markdown-filename": "python my_scripts/check_md_name.py source/_posts",
"ob2co": "python my_scripts/obsidian2cosma.py -i source/_posts -o my_scripts/cosma_dir/data --ignore -f --method abbrlink --attrreplacement categories,types date,begin --verbose",
"cosma-total": "cd my_scripts/cosma_dir/dir_total && cosma m",
"cosma-trim": "cd my_scripts/cosma_dir/dir_trim && cosma m && python ../../cosmoscope_trim.py ../../cosma_dir/dir_trim/cosmoscope.html",
"cosma-copy": "copy \".\\my_scripts\\cosma_dir\\dir_total\\cosmoscope.html\" \".\\source\\DO_NOT_render\\cosmoscope\\cosmoscope.html\" & copy \".\\my_scripts\\cosma_dir\\dir_trim\\cosmoscope_trim.html\" \".\\source\\DO_NOT_render\\cosmoscope\\cosmoscope_trim.html\"",
"cosma-rm-output": "echo \"删除转换后的输出文件 - Cosma\" && rmdir /s /q .\\my_scripts\\cosma_dir\\data\\",
"gen-cosma": "npm run ob2co && npm run cosma-total && npm run cosma-trim && npm run cosma-copy && npm run cosma-rm-output"
}

每个步骤保持分开,以方便调试。

obsidian2cosma 脚本指令:

1
2
3
4
5
6
7
8
python my_scripts/obsidian2cosma.py
-i source/_posts
-o my_scripts/cosma_dir/data
--ignore
-f
--method abbrlink
--attrreplacement categories,types date,begin
--verbose

后记

最终能完成这样的效果我也很欣慰,和设想的差不多,似乎感受到了一丢丢程序员改变世界的力量 🤭🤭🤭。很多细节我没有详细写出,但是把最重要的思路理清了,有问题的小伙伴可直接评论。

后续改进

  • 事后清洁,删除输出的 Markdown 文档以免混淆 Obsidian
  • 处理侧边栏图片报错问题,太吵了(有点困难,因为 js 有压缩,只能另辟蹊径)
  • 研究不同页面 Focus
  • 主页面增加原文跳转链接(这可能得深入源码去改,或者编写脚本,统一从源 HTML 创造 N 份版本)
  • 攻克侧边栏 Iframe 网页内的夜间模式问题
  • 移动端适配不是很友好
  • 历史记录 history 臃肿,需要不时删一删
  • cosma 2.1.0 侧边栏显示图像过小(只能等更新,脚本暂时无法处理)(新版本已修复问题)
  • 完整网页加载较慢。原因是cosma需要对所有的网络图片进行加载,没有懒加载功能。这会导致CDN流量过大。

其实,自己弄一个简单的,就可以解决上面所有问题。

踩过的坑

  1. npm script 中使用 copy 命令时,路径用 \\ 分隔,不然会找不到文件
  2. 你可能会把 Obsidian2cosma 脚本输出的文档放在了平时用 Obsidian 写作的 Vault 内。请注意,这样的话 Obsidian 仓库中就会出现两个同名但不同文件夹的文章,会导致以后平时插入双向链接或修改双向链接时出现双向链接题目不对的问题,进而导致第二次使用该脚本时一些链接识别不出的问题。
  3. 博客根目录不要创建 scripts 目录并在里面放自己写的脚本,除非你知道自己在做什么。否则导致 hexo clean 出错。针对这一点,我似乎找到了根源:插件 | Hexo

本文参考