Obsidian 的高级玩法|打造能跳转到任何格式文件的笔记

41,678 阅读13分钟

我把学习一个知识点的过程分为:收集信息 -> 处理信息 -> 应试,这篇文章也会以这个脉络为大纲来写 收集信息分为两步,即“收”和“集”。

“收”,即从多个平台获取文章(这里之所以明确是文章,是针对计算机学习。对于摄影或其他领域的学习就不只是文章了,还有图片之类形式的信息,这时候可能就不适用了,请读者自行斟酌使用),电脑端我是用简悦(简悦可以把网页文章转成本地 Markdown ),而手机端我是用Cubox(Cubox 可以把手机上收藏的文章在电脑打开)。

“集”,即将信息聚合成自己的笔记,然鹅信息有很多种形式,比如 pdf、网页视频/音频、图片、网页文章等等。Obsidian 恰好能很好地聚合这些形式,本文也着重讲解我是怎么玩转 Obsidian 的。

处理信息则是在收集信息后,怎么把这些知识变成自己的东西,也可以说是转化吧。

应试很好理解,即各种考试、面试。这一步看情况而定,像如果我学习这些东西只是为了提升自己,应对日常开发的话,是不需要应试的,因为我不记得了就随时可以翻这些笔记,但如果做笔记是为了考试和面试,那么应试这步却是不可或缺的一环,毕竟应试是为了背诵然后去经历考试的筛选,挺无奈的。。。

什么是 Obsidian

简单地说,Obsidian 是一款支持双链的本地 Markdown 笔记软件,拥有海量高效的插件增强,并且支持多平台。 详细的介绍就不说了,毕竟这篇不是入门级指南,入门级指南请看:

笔者以前也用过几款笔记软件,就不点名了,免得有拉踩的嫌疑。最终选择 Obsidian 的原因是因为它解决了我在信息收集处理过程中的以下痛点:

痛点

  • 在笔记中引用了某本书的一些话,过一段时间想回原文考究时却很难找到出处
  • 在根据某本书学习时,想要摘抄原文的一些话却只能自己跟着书手敲一遍到自己的笔记中
  • 想对视频做笔记
  • 在流程图中想对特定关键字跳转到某篇自己的笔记
  • 想把笔记内容导入到 anki 去背诵
  • 想把网页文章变成自己本地的一篇笔记
  • 笔记之间能想维基百科那样跳转

那么 Obsidian 是怎么解决这些痛点的呢:

解决痛点

  • 在笔记中快速引用 pdf 原文

    (对应痛点:在根据某本书学习时,想要摘抄原文的一些话却只能自己跟着书手敲一遍到自己的笔记中)

  • 能跳转到 pdf 原文

    (对应痛点:在笔记中引用了某本书的一些话,过一段时间想回原文考究时却很难找到出处)

  • 能跳转到视频的某个时间点

    (对应痛点:想对视频做笔记) video.gif

  • 在流程图中跳转到自己的笔记

    (对应痛点:在流程图中想对特定关键字跳转到某篇自己的笔记) pic.gif

  • 把笔记内容导入到 anki 去背诵

    (对应痛点:想把笔记内容导入到 anki 去背诵)

  • 剪藏网页文章到本地

    (对应痛点:想把网页文章变成自己本地的一篇笔记) web.gif

  • 笔记之间能跳转

    (对应痛点:笔记之间能像维基百科那样跳转)

如果 Obsidian 解决的痛点满足你的需求,可以看 Obsidian 的安装基础使用,本篇文章不介绍基础使用 目前我已经使用了此学习流稳定输出了 823 篇笔记,累计使用时间为 6 个月,通过以下笔记的 关系图谱 可以看到我写的所有笔记之间的联系,并且可以看出我对不同领域的积累量(看笔记的数量,一个点代表一篇笔记)

收集信息

使用 Obsidian 做笔记时可以在学习过程中做到真正的有迹可循,也可以让知识点之间解耦,使知识点原子化,以强化知识点之间的联系。

连接电子书

展示

配置过程

使用工具

  • Annotator
    • 是一个用于给 pdf 做标注和跳转的开源插件
  • Linter
    • 是一个用于格式化笔记的开源插件

配置 Linter

目标:修改其源代码,添加将 Annotator 样式修改为 📌

添加规则分类:

添加具体的规则处理:

代码如下:

