之前一直使用Typora+各种博客(Wordpress/Hexo/Typecho)来进行笔记和写作,后来接触并爱上了语雀,主要是贴图太方便了。(使用Typora的时候会搭配PicGo+云存储,但是有时候会粘贴了多余的图片或者想替换已有图片时,懒得打开云存储进行删除,久而久之就忘了,造成了一定的空间浪费。)
刚开始用语雀的时候还特地看了下,可以导出md格式的文章。但最近想批量导出知识库时,发现只能选择PDF或者语雀特定的格式,数据不在自己手里感觉不大放心。于是弄了个脚本通过语雀官方API导出了全部文章,并开始寻找本地存储的笔记软件。
结合个人情况进行筛选后发现Obisidian比较适合,但是一开始不会用,不会怎么处理图片路径的问题。语雀是没有目录这个概念的,所以导出的文章都放到了一起,然后图片等资源也统一放到了文章目录中的某一目录。而如果我在Obsidian里通过建立多级文件夹的方式来分类文章,那么所有图片资源的链接都要进行改动,差点弃坑了。还好在B站看了关于ob的视频,学到了通过索引的方式来进行管理。
先上一张Obisidian的图
./assets/修改为assets/
,用于匹配Obsidianhttps://www.yuque.com/<xxx>
中的xxx
部分$ python3 ExportMD.py
> 请输入语雀namespace: xxx
> 请输入语雀Token: xxx
ExportMD.py
完整代码:# -*- coding: UTF-8 -*-
from prettytable import PrettyTable
import re
import os
import aiohttp
import asyncio
from urllib import parse
from PyInquirer import prompt, Separator
from examples import custom_style_2
from colr import color
from cfonts import render, say
class ExportMD:
def __init__(self):
self.repo_table = PrettyTable(["知识库ID", "名称"])
self.namespace, self.Token = self.get_UserInfo()
self.headers = {
"Content-Type": "application/json",
"User-Agent": "ExportMD",
"X-Auth-Token": self.Token
}
self.repo = {}
self.export_dir = './yuque'
def print_logo(self):
output = render('ExportMD', colors=['red', 'yellow'], align='center')
print(output)
# 语雀用户信息
def get_UserInfo(self):
f_name = ".userinfo"
if os.path.isfile(f_name):
with open(f_name, encoding="utf-8") as f:
userinfo = f.read().split("&")
else:
namespace = input("请输入语雀namespace:")
Token = input("请输入语雀Token:")
userinfo = [namespace, Token]
with open(f_name, "w") as f:
f.write(namespace + "&" + Token)
return userinfo
# 发送请求
async def req(self, session, api):
url = "https://www.yuque.com/api/v2" + api
# print(url)
async with session.get(url, headers=self.headers) as resp:
result = await resp.json()
return result
# 获取所有知识库
async def getRepo(self):
api = "/users/%s/repos" % self.namespace
async with aiohttp.ClientSession() as session:
result = await self.req(session, api)
for repo in result.get('data'):
repo_id = str(repo['id'])
repo_name = repo['name']
self.repo[repo_name] = repo_id
self.repo_table.add_row([repo_id, repo_name])
# 获取一个知识库的文档列表
async def get_docs(self, repo_id):
api = "/repos/%s/docs" % repo_id
async with aiohttp.ClientSession() as session:
result = await self.req(session, api)
docs = {}
for doc in result.get('data'):
title = doc['title']
slug = doc['slug']
docs[slug] = title
return docs
# 获取正文 Markdown 源代码
async def get_body(self, repo_id, slug):
api = "/repos/%s/docs/%s" % (repo_id, slug)
async with aiohttp.ClientSession() as session:
result = await self.req(session, api)
body = result['data']['body']
body = re.sub("<a name=\".*\"></a>","", body) # 正则去除语雀导出的<a>标签
return body
# 选择知识库
def selectRepo(self):
choices = [{"name": repo_name} for repo_name, _ in self.repo.items()]
choices.insert(0, Separator('=== 知识库列表 ==='))
questions = [
{
'type': 'checkbox',
'qmark': '>>>',
'message': '选择知识库',
'name': 'repo',
'choices': choices
}
]
repo_name_list = prompt(questions, style=custom_style_2)
return repo_name_list["repo"]
# 创建文件夹
def mkDir(self, dir):
isExists = os.path.exists(dir)
if not isExists:
os.makedirs(dir)
# 获取文章并执行保存
async def download_md(self, repo_id, slug, repo_name, title):
"""
:param repo_id: 知识库id
:param slug: 文章id
:param repo_name: 知识库名称
:param title: 文章名称
:return: none
"""
body = await self.get_body(repo_id, slug)
new_body, image_list = await self.to_local_image_src(body)
if image_list:
# 图片保存位置: .yuque/<repo_name>/assets/<filename>
save_dir = os.path.join(self.export_dir, repo_name, "assets")
self.mkDir(save_dir)
async with aiohttp.ClientSession() as session:
await asyncio.gather(
*(self.download_image(session, image_info, save_dir) for image_info in image_list)
)
self.save(repo_name, title, new_body)
print("%s 导出成功!" % color(title, fore='green', style='bright'))
# 将md里的图片地址替换成本地的图片地址
async def to_local_image_src(self, body):
pattern = r"!\[(?P<img_name>.*?)\]" \
r"\((?P<img_src>https:\/\/cdn\.nlark\.com\/yuque.*\/(?P<slug>\d+)\/(?P<filename>.*?\.[a-zA-z]+)).*\)"
# repl = r""
repl = r"" # 修改
images = [_.groupdict() for _ in re.finditer(pattern, body)]
new_body = re.sub(pattern, repl, body)
# new_body = body
return new_body, images
# 下载图片
async def download_image(self, session, image_info: dict, save_dir: str):
img_src = image_info['img_src']
filename = image_info["filename"]
async with session.get(img_src) as resp:
with open(os.path.join(save_dir, filename), 'wb') as f:
f.write(await resp.read())
# 保存文章
def save(self, repo_name, title, body):
# 将不能作为文件名的字符进行编码
def check_safe_path(path: str):
for char in r'/\<>?:"|*':
path = path.replace(char, parse.quote_plus(char))
return path
repo_name = check_safe_path(repo_name)
title = check_safe_path(title)
save_path = "./yuque/%s/%s.md" % (repo_name, title)
with open(save_path, "w", encoding="utf-8") as f:
f.write(body)
async def run(self):
self.print_logo()
await self.getRepo()
repo_name_list = self.selectRepo()
self.mkDir(self.export_dir) # 创建用于存储知识库文章的文件夹
# 遍历所选知识库
for repo_name in repo_name_list:
dir_path = self.export_dir + "/" + repo_name.replace("/", "%2F")
dir_path.replace("//", "/")
self.mkDir(dir_path)
repo_id = self.repo[repo_name]
docs = await self.get_docs(repo_id)
await asyncio.gather(
*(self.download_md(repo_id, slug, repo_name, title) for slug, title in docs.items())
)
print("\n" + color('导出完成!', fore='green', style='bright'))
print("已导出到:" + color(os.path.realpath(self.export_dir), fore='green', style='bright'))
if __name__ == '__main__':
export = ExportMD()
loop = asyncio.get_event_loop()
loop.run_until_complete(export.run())
$ ulimit -n # 查看当前最大打开数文件
$ ulimit -n 512 # 设置多一点
导出时发现一个小Bug:通过上面脚本导出文档时,如果文档中存在2张连着的图片时,如
pic_a
和pic_b
,那么会重复导出2次pic_a
,并将第二张重命名为pic_b
。应该是正则没匹配好的问题。
当大量文档中存在以上Bug时,解决方法如下:
AAA/
目录,使用前面的ExportMD.py
脚本下载全部文档BBB/
目录,注释ExportMD.py
中的139
行,并打开140
行# new_body = re.sub(pattern, repl, body) # 注释此行
new_body = body # 打开此行
└── AAA/
└── yuque/
└── assets/ # 图片文件夹
└── ... # MarkDown文档
└── BBB/
└── yuque/
└── assets/ # 图片文件夹
└── ... # MarkDown文档
AAA/assets/
这个图片文件夹删除,并将BBB/assets/
图片文件夹移动到AAA/
即可解释一下: 第一次在
AAA/
目录下载的文档中的链接格式为:assets/xxx.png
,这里部分图片存在上面所说的Bug; 而第二次在BBB/
目录下载的文档中的链接格式为:https://cdn.nlark.com/yuque/xxx.png
,这里的图片链接没经过正则匹配修改,是语雀文档中图片的原生链接; 所以这里先把2种链接格式的文档都下载下来,然后通过脚本从原生链接中下载图片,并替换掉存在Bug那部分的图片。
Download_Yuque_imgs.py
完整代码:# python3 Download_Yuque_imgs.py <Markdown文件路径>
# -*- coding: utf-8 -*-
import os
import re
import sys
from urllib.request import urlretrieve
# 读取markdown文件中全部图片URL
def get_urls(file_path):
print(f"[+] 获取 {file_path} 全部图片链接 ... ")
imgs = []
with open(file_path, encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
img = re.findall(r"!\[image.png\]\((.*?\.png)", line, re.S)
if img:
imgs.append(img[0])
return imgs
# 遍历URL下载并存放到图片目录下
def save_imgs(imgs):
index = 1
for img_url in imgs:
try:
img_name = re.findall(r"520228\/(.*?\.png)", img_url)[0]
urlretrieve(img_url, f'./{img_name}')
print(f"[+] <{index}/{len(imgs)}> {img_name}")
index += 1
except Exception as e:
print(f"!!! [-] <{index}/{len(imgs)}> {img_name} !!!")
continue
if __name__ == '__main__':
path = sys.argv[1]
files = list(filter(lambda x: x[-3:]==".md", os.listdir(path)))
for file in files:
file_path = os.path.join(path, file)
imgs = get_urls(file_path)
if(imgs):
save_imgs(imgs)
print(f"====== 执行完成: {file} ======")
这里以语雀目录为内容,批量添加
obsidian
的内链格式[[xxx]]
,以建立索引
file = "list.txt"
new_file = "list2.txt"
datas = []
with open(file, "r") as f:
lines = f.readlines()
for line in lines:
data = "[[" + line.strip() + "]]"
datas.append(data)
with open(new_file, "w") as f2:
for line in datas:
f2.writelines(line + "\n")
版权属于:Naraku
本文链接:https://cloud.tencent.com/developer/article/1937153
本站所有原创文章均采用 知识共享署名-非商业-禁止演绎4.0国际许可证 。如需转载请务必注明出处并保留原文链接,谢谢~