├── LICENSE ├── README.md ├── exportevernote ├── EverNote.py └── __init__.py ├── requirement.txt └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 dong-s 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 | ## 批量导出EverNote中的所有笔记 2 | 3 | 印象笔记客户端中,导出笔记这个功能不能支持全量一次性导出,只能按笔记或者笔记本来导出,并且导出的笔记需要自己手工维护路径,感觉有点麻烦。 4 | 5 | 本地备份一方面比较安全,另一方面如果印象笔记以后不提供服务了,可以直接将导出的文件恢复到其他笔记应用,目前大部分笔记应用都支持enex文件导入。 6 | 7 | 通过使用该工具可以将笔记按**笔记本组/笔记本/笔记.enex**路径来导出。 8 | 9 | ## Token获取 10 | 11 | **1.页面申请:** 12 | - [印象笔记](https://app.yinxiang.com/api/DeveloperToken.action) 13 | - [Evernote](https://www.evernote.com/api/DeveloperToken.action) 14 | 15 | **2.网页获取:** 16 | 登录印象笔记首页: 17 | ![](http://file.dong-s.com/2019/08/evernote.jpg) 18 | 19 | 20 | ## 安装 21 | 22 | ```bash 23 | pip install evernote-export 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```bash 29 | $ evernote-export 30 | Usage: evernote-export [options] 31 | 32 | Options: 33 | -h, --help show this help message and exit 34 | -t TOKEN, --token=TOKEN 35 | evernote_api_token 36 | -d DIR, --dir_path=DIR 37 | export dir path 38 | --sandbox_model is sandbox model,default False 39 | --china_user is chinese user,default False 40 | 41 | # token和导出文件路径是必选参数 42 | # 注意:指定的导出路径在运行时会先清空 43 | $ evernote-export -t your_api_token -d /home/dong/evernote --china_user 44 | ``` 45 | 46 | ## 导出文件示例 47 | 48 | ``` 49 | $ tree 50 | . 51 | ├── EverMemo 52 | │   ├── xxxx.enex 53 | │   ├── xxx.enex 54 | │   └── xxx.enex 55 | ├── 大数据 56 | │   ├── hadoop 57 | │   │   ├──xxxx.enex 58 | │   │   ├── xxxx.enex 59 | │   │   └── xxxx.enex 60 | │   ├── hbase 61 | │   │   ├── xxxx.enex 62 | │   │   ├── xxxx.enex 63 | │   │   └── xxxx.enex 64 | │   ├── hive 65 | │   │   ├── xxxx.enex 66 | │   │   └── xxxx.enex 67 | │   └── spark 68 | │   ├── xxxx.enex 69 | │   └── xxxx.enex 70 | └── 个人 71 | └── 随笔 72 | ├── xxx.enex 73 | └── xxx.enex 74 | 75 | ``` 76 | 77 | ## 注意事项 78 | 79 | - 笔记的tag未导出 80 | - 笔记标题中特殊字符[/\\\s<>],会被替换为下划线 81 | - 仅在Mac和linux系统Python2.7环境下测试过,Python3不支持 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /exportevernote/EverNote.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import re 4 | import sys 5 | import time 6 | import base64 7 | import logging 8 | from datetime import datetime 9 | from optparse import OptionParser 10 | from evernote.edam.notestore import NoteStore 11 | from evernote.api.client import EvernoteClient 12 | 13 | 14 | logging.basicConfig(level=logging.INFO, format="<%(asctime)s> [%(levelname)s] %(message)s") 15 | 16 | note_header = """ 17 | 18 | 19 | {} 20 | {}]]>""" 21 | 22 | note_mid = """{}{}{}""" 23 | 24 | note_tail = """""" 25 | 26 | now = datetime.now().strftime('%Y%m%dT%H%M%SZ') 27 | 28 | 29 | def clear_dir(current_dir): 30 | """ 31 | 清空当前目录 32 | :param current_dir: 33 | :return: 34 | """ 35 | for root, dirs, files in os.walk(current_dir, topdown=False): 36 | for f in files: 37 | os.remove(os.path.join(root, f)) 38 | for d in dirs: 39 | os.rmdir(os.path.join(root, d)) 40 | 41 | 42 | def create_dir(path): 43 | """ 44 | 创建目录 45 | :param path: 46 | :return: 47 | """ 48 | if not os.path.exists(path): 49 | os.mkdir(path) 50 | 51 | 52 | def write_file(path, content): 53 | """ 54 | 将生成的文档写入文件 55 | :param path: 56 | :param content: 57 | :return: 58 | """ 59 | with open(path + ".enex", 'w') as f: 60 | f.write(content) 61 | 62 | 63 | def format_str(text, length): 64 | """ 65 | 按指定长度切分文本 66 | :param text: 67 | :param length: 68 | :return: 69 | """ 70 | arr = [] 71 | for i in range(len(text) / length + 1): 72 | arr.append(text[i * length:i * length + length]) 73 | return "\n".join(arr) 74 | 75 | 76 | def format_time(timestamp): 77 | return time.strftime('%Y%m%dT%H%M%SZ', time.localtime(timestamp / 1000)) 78 | 79 | 80 | class EverNoteCustomClient: 81 | def __init__(self, token, sandbox, china): 82 | logging.info("初始化EverNote客户端!") 83 | self.client = EvernoteClient(token=token, sandbox=sandbox, china=china) 84 | self.note_store = self.client.get_note_store() 85 | 86 | self._tags = {i.guid:i.name for i in self.note_store.listTags()} 87 | 88 | def list_notebooks(self): 89 | logging.info("获取所有笔记本!") 90 | return self.note_store.listNotebooks() 91 | 92 | def get_notes_by_notebookid(self, notebook_guid, start, end): 93 | """ 94 | 获取当前笔记本下的所有笔记 95 | :param notebook_guid: 96 | :return: 97 | """ 98 | note_filter = NoteStore.NoteFilter() 99 | note_filter.notebookGuid = notebook_guid 100 | return self.note_store.findNotes(note_filter, start, end).notes 101 | 102 | def get_note(self, note_guid): 103 | """ 104 | 获取该笔记的完整内容 105 | :param note_guid: 106 | :return: 107 | """ 108 | return self.note_store.getNote(note_guid, True, True, True, True) 109 | 110 | def get_note_attributes(self, attribute): 111 | res = "" 112 | if attribute.subjectDate: 113 | res += "{}".format(attribute.subjectDate) 114 | if attribute.latitude: 115 | res += "{}".format(attribute.latitude) 116 | if attribute.longitude: 117 | res += "{}".format(attribute.longitude) 118 | if attribute.altitude: 119 | res += "{}".format(attribute.altitude) 120 | if attribute.author: 121 | res += "{}".format(attribute.author) 122 | if attribute.source: 123 | res += "{}".format(attribute.source) 124 | if attribute.sourceURL: 125 | res += "{}".format(attribute.sourceURL.replace("&", "&")) 126 | if attribute.sourceApplication: 127 | res += "{}".format(attribute.sourceApplication) 128 | if attribute.shareDate: 129 | res += "{}".format(attribute.shareDate) 130 | if attribute.reminderOrder: 131 | res += "{}".format(attribute.reminderOrder) 132 | if attribute.reminderTime: 133 | res += "{}".format(attribute.reminderTime) 134 | if attribute.placeName: 135 | res += "{}".format(attribute.placeName) 136 | if attribute.contentClass: 137 | res += "{}".format(attribute.contentClass) 138 | if attribute.classifications: 139 | res += "{}".format(attribute.classifications) 140 | if attribute.creatorId: 141 | res += "{}".format(attribute.creatorId) 142 | return res 143 | 144 | def get_note_resource_attributes(self, attribute): 145 | res = "19700101T000000Z" 146 | if attribute.sourceURL: 147 | res += "{}".format(attribute.sourceURL.replace("&", "&")) 148 | if attribute.recoType: 149 | res += "{}".format(attribute.recoType) 150 | else: 151 | res += "unknown" 152 | if attribute.fileName: 153 | res += "{}".format(attribute.fileName) 154 | return res 155 | 156 | def get_note_resource(self, resource): 157 | res = "" 158 | if resource.data.body: 159 | res += "{}".format( 160 | format_str(base64.b64encode(resource.data.body), 80)) 161 | if resource.mime: 162 | res += "{}".format(resource.mime) 163 | if resource.width: 164 | res += "{}".format(resource.width) 165 | if resource.height: 166 | res += "{}".format(resource.height) 167 | if resource.duration: 168 | res += "{}".format(resource.duration) 169 | else: 170 | res += "0" 171 | if resource.recognition: 172 | res += "".format(resource.recognition) 173 | if resource.attributes: 174 | res += "{}".format( 175 | self.get_note_resource_attributes(resource.attributes)) 176 | return "{}".format(res) 177 | 178 | def format_enex_file(self, note_guid): 179 | """ 180 | 组装enex文件 181 | :param note_guid: 182 | :return: 183 | """ 184 | note = self.note_store.getNote(note_guid, True, True, True, True) 185 | 186 | try: 187 | content = note.content[note.content.find("", "
" + "
".join(details) + "
") 197 | 198 | result = note_header.format(now, note.title, content) 199 | result += note_mid.format(format_time(note.created), format_time(note.updated), 200 | self.get_note_attributes(note.attributes)) 201 | 202 | if note.resources: 203 | for resource in note.resources: 204 | result += self.get_note_resource(resource) 205 | 206 | result += note_tail 207 | return result 208 | except Exception as e: 209 | logging.error("《{}》导出异常!".format(note.title), e) 210 | 211 | 212 | def main(): 213 | parser = OptionParser() 214 | 215 | parser.add_option("-t", "--token", dest="token", help="evernote_api_token") 216 | parser.add_option("-d", "--dir_path", dest="dir", help="export dir path") 217 | parser.add_option('--sandbox_model', dest="sandbox", help='is sandbox model', action='store_true', default=False) 218 | parser.add_option('--china_user', dest="china", help='is chinese user', action='store_true', default=False) 219 | (options, args) = parser.parse_args() 220 | 221 | token = options.token 222 | target_dir = options.dir 223 | sandbox = options.sandbox 224 | china = options.china 225 | 226 | note_count = 0 227 | notebook_count = 0 228 | 229 | if token is None or target_dir is None: 230 | parser.print_help() 231 | exit(0) 232 | 233 | logging.info("清空目标文件夹:{}".format(target_dir)) 234 | clear_dir(target_dir) 235 | 236 | client = EverNoteCustomClient(token=token, sandbox=sandbox, china=china) 237 | 238 | notebooks = client.list_notebooks() 239 | 240 | for notebook in notebooks: 241 | notebook_count += 1 242 | # 判断当前笔记本是否有上级目录 243 | logging.info("================================") 244 | logging.info("开始导出notebook《{}》中的笔记".format(notebook.name)) 245 | if notebook.stack: 246 | create_dir(os.path.join(target_dir, notebook.stack)) 247 | note_path = os.path.join(target_dir, notebook.stack, notebook.name) 248 | create_dir(note_path) 249 | else: 250 | note_path = os.path.join(target_dir, notebook.name) 251 | create_dir(note_path) 252 | logging.info("创建notebook对应的目录:{}".format(note_path)) 253 | 254 | for i in range(10): 255 | notes = client.get_notes_by_notebookid(notebook.guid, i*50, i*50+50) 256 | 257 | if len(notes) <= 0: 258 | break 259 | 260 | for note in notes: 261 | note_count += 1 262 | logging.info("开始导出笔记《{}》".format(note.title)) 263 | result = client.format_enex_file(note.guid) 264 | if result: 265 | write_file(os.path.join(note_path, re.sub(r'[/\\\s<>]', '_', note.title[:100])), result) 266 | # exit() 267 | 268 | logging.info("当前notebook《{}》已导出完成!".format(notebook.name)) 269 | 270 | logging.info("全部笔记已导出,共导出笔记本:{},共导出笔记:{}".format(notebook_count, note_count)) 271 | 272 | 273 | if __name__ == '__main__': 274 | main() 275 | -------------------------------------------------------------------------------- /exportevernote/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dong-s/ExportAllEverNote/f44d2a141a2587c3ca80efd5cdc9ba527015f379/exportevernote/__init__.py -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | evernote==1.25.3 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | from setuptools import setup, find_packages 4 | from codecs import open 5 | import os 6 | 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | setup( 10 | name='evernote-export', 11 | version='1.0.1', 12 | description='批量导出Evernote中的所有笔记,按照笔记目录,存档到本地对应文件夹', 13 | long_description=open(os.path.join(here, 'README.md'), encoding='utf-8').read(), 14 | long_description_content_type='text/markdown', 15 | url='https://github.com/dong-s/ExportAllEverNote', 16 | author='Dong', 17 | author_email='dong@dong-s.com', 18 | license='MIT', 19 | classifiers=[ 20 | 'Development Status :: 3 - Alpha', 21 | 'Intended Audience :: Developers', 22 | 'Topic :: Software Development :: Libraries :: Python Modules', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Programming Language :: Python :: 2.7', 25 | ], 26 | keywords='evernote export enex', 27 | packages=find_packages(), 28 | install_requires=['evernote'], 29 | extras_require={}, 30 | entry_points={ 31 | 'console_scripts': [ 32 | 'evernote-export = exportevernote.EverNote:main' 33 | ] 34 | }, 35 | ) 36 | --------------------------------------------------------------------------------