new Rule('Reform Annotation', '修改 Annotation 链接的样式', RuleType.MINE, (text, options = {}) => {  
    const searchRegex = new RegExp(`${options['Find']}`, "gm");  
    text = text.replace(searchRegex, `${options['Replace']}`);  
    return text;  
},[  
], [  
    new TextOption('Find', 'regular expression for finding', "\\[\\[([^@]+)@annote#([^|]+)\\|([^📌\\]]+)\\]\\]"),  
    new TextOption('Replace', 'result of replacement', '[[$1@annote#$2|📌]]$3')  
])

保存后退出 ide,并重启 Obsidian,打开 Linter 的设置,并开启新功能的开关

测试一下: (测试前) (测试后)

日常工作流

OCR 字体识别

如果目标 pdf 文件不能选中文本,那么先用 Acrobat 进行字体识别:

点击 编辑

接着就会自动识别文档了

标注 pdf

打开一个 pdf 文件,左键选中要标注的句子,然后会自动弹出 Hightlight 按钮并点击,就可以完成一个句子的标注

接着按如下操作即可完成链接到 pdf 中的标注

连接网页

实际是网页剪藏和标注

展示

web.gif

实用工具

配置

安装简悦

  1. 在 chrome 网上应用店安装简悦的浏览器插件
  2. 安装成功后
  3. 安装能在 简悦 内使用的脚本插件 可以看到在 插件中心 有挺多脚本插件的 然后在 插件中心 下载以下脚本插件 因为很多技术文章的标题都带有特殊符号,这会导致导入 Obsidian 失败,所以还得安装以下插件解决该问题:
  4. 设置“导入到 Obsidian”
  5. 设置“Markdown 模板辅助增强插件”
  6. 设置“自动化辅助增强”
  7. 设置快捷键 使按 ob 时只导出全文

配置 Linter

修改 Linter 源代码,添加可以将图片外链转换成自己图床的链接 ps. 我使用的图床是 github,并且使用 PicGo 快速上传图片到图床。

首先引入 http、https、fs 库

var fs = require('fs');  
var http = require('http');  
var https = require('https');

添加自定义的方法

ps. 我的前端能力很菜很菜,献丑了。大佬们可以自己写这部分的功能。这段代码的逻辑就是先在本地新建一个文件夹 temp_pic_folder,然后通过正则表达式匹配到图片链接,然后通过这些链接把图片一个一个下载到文件夹 temp_pic_folder 中(没错,就是那么弱智的方法,因为不想在这方面花太多时间,毕竟工作是后端),然后再把下载下来的图片一个一个上传到用户设置的图床中,并修改文章中这些图片链接为最终图床上的图片链接,最后再把文件夹 temp_pic_folder 及其内部的图片都从本地删了。 注意!我这段代码只能匹配末尾有图片文件后缀名的链接,如 https://xxxx/xxx/.../xxx.jpg,如果是 https://xxxx/xxx/.../xxx 就不能识别,别问我为什么不处理这种情况,懒且够用

/**  
 * 将外链图片链接转换为自己的图床链接  
 *  
 * @param text 文档的文本  
 * @param options 插件在 setting tab 的设置  
 */  
