目录
- 前言
- 有道云笔记导出
- 有道云笔记信息导出
- Python2和Python3共存
- 修改原代码
- 执行程序
- 一些可能遇到的问题
- 修改markdown文件的创建时间和修改时间
- 安装最新版powershell(版本7.1以上)
- 修改powershell编码(代码页)
- 编写脚本
- 使用脚本
- 一些可能遇到的问题
- 结语
前言
前段时间突然发现,有道云笔记禁止导出笔记了。
以前好歹还是可以批量导出的,虽然只能导出成有道云的独有格式或者是pdf,但是好歹还是能导出的,结果现在是彻底禁止导出了。
去搜索了下有道云笔记导出,没想到真的有第三方的插件可以把有道云笔记里的笔记导出成markdown文件(下称md文件),再加上我最近正好接触到了一个免费笔记管理软件Obsidian,虽然这个软件没有云备份功能,但是他的文件都是本地存储,且是通用的markdown格式,也方便未来万一需要换软件的情况。(国内的大部分软件,就算能导出也都是独有格式,猜测是为了防止用户跑路吧)
思索之后,决定趁现在赶紧把有道云笔记里的东西迁移出来,防止之后彻底无法导出就完蛋了。
于是就有了这个教程。
这个教程主要分为两部分:
- 导出有道云笔记里的内容为markdown文件
- 修改导出的文件的创建时间和修改时间,与有道云笔记内匹配。
当然第二点只是针对于有强迫症的用户,对于一般用户来说,这点完全可以不用考虑。
那么总之,教程开始。
有道云笔记导出
关于有道云笔记导出为md文件,其实github上已经有一个相当好的软件了:youdaonote-pull
这个软件的功能有:
- 可将所有笔记(文件)按原格式下载到本地
- 由于「笔记」类型文件下载后默认为 Xml 格式,不是正常笔记内容,默认将其转换为 Markdown 格式
- 由于有道云笔记图床图片不能在有道云笔记外显示,默认将其下载到本地,或指定上传到 SM.MS
嗯……具体的使用方法在原网页上写的非常详细,我在这里就不搬了,大家自己去原网页看就行了。
我在这里就说两点:
- 原网页里的第一步,如果没有git,也不想下载的话,可以不用按照原网页里说的使用git clone项目,只需要点击code内的Download ZIP按钮,即可下载全部文件,然后在本机上解压即可使用。
- 如果在软件执行的过程中出现
转换为 Markdown 失败!请检查文件!
的错误,请自行检查文件名是否出错。(我自己是因为有道云的文件名里不知道为啥会包含两个换行符,所以报错了)
执行完这个程序,大家就能得到一个包含所有嵌套文件夹、笔记、图片文件(在根目录下的youdaonote-images文件夹内)、附件文件(在根目录下的youdaonote-attachments文件夹内)的文件夹了。
这个文件夹其实已经可以当成Obsidian的库被识别了,只需要在Obsidian内选择『打开库文件夹』,选择这个导出的根目录,就可以使用了。
如果没有强迫症的话,这个教程看到这里就结束了。但是,我有强迫症啊!
有道云笔记里,每一篇笔记都会记录他的创建时间和修改时间,但是使用youdaonote-pull导出的笔记,创建时间全部为导出的当天,这样就丢失了重要的时间信息。
于是才有了接下来的教程。
有道云笔记信息导出
如果想要导出有道云笔记的信息(如时间等),就需要另一个软件了:YoudaoNoteExport
这个软件稍微有点年头了,2018年的,但是仍然可以使用。他的主要功能是导出有道云笔记,保存为JSON和DOCX/XML文件。DOCX/XML文件是笔记的内容,JSON文件是笔记的其它信息(包括标题、创建时间、修改时间等)。
我们正好需要这个软件导出的JSON文件,里面包含了文件的创建时间和修改时间。(这个软件导出DOCX有点问题,我用word无法打开,不过无所谓了,我们也不需要这个功能。)
这个软件导出的笔记默认文件名是笔记的id(一串数字和字母构成的字符串),我本来想把它改成以原笔记的名称作为文件名,但是遇到了一些问题,所以最后还是保留了id文件名,反正到时候遍历json文件的时候也不需要看这个文件名。
Python2和Python3共存
但是因为这个软件运行的环境是在Python2.7里的,而之前的youdaonote-pull是运行在Python3内的,这里就有一个问题就是需要Python2和Python3在同一个电脑内共存。
这个问题可以参考下面这两篇文章:
Windows10下python3和python2同时安装(一)安装python3和python2
Windows10下python3和python2同时安装(二)python2.exe、python3.exe和pip2、pip3设置
整体的做法按照这两篇文章内的设置就行,不过实际上,我自己只修改了python2和pip2。也就是命令python对应的是python3,而python2对应的就是python2,pip同理。(因为我准备做完这个导出就把python2卸载,然后保留python3的,所以我python3就没有做修改。)
总之不管怎么修改,现在的电脑内已经有了python2和python3两个版本了。不过在下载之前,还需要在python2内也安装requests库,如果大家按照之前两篇文章更改的话,那么安装的方式就是在命令提示符内输入:
pip2 install requests
然后接下来就是下载YoudaoNoteExport,和之前一样,只需要点击code里的Download ZIP下载所有文件,然后解压,得到一个名为YoudaoNoteExport-master的文件夹。
修改原代码
在使用之前,还需要对源码做一些修改。
也不知道为啥,下载下来的源码如果直接使用,会有非常多的错误,这也是我之前导出的时候遇到的问题。所以要对原代码进行一些修改。
打开之前解压的YoudaoNoteExport-master文件夹,然后右键main.py文件,选择Edit with IDLE,或者打开方式记事本,然后参照下文修改程序即可。(建议安装一个sublime编辑器)
这个程序的问题主要集中在函数getFileRecursively
内,代码如下:
def getFileRecursively(self, id, saveDir, doc_type):
data = {
'path': '/',
'dirOnly': 'false',
'f': 'false',
'cstk': self.cstk
}
url = 'https://note.youdao.com/yws/api/personal/file/%s?all=true&f=true&len=30&sort=1&isReverse=false&method=listPageByParentId&keyfrom=web&cstk=%s' % (id, self.cstk)
lastId = None
count = 0
total = 1
while count < total:
if lastId == None:
response = self.get(url)
else:
response = self.get(url + '&lastId=%s' % lastId)
print('\n------')
# print('getFileRecursively:' + response.content)
jsonObj = json.loads(response.content)
total = jsonObj['count']
for entry in jsonObj['entries']:
fileEntry = entry['fileEntry']
id = fileEntry['id']
name = fileEntry['name']
print('%s' % (id))
# print('%s %s' % (id, name))
if fileEntry['dir']:
subDir = saveDir + '/' + name
try:
os.lstat(subDir)
except OSError:
os.mkdir(subDir)
self.getFileRecursively(id, subDir, doc_type)
else:
with open('%s/%s.json' % (saveDir, id), 'w') as fp:
fp.write(json.dumps(entry,ensure_ascii=False).encode('utf-8'))
if doc_type == 'xml':
self.getNote(id, saveDir)
else: # docx
self.getNoteDocx(id, saveDir)
count = count + 1
lastId = id
需要修改的地方我都已经修改掉了,并且把原句注释了。
总共有两句需要修改:
第一句
# 原句:
print('getFileRecursively:' + response.content)
# 修改成print('\n------')
修改这句的原因是因为,原程序这里输出了返回内容,但是猜测是因为返回内容过于巨大,实际输出之后会报错IOError: [Errno 0] Error
。
Traceback (most recent call last):
File "main.py", line 137, in <module>
sess.getAll(saveDir, doc_type)
File "main.py", line 118, in getAll
self.getFileRecursively(rootId, saveDir, doc_type)
File "main.py", line 91, in getFileRecursively
print('getFileRecursively:' + response.content)
IOError: [Errno 0] Error
实测把这句直接注释掉,或者改成一些人畜无害的分隔线之类的就可以了。
第二句
# 原句:
print('%s %s' % (id, name))
# 修改成print('%s' % (id))
这句的问题是,最后输出了有道云笔记的文件名。
本来是没什么问题的,而且也能让人看得更清楚,但是笔记的文件名里如果有一些奇怪的Unicode字符(比如⊊),这里就会报错UnicodeEncodeError
。
Traceback (most recent call last):
File "main.py", line 138, in <module>
sess.getAll(saveDir, doc_type)
File "main.py", line 119, in getAll
self.getFileRecursively(rootId, saveDir, doc_type)
File "main.py", line 106, in getFileRecursively
self.getFileRecursively(id, subDir, doc_type)
File "main.py", line 106, in getFileRecursively
self.getFileRecursively(id, subDir, doc_type)
File "main.py", line 99, in getFileRecursively
print('%s %s' % (id, name))
UnicodeEncodeError: 'gbk' codec can't encode character u'\u228a' in position 35: illegal multibyte sequence
处理方法的话……我是直接把这段的name给删了,简单粗暴。虽然看不清文件名了但是好歹不会出问题了。大佬的话可以把这段稍微修改下……
执行程序
修改完程序,保存之后,就可以执行了。
- 首先打开命令提示符,将路径移动到之前解压的YoudaoNoteExport-master文件夹内,如:
D:\桌面\YoudaoNoteExport-master>
- 接着需要在YoudaoNoteExport-master文件夹内,新建一个notes文件夹。(这个文件夹名称可以随意,但是需要和下文的输入的路径统一)
- 在命令提示符内输入如下命令:
D:\桌面\YoudaoNoteExport-master>python2 main.py <有道云的账号> <有道云的密码> ./notes xml
接着等待程序自动执行完成即可。
一些可能遇到的问题
-
报错:
IOError: [Errno 0] Error
这个之前提到过,修改上文的第一句应该就行了。
-
报错:
UnicodeEncodeError: 'gbk' codec can't encode character u'\uxxxx' in position xxx: illegal multibyte sequence
也是之前提到过的,修改上文的第二句应该就行了。
-
报错:
IOError: [Errno 22] invalid mode ('w') or filename: u'./notes/xxxx.json'
这个错误是我之前想让程序以原笔记标题作为文件名,然后才遇到的问题。还是上文说的,不知道为啥我有个笔记标题里有两个换行符,所以导出的时候直接就报错了,后来我还是以id作为文件名就不报错了。
-
报错:
KeyError: 'fileEntry'
这个错误会显示如下的信息:
getRoot:{"canTryAgain":false,"scope":"SECURITY","error":"207","message":"Message[AUTHENTICATION_FAILURE]: User token must be authenticated.","objectUser":null} Traceback (most recent call last): File "main.py", line 138, in <module> sess.getAll(saveDir, doc_type) File "main.py", line 118, in getAll rootId = self.getRoot() File "main.py", line 53, in getRoot return jsonObj['fileEntry']['id'] KeyError: 'fileEntry'
这个错误其实是因为登录次数太多,网页需要输入验证码了,没有验证码就相当于登不上去,所以返回的页面直接报错。
解决办法……就是再过一天再登录,应该就好了。
修改markdown文件的创建时间和修改时间
经过了前两步,我们现在已经得到了两个文件夹,分别是由youdaonote-pull导出的装满markdown的文件夹,和YoudaoNoteExport导出的装满json文件的文件夹。
接下来要做的,就是将json文件中的创建时间、修改时间提取出来,再对md文件的属性进行更改。
这里我们选择windows10自带的powershell来完成这个工作,因为powershell对于文件基础属性的修改操作起来比较简单。
先分析一下YoudaoNoteExport导出的json文件:(实际导出的文件为仅有一行的json文件,为了看得清楚我已经格式化了)
{
"otherProp": {},
"fileMeta": {
"metaProps": {
"WHOLE_FILE_TYPE": "NOS",
"FILE_IDENTIFIER": "xxxx",
"spaceused": "2213",
"tp": "0",
"st": "0"
},
"sourceURL": "",
"contentType": null,
"author": "",
"modifyTimeForSort": 1548934186, # 修改时间
"title": "xxxx.note", # 文件名
"sharedCount": 0,
"externalDownload": [],
"storeAsWholeFile": true,
"fileSize": 2213,
"resourceMime": null,
"resourceName": null,
"chunkList": null,
"coopNoteVersion": 0,
"resources": [],
"createTimeForSort": 1547818247 # 创建时间
},
"fileEntry": {
"domain": 0,
"createProduct": null,
"hasComment": false,
"modifyTimeForSort": 1548934225, # 修改时间
"userId": "xxxxxx",
"myKeepAuthorV2": "",
"transactionTime": 1548934225,
"myKeepAuthor": "",
"id": "xxxx",
"erased": false,
"entryType": 0,
"orgEditorType": 1,
"version": 15899,
"entryProps": {
"orgEditorType": "1",
"encrypted": "false",
"modId": "xxxx",
"bgImageId": "d-00",
"PE_IMPORTED": "false"
},
"createTimeForSort": 1547818258, # 创建时间
"parentId": "xxxx",
"favorited": false,
"noteSourceType": 0,
"subTreeFileNum": 0,
"tags": "",
"deleted": false,
"subTreeDirNum": 0,
"myKeepV2": false,
"noteType": "0",
"fileSize": 2213,
"modDeviceId": "xxxx",
"financeNote": null,
"noteTextSize": 0,
"myKeep": false,
"name": "xxxx.note", # 文件名
"checksum": "xxxx",
"summary": "xxxxxxxxxxxx",
"dirNum": 0,
"rightOfControl": 0,
"namePath": null,
"transactionId": "xxxx",
"publicShared": false,
"fileNum": 0,
"dir": false
},
"ocrHitInfo": []
}
可以看见fileMeta
里的title
字段的值,就是我们需要的文件名,而createTimeForSort
和modifyTimeForSort
就是文件的创建时间和修改时间。
注意:fileEntry
里也有文件名(name字段),创建时间和修改时间,只需要选择一个来当做源数据即可,这里我们选择的是fileMeta
里的。
因为两个软件导出的文件,相同笔记对应的路径都是相同的,所以整个powershell脚本的思路就是遍历所有的json,接着针对每一个json文件,提取出其中的文件名。
因为之前导出的笔记已经全部转换成了.md文件,所以这里就需要将文件名内的.note全部改为.md,其他后缀名不变。
而有了这个文件名之后,就可以到youdaonote-pull导出的文件夹里,找到对应的笔记文件。
然后提取createTimeForSort
和modifyTimeForSort
的值,这个值是一个时间戳,需要将它们转换成powershell可以直接读取的时间格式,接着修改对应的.md文件的创建时间和修改时间就行了。
安装最新版powershell(版本7.1以上)
不过想要实现这个功能,还有一个问题,那就是时间戳的转换。虽然powershell里自带的Get-Date
命令可以转换时间戳,但是这个功能需要在powershell 7.1以上才能使用,所以我们还需要安装一下powershell 7.1。
这个的安装比较简单,只需要在win10自带的应用商店里,搜索powershell,然后安装就行了。
这样安装的应该就是最新版的powershell了,目前2022年2月,安装的版本是7.2.1。(我win10自带的版本是5.1,不能转换时间戳)
注:查看powershell版本的方法:在powershell窗口内输入命令$PSVersionTable.PSVersion
,然后得到的major、minor、patch三项的值合起来就是powershell的版本。
注2:如果windows没有应用商店,或者连不上应用商店,可以选择去GitHub上下载PowerShell,版本的话,可以根据自己的系统选择PowerShell-7.3.0-preview.1-win-x86.msi(32位系统)或者PowerShell-7.3.0-preview.1-win-x64.msi(64位系统)
安装完成之后,开始菜单里应该会有一个powershell程序,右键它,点击更多,然后以管理员身份运行,就可以打开powershell了。(其实也可以不以管理员身份运行,但是以管理员身份比较保险)
修改powershell编码(代码页)
不过在使用powershell之前,还需要设置powershell的编码(代码页)。因为powershell默认使用的代码页是936,中文会显示乱码,然后就会导致json里有中文时执行程序报错Cannot find path '<文件路径>' because it does not exist.
。
这个问题的解决方法可以参考这篇文章:解决windowspowershell中文显示问号及乱码问题
注:想要查看当前代码页,可以右击powershell窗口的标题栏,然后点击属性里的『选项』选项卡,在下方可以看当前代码页的值。这里默认写的是936 (ANSI/OEM - 简体中文 GBK),需要将它改成65001 (UTF-8)。
编写脚本
根据之前的思路,我写了一个powershell脚本。
# 批量修改有道云笔记导出文件的创建时间和修改时间,使之与有道云笔记内部统一的小程序
# 因为YoudaoNoteExport导出的json不含文件夹的创建、修改时间
# 所以只修改了所有文件的创建时间、修改时间、访问时间,但是文件夹的没有修改
$jsonPath = "D:\Obsidian\3" # YoudaoNoteExport导出文件的根目录(json文件目录)
$mdPath = "D:\Obsidian\4" # youdaonote-pull导出文件的根目录(md文件目录)
# 自己使用时注意把 $pathJson 和 $mdPath 改成自己的路径
Get-Childitem -Path $jsonPath -Recurse -Include *.json | Foreach-Object { # 遍历所有导出的json
$jsonContent = Get-Content $_ | ConvertFrom-Json # 转换json成powershell能识别的格式
$fileName = $jsonContent.fileMeta.title # 取出json里的title属性(文件名)
if ($fileName -ne $null) { # 防止取不到title属性报错
if ($fileName.EndsWith(".note")) {
# 搞这么复杂是防止文件名中出现".note",然后一用replace就全替换没了,所以只替换文件名最后的".note"(虽然是小概率事件……)
$fileName = ($fileName.Remove($fileName.LastIndexOf(".note") , 5) + ".md") # 把json里以.note结尾的文件名改成.md
}
$mdFile = Get-Item ($_.DirectoryName.Replace($jsonPath , $mdPath) + "\" + $fileName) # 替换对应路径。DirectoryName取出的路径没有\,所以这边要补上
Write-Host ("正在修改:" + $_.BaseName + ",路径:" + $mdFile.FullName + "`n") # 最后加一个换行是因为不知道为啥经常会两行输出在一起……
if ($jsonContent.fileMeta.createTimeForSort -ne $null) { # 防止取不到createTimeForSort报错
$mdFile.CreationTime = Get-Date -UnixTimeSeconds $jsonContent.fileMeta.createTimeForSort # 修改创建时间
}
if ($jsonContent.fileMeta.modifyTimeForSort -ne $null) { # 防止取不到modifyTimeForSort报错
$mdFile.LastWriteTime = Get-Date -UnixTimeSeconds $jsonContent.fileMeta.modifyTimeForSort # 修改修改时间
$mdFile.LastAccessTime = Get-Date -UnixTimeSeconds $jsonContent.fileMeta.modifyTimeForSort # 修改最后访问时间,和修改时间相同
}
}
else {
Write-Host ("【警告】" + $_.Name + " 缺少title属性,无法处理。`n")
}
}
使用脚本
首先需要注意一下,大家如果想要使用这个脚本,注意把最前面的$pathJson
和$mdPath
两个变量的值改成自己对应的路径(注意最后不要带\)。
然后使用脚本,有两种方式:
- 将脚本代码修改之后,直接全选复制,然后在powershell界面里点击鼠标右键粘贴,之后回车执行代码。
- 新建一个文本文档,将代码复制进文本文档,修改之后保存,接着把文件的后缀名改成.ps1,进入powershell界面执行。
不过第二种方式操作起来比较麻烦,还需要修改powershell执行脚本的权限,我就不多做介绍了,仅在此贴一篇文章供大家参考:Powershell实现编写和运行脚本。这篇文章里把powershell运行脚本遇到的一些问题和解决方法都写出来了,如果实在是需要使用脚本文件来执行代码,可以参考这篇文章。
一些可能遇到的问题
- 报错:
ConvertFrom-Json : 传入的对象无效,应为“:”或“}”。
ConvertFrom-Json : 传入的对象无效,应为“:”或“}”。
<这里是json内容>
所在位置 行:2 字符: 37
+ $JsonContent = Get-Content $_ | ConvertFrom-Json
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [ConvertFrom-Json], ArgumentException
+ FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand
这个错误上文说过,是因为powershell使用的编码不支持中文造成的,解决方法可以参考这篇文章:解决windowspowershell中文显示问号及乱码问题
- 报错:
Cannot find path '<文件路径>' because it does not exist.
Get-Item:
Line |
10 | … $mdFile = Get-Item ($_.DirectoryName.Replace($jsonPath , $mdPath) + …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot find path '<文件路径>' because it does not exist.
正在修改:<文件名称>,路径:
InvalidOperation:
Line |
13 | $mdFile.CreationTime = Get-Date -UnixTimeSeconds $jsonCon …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The property 'CreationTime' cannot be found on this object. Verify that the property exists and can be set.
InvalidOperation:
Line |
17 | $mdFile.LastWriteTime = Get-Date -UnixTimeSeconds $jsonCo …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The property 'LastWriteTime' cannot be found on this object. Verify that the property exists and can be set.
InvalidOperation:
Line |
19 | $mdFile.LastAccessTime = Get-Date -UnixTimeSeconds $jsonC …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The property 'LastAccessTime' cannot be found on this object. Verify that the property exists and can be set.
这个错误虽然错误信息这么长,核心问题其实是从YoudaoNoteExport导出的json文件里提取出文件名之后,在youdaonote-pull导出的相应文件夹内找不到对应的文件(一般为.md文件)。
错误产生的原因一般而言是json里的文件名和实际导出的文件名不相同造成的,比如文件名里有一些奇怪的字符,或者换行符(我就遇到过这两种情况),而转换成.md文件的时候这些奇怪的字符被删掉了,于是两边的文件名就不匹配了。
这时候只需要修改一下json内的title
字段,删除掉那些有错误的文件名,或者直接将文件名改成和对应笔记相同即可。具体哪个json文件出错了,可以查看错误信息里的正在修改:<文件名称>
里的文件名称找到对应的json文件。
不过有几点要注意:
- 修改的时候要修改
fileMeta
下的title
字段,因为原json文件里有两个地方都有文件名,一个是fileMeta
下的title
字段,另一个是fileEntry
下的name
字段,注意要修改前者。 - 修改的软件可以使用记事本,但是建议使用sublime编辑器,如果json文件里有一些不显示的字符,这里面可以显示出来。
- 因为原本的json文件是压缩成一行的,如果觉得寻找
title
字段比较困难,可以使用JOSN格式化网站来使json更加的可读,也可以在网页上直接修改之后再复制到原json文件里保存。
结语
那么到这里整个教程就算是结束了,如果操作没有问题的话,导出的笔记的创建时间应该已经和有道云内的创建时间同步了。
接下来只需要使用Obsidian打开这个文件夹作为库,就可以完美使用了。
不过最后要注意一点:如果想要移动这个文件夹的位置请直接使用剪切和粘贴,如果复制的话前面的工作就全部功亏一篑了。
最后感谢大家的阅读。
Roi写于2022年2月2日。
转载请注明出处。