├── LICENSE ├── README.md └── main.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wesley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YoudaoNoteExport 2 | 导出有道云笔记,保存为JSON和DOCX/XML文件。DOCX/XML文件是笔记的内容,JSON文件是笔记的其它信息(包括标题、创建时间、修改时间等) 3 | 4 | 使用方法: 5 | 6 | python main.py 用户名 密码 [存盘目录 [文件类型]] 7 | 8 | 文件类型是xml(默认)或docx 9 | 10 | 11 | 举例: 12 | 13 | mkdir notes 14 | 15 | python main.py 用户名 密码 ./notes docx 16 | 17 | 18 | 在Ubuntu 16.04 + Python 2.7的环境中测试通过。 19 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import requests 4 | import sys 5 | import time 6 | import hashlib 7 | import os 8 | from requests.cookies import create_cookie 9 | import json 10 | 11 | def timestamp(): 12 | return str(int(time.time() * 1000)) 13 | 14 | class YoudaoNoteSession(requests.Session): 15 | def __init__(self): 16 | requests.Session.__init__(self) 17 | self.headers = { 18 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36', 19 | 'Accept': '*/*', 20 | 'Accept-Encoding':'gzip, deflate, br', 21 | 'Accept-Language':'zh-CN,zh;q=0.9,en;q=0.8' 22 | } 23 | 24 | def login(self, username, password): 25 | self.get('https://note.youdao.com/web/') 26 | 27 | self.headers['Referer'] = 'https://note.youdao.com/web/' 28 | self.get('https://note.youdao.com/signIn/index.html?&callback=https%3A%2F%2Fnote.youdao.com%2Fweb%2F&from=web') 29 | 30 | self.headers['Referer'] = 'https://note.youdao.com/signIn/index.html?&callback=https%3A%2F%2Fnote.youdao.com%2Fweb%2F&from=web' 31 | self.get('https://note.youdao.com/login/acc/pe/getsess?product=YNOTE&_=' + timestamp()) 32 | self.get('https://note.youdao.com/auth/cq.json?app=web&_=' + timestamp()) 33 | self.get('https://note.youdao.com/auth/urs/login.json?app=web&_=' + timestamp()) 34 | data = { 35 | "username": username, 36 | "password": hashlib.md5(password).hexdigest() 37 | } 38 | self.post('https://note.youdao.com/login/acc/urs/verify/check?app=web&product=YNOTE&tp=urstoken&cf=6&fr=1&systemName=&deviceType=&ru=https%3A%2F%2Fnote.youdao.com%2FsignIn%2F%2FloginCallback.html&er=https%3A%2F%2Fnote.youdao.com%2FsignIn%2F%2FloginCallback.html&vcode=&systemName=&deviceType=×tamp=' + timestamp(), data=data, allow_redirects=True) 39 | self.get('https://note.youdao.com/yws/mapi/user?method=get&multilevelEnable=true&_=' + timestamp()) 40 | print(self.cookies) 41 | self.cstk = self.cookies.get('YNOTE_CSTK') 42 | 43 | def getRoot(self): 44 | data = { 45 | 'path': '/', 46 | 'entire': 'true', 47 | 'purge': 'false', 48 | 'cstk': self.cstk 49 | } 50 | response = self.post('https://note.youdao.com/yws/api/personal/file?method=getByPath&keyfrom=web&cstk=%s' % self.cstk, data = data) 51 | print('getRoot:' + response.content) 52 | jsonObj = json.loads(response.content) 53 | return jsonObj['fileEntry']['id'] 54 | 55 | def getNote(self, id, saveDir): 56 | data = { 57 | 'fileId': id, 58 | 'version': -1, 59 | 'convert': 'true', 60 | 'editorType': 1, 61 | 'cstk': self.cstk 62 | } 63 | url = 'https://note.youdao.com/yws/api/personal/sync?method=download&keyfrom=web&cstk=%s' % self.cstk 64 | response = self.post(url, data = data) 65 | with open('%s/%s.xml' % (saveDir, id), 'w') as fp: 66 | fp.write(response.content) 67 | 68 | def getNoteDocx(self, id, saveDir): 69 | url = 'https://note.youdao.com/ydoc/api/personal/doc?method=download-docx&fileId=%s&cstk=%s&keyfrom=web' % (id, self.cstk) 70 | response = self.get(url) 71 | with open('%s/%s.docx' % (saveDir, id), 'w') as fp: 72 | fp.write(response.content) 73 | 74 | def getFileRecursively(self, id, saveDir, doc_type): 75 | data = { 76 | 'path': '/', 77 | 'dirOnly': 'false', 78 | 'f': 'false', 79 | 'cstk': self.cstk 80 | } 81 | 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) 82 | lastId = None 83 | count = 0 84 | total = 1 85 | while count < total: 86 | if lastId == None: 87 | response = self.get(url) 88 | else: 89 | response = self.get(url + '&lastId=%s' % lastId) 90 | print('getFileRecursively:' + response.content) 91 | jsonObj = json.loads(response.content) 92 | total = jsonObj['count'] 93 | for entry in jsonObj['entries']: 94 | fileEntry = entry['fileEntry'] 95 | id = fileEntry['id'] 96 | name = fileEntry['name'] 97 | print('%s %s' % (id, name)) 98 | if fileEntry['dir']: 99 | subDir = saveDir + '/' + name 100 | try: 101 | os.lstat(subDir) 102 | except OSError: 103 | os.mkdir(subDir) 104 | self.getFileRecursively(id, subDir, doc_type) 105 | else: 106 | with open('%s/%s.json' % (saveDir, id), 'w') as fp: 107 | fp.write(json.dumps(entry,ensure_ascii=False).encode('utf-8')) 108 | if doc_type == 'xml': 109 | self.getNote(id, saveDir) 110 | else: # docx 111 | self.getNoteDocx(id, saveDir) 112 | count = count + 1 113 | lastId = id 114 | 115 | def getAll(self, saveDir, doc_type): 116 | rootId = self.getRoot() 117 | self.getFileRecursively(rootId, saveDir, doc_type) 118 | 119 | if __name__ == '__main__': 120 | if len(sys.argv) < 3: 121 | print('args: [saveDir [doc_type]]' ) 122 | print('doc_type: xml or docx') 123 | sys.exit(1) 124 | username = sys.argv[1] 125 | password = sys.argv[2] 126 | if len(sys.argv) >= 4: 127 | saveDir = sys.argv[3] 128 | else: 129 | saveDir = '.' 130 | if len(sys.argv) >= 5: 131 | doc_type = sys.argv[4] 132 | else: 133 | doc_type = 'xml' 134 | sess = YoudaoNoteSession() 135 | sess.login(username, password) 136 | sess.getAll(saveDir, doc_type) 137 | --------------------------------------------------------------------------------