async changeOuterLink (text, options = {}, app) {  
    // 获取当前编辑文档的所在文件系统的绝对地址  
    const tempFolder = "temp_pic_folder";  
    const anchorFile = "anchor_file";  
    const parentPath = app.vault.getAbstractFileByPath(app.workspace.getActiveFile().path).parent.path;  
    // 以 \ 为分隔符的就当作是 path    const tempFolderRelativePath = parentPath + "\\" + tempFolder;  
    // 以 / 为分隔符的就当作是 url    const tempFolderRelativeUrl = parentPath + "/" + tempFolder;  
    const anchorFileRelativeUrl = tempFolderRelativeUrl + "/" + anchorFile;  
    const basePath = app.vault.adapter.getBasePath();  
    const absolutePath = basePath + "\\" + tempFolderRelativePath;  
  
    // 匹配图片网址的正则表达式(已测网站图床包括:知乎、CSDN等)  
    const searchRegex = new RegExp(`${options['Image regex']}`, "gm");  
  
    // 自己图床的 url    let myPicBedUrl = `${options['Host']}` + `${options['Repository']}`;  
    // 已经下载的文件的列表  
    let fileList = [];  
    // 文件名到其相应文件后缀的映射  
    let fileExtMap = {};  
    // 下载图片后返回的多个 promise    let promiseList = [];  
    // 存放图片的文件夹里的文件列表  
    let tempFileSet = new Set();  
  
    let folderAbstractFile = app.vault.getAbstractFileByPath(tempFolderRelativeUrl);  
    if (folderAbstractFile) {  
        // 将文件名保存到 set 中  
        let downloadedPicList = app.fileManager.getNewFileParent(anchorFileRelativeUrl).children;  
        for (let i = 0; i < downloadedPicList.length; i++) {  
            tempFileSet.add(downloadedPicList[i].basename);  
        }  
    } else {  
        // 创建存放图片的文件夹  
        await app.vault.createFolder(tempFolderRelativePath);  
        folderAbstractFile = app.vault.getAbstractFileByPath(tempFolderRelativeUrl);  
        await app.vault.create(anchorFileRelativeUrl, "");  
    }  
  
    text = text.replace(searchRegex, (rs, $1, $2) => {  
        // 检查是否是自己图床的图片  
        if ($1.indexOf(myPicBedUrl) === -1) {  
            // 检查本地是否已经下载  
            let picName = $1.slice($1.lastIndexOf('/') + 1).toString();  
            if (!tempFileSet.has(picName)) {  
                let perPromise = new Promise((resolve, reject) => {  
                    let request;  
                    if ($1[4] === "s") {  
                        request = https.get($1 + $2, (res) => {  
                            if (res.statusCode !== 200) {  
                                console.log("download image error!");  
                                return;                            }  
                            let ext = res.headers['content-type'].split('/')[1];  
                            let dest = absolutePath + "\\" + picName + "." + ext;  
                            let file = fs.createWriteStream(dest);  
  
                            res.on('end', () => {  
                                console.log("图片下载完毕");  
                            });  
  
                            // 进度、超时等  
                            file.on('finish', () => {  
                                file.close();  
                                fileList.push(dest);  
                                fileExtMap[picName] = ext;  
                                resolve();  
                            }).on('error', (err) => {  
                                fs.unlink(dest);  
                            });  
                            res.pipe(file);  
                        });  
                    } else {  
                        // todo http 的不管用  
                        // 和 https 一样的代码  
                    }  
                    request.on('error', reject);  
                    request.end();  
                });  
                promiseList.push(perPromise);  
            } else {  
                console.log("图片已存在");  
            }  
        } else {  
            console.log("自己的地址");  
        }  
        return rs;  
    });  
  
    return Promise.allSettled(promiseList)  
        .then((results) => {  
            console.log("文件列表 " + fileList);  
            if (fileList.length > 0) {  
                return new Promise((resolve, reject) => {  
                    const postOptions = {  
                        hostname: `${options['PicGoUrl']}`,  
                        port: `${options['PicGoPort']}`,  
                        path: `${options['PicGoPath']}`,  
                        method: 'POST',  
                        headers: {  
                            'Content-Type': 'application/json'  
                        }  
                    }  
  
                    const req = http.request(postOptions, async res => {  
                        if (res.statusCode === 200) {  
                            console.log("删除图片文件夹 " + folderAbstractFile.path);  
                            await app.vault.delete(folderAbstractFile, true);  
                            // 正则替换图片地址  
                            text = text.replace(searchRegex, (rs, $1) => {  
                                console.log("替换链接名");  
                                // 根据文件名在 map 中找到对应的后缀  
                                let picName = $1.slice($1.lastIndexOf('/') + 1);  
                                let ext = fileExtMap[picName];  
                                return "![](" + myPicBedUrl + picName + "." + ext;  
                            });  
                        } else {  
                            console.log("上传失败");  
                        }  
                        resolve(text);  
                    });  
  
                    req.on('error', error => {  
                        console.error(error);  
                        reject(error);  
                    });  
  
                    req.write(JSON.stringify({list: fileList}));  
                    req.end();  
                });  
            } else {  
                return new Promise((resolve, reject) => {  
                    resolve(text);  
                });  
            }  
        });  
}

该方法调用处也要做修改:

因为 changeOuterLink 方法是异步的,所以调用该方法的地方要加上 await

然后在 rules 上添加新的规则

new Rule('Upload Image', 'Switch', RuleType.MINE, (text, options = {}, app, changeOuterLink) => {  
    return changeOuterLink(text, options, app);  
},[  
], [  
    new TextOption('PicGoUrl', '', "127.0.0.1"), 
    //  PicGoPort 是 picgo 默认的端口号
    new TextOption('PicGoPort', '', "36677"),  
    new TextOption('PicGoPath', '', "/upload"),  
    new TextOption('Host', 'target host', "https://raw.githubusercontent.com"),  
    // Repository 为 github 的仓库名
    new TextOption('Repository', '', "/xxx/xx/"),  
    new TextOption('Image regex', '', '\\!\\[[^\\[\\]]*\\]\\(([a-zA-z]+://[^/]+/[^.?)]+)([^)?]*)')  
])

日常工作流

导出全文

在网页内按 ob,会看到如下画面

可以看到已经添加成功了 👇

