├── 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 | 
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 |
--------------------------------------------------------------------------------