根据我个人制定的信息可信度标准(一般是根据文章末尾写的参考文献数量,如果是没写参考,除非是自己的一些经验总结,不然我都认为是抄袭,并且认为其参考价值极低,之所以会剪藏这篇低参考价值的文章仅仅是因为其他文章写得更烂),给文章添加可信等级标识

导出标注

当做了第一个标注后,文章信息框会出现 👇

修改文章信息框内的内容 👇

其中,如果文章不是很可信,那么一定要设置对应的参考价值的标签:

20220521163907.png

信息处理完后给文件做标记

每个网页的信息处理过程如下图的复选框,三个选项都完成后就要给该文件添加一个图标,标识该文件已经处理完成 在阅读文章的过程中,不是很重要的句子就用 加粗,特别重要的句子就用 高亮, 把文章阅读完成后写一个 总结,完成这三步才算真正看完一篇文章

连接视频

展示

video.gif

配置

使用工具

  • Media Extended
    • 使用这个插件能够播放网络视频并能跳转进度条(我已测试能行的网站有 youtube,其他没试过)
  • Media Extended BiliBili Plugin
    • 这个插件是支持播放 bilibili 视频并能跳转进度条的插件
  • Regex Find and Replace
    • 该插件可用于正则表达式查找和替换
  • Language Reactor
    • 这是个 chrome 插件,可以在看 youtube 的时候把英文字幕翻译成中文(或其他语言),并且能选中字幕中的单词查看释义甚至可以把单词加入生词库,在生词库可以使用类似 anki 的复习方式背诵单词

安装 Obsidian 插件

直接在 Obsidian 里搜 Media Extended、Media Extended BiliBili Plugin 和 Regex Find and Replace 就可以下载了

安装 chrome 插件

设置 Regex Find and Replace

修改该插件的源代码

// 匹配 xxs
let regDocumentText = documentText.replace(/(^[0-5]?[0-9])s$/gm, (rs, $1) => {  
    return ">👇[" + $1 + "s](" + replaceString + "#t=" + $1 + ")";  
});  
  
// 匹配 mm:ssregDocumentText = regDocumentText.replace(/(^[0-5]?[0-9]):([0-5][0-9])$/gm, (rs, $1, $2) => {  
    return ">👇[" + $1 + ":" + $2 + "](" + replaceString + "#t=" + ($1 * 60 + $2 * 1) + ")";  
});  
  
// 匹配 hh:mm:ssregDocumentText = regDocumentText.replace(/((^[0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]))$/gm, (rs, $1, $2, $3) => {  
    return ">👇[" + $1 + ":" + $2 + ":" + $3 + "](" + replaceString + "#t=" + ($1 * 60 * 60 + $2 * 60 + $3 * 1) + ")";  
});  
  
editor.setValue(regDocumentText);

第一个正则表达式 ([0-5]?[0-9])s$ 是转换 xxs\n 格式的字符串 第二个正则表达式 ([0-5]?[0-9]):([0-5][0-9]) 是转换 m:ssmm:ss 格式的字符串 第三个正则表达式 (([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]))$ 是转换 hh:mm:ss 格式的字符串 以上统一最终转换为 >👇[$1s](网址#t=$1)

之所以要设置以上正则,是为了匹配 Language Reactor 生成的字幕文件,下面工作流会介绍到

然后设置一下该插件的快捷键:

日常工作流

导出视频的中英字幕

先打开 Language Reactor

20220521214053.png

一开始打开某个视频可能默认是中文字幕,要改成英文的:

点击 设置 查看字幕情况:

(一般没有人类翻译的话相应的单选框是选择不了的) 因为根据上图可以知道这个视频是有人类翻译的,所以导出字幕时可以仅仅导出英文字幕和人类翻译:

接着按 导出 就会弹出字幕的 html:

接着 ctrl + a 全选整个 html 并且 ctrl + c 复制:

新建新的关于这个视频笔记的 markdown 并插入自己设置好的视频模板(其实就只是一些元数据):

接着在这个 markdown 里 ctrl + v 一下,可以看到复制进来了,并且把选中的这三行没用的信息删了:

使用正则替换修改字幕笔记的格式

复制一下视频的地址:

回到刚刚新建的笔记,按 ctrl + alt + f 进行正则替换:

可以看到替换成功了 👇

注意:想跳转视频一定要切换到 查看视图

20220521220838.png

接着你就可以给这个视频的 markdwon 上做笔记了

连接图片

展示

pic.gif

配置

使用工具

  • Excalidraw
    • 该插件可以在 obsidian 中画各种图(有点像 visio)
    • 可以去 这里 大概试用一下

安装 Obsidian 插件

直接在 Obsidian 里搜

日常工作流

在需要跳转到某个笔记时,使用文字工具输入笔记链接就行了(语法和 Obsidian 的一样),如下图:

处理信息

处理信息的方式可以模仿维基百科,毕竟是运转了那么多年的平台,肯定有可取之处 我认为处理信息有几个比较重要的点:

  • 知识点原子化
  • 知识点之间的链接跳转
  • 可信度标识
  • 知识点的转化
  • 知识点的归类

知识点原子化和知识点之间的链接跳转

知识点原子化知识点之间的链接跳转 之间是相辅相成的 比如知识点——红黑树,红黑树是一种自平衡二叉树,红黑树和自平衡二叉树是独立的两篇文章,红黑树这篇文章使用链接的方式跳转到自平衡二叉树,而不要因为红黑树和自平衡二叉树有关就把两者写在一篇文章内

实际应用:

可信度标识

一般我对剪藏进笔记的文章都会添加上如下的可信度标识:(高参考价值的不需要标识) 我简单列一下自己制定的==可信度标准==:

  • 有标明可靠的信息源
    • 直接引用一手信息源
    • 其被引用的信息源有引用一手信息源
  • 文章内容
    • 条理是否清晰
    • 是否有自己的思考,有自己对于一个知识的转化(图、费曼学习法式描述等等)

接着依托于以上的标准,我设置了以下==可信等级==:

  • 高参考价值
    • 标识:无
    • 标准
      • 满足所有可信度的标准
    • 举例
      • 没有修改建议的维基百科条目 如下: 但像下面这个就是有修改建议的:(有修改建议说明该条目有误)
      • 官方文档
      • 经典英文版计算机的书(如黑皮书等好多好多书)
      • 源代码
  • 中参考价值
    • 标识
    • 标准
      • 不满足高参考价值的标准
      • 有标明信息源,但缺少一手资源
  • 低参考价值
    • 标识
    • 标准
      • 不满足中参考价值的标准
      • 没有标明任何信息源
      • 因为自己不懂这方面的知识,而且其他文章写得更烂,所以有那么一点点参考价值
  • 不录入
    • 标识:无
    • 标准
      • 不满足低参考价值的标准
      • 文章写得没有条理,所以不录入我的笔记系统中

为什么要设置可信度标准? 因为网络上的信息良莠不齐,当你引用了这篇文章最好做一个标记,标记这个文章可不可信,如果不标记,等过一段时间回头看就不知道这篇文章靠不靠谱,如果不靠谱还信了里面的内容,那真的是害了自己

知识点的转化

每个网页的信息处理过程如下图的复选框,三个选项都完成后就要给该文件添加一个图标,标识该文件已经处理完成 在阅读文章的过程中,不是很重要的句子就用 加粗,特别重要的句子就用 高亮, 把文章阅读完成后写一个 总结,完成这三步才算真正看完一篇文章 总结 是可以是简单的知识导图,也可以是用自己的话去描述一遍(即费曼学习法)

知识点的归类

在刚新建笔记时,给笔记添加标签,标签的作用就是分类

应试

连接 anki

展示

anki.gif

配置

使用工具

  • Obsidian_to_Anki
    • 该插件可以将笔记内的一段文字导入到 anki,并且导入后成为一个卡片后还能跳转回 obsidian
    • 安装流程

日常工作流

初始创建

在笔记顶部添加:

ctrl + alt + l 格式化整篇笔记(可以同时将图片保存到自己的图床)

alt + s 删除笔记的多余空行

添加卡片

卡片类型有两种:

  • Neuracache flashcard style
    • 即简单的正反面卡片
  • Cloze Paragraph style
    • 即挖空原文的卡片

Neuracache flashcard style

在问题后空格添加标签 #flashcard,在问题下紧接着的一行必须跟上回答

确保问题的顶部和答案的底部都有空行

alt + a 同步到 anki

Cloze Paragraph style

这是完形填空式的笔记,两个空行之间的内容会被放进卡片中

{} 包围住需要挖空的文本,如果文本中有多个需要被挖空的对象,则全体一致使用 {1:文本}(冒号前的数字表示所属的分组,数字相同则属于同一组,同组的会同时被挖空) 出来的效果:

alt + a 同步到 anki

删除卡片

在生成的 id 上一行添加 DELETE

alt + a 同步到 anki

结尾

这篇文章末尾就不写参考了,因为纯纯是我的经验分享,如有雷同是他抄我的。 该给的链接都给了,希望大家不要忘了给那些开源插件点个 star 哦~

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!