Repository navigation
Expand file tree
/
Copy pathexport_apple_notes.py
More file actions
90 lines (78 loc) · 2.85 KB
/
Copy pathexport_apple_notes.py
File metadata and controls
90 lines (78 loc) · 2.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import os
import sqlite3
import html2text
import gzip
import io
import string
# Path to the Notes database
DB_PATH = os.path.expanduser('~/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite')
EXPORT_DIR = 'exported_notes_txt'
def clean_filename(name):
# Remove characters not allowed in filenames
return "".join(c for c in name if c.isalnum() or c in (" ", "_", "-")).rstrip()
def try_decompress_gzip(data):
try:
with gzip.GzipFile(fileobj=io.BytesIO(data)) as f:
return f.read().decode('utf-8', errors='replace')
except Exception:
return None
def is_mostly_printable(line, threshold=0.7):
printable = set(string.printable)
if not line.strip():
return False
ratio = sum(1 for c in line if c in printable) / len(line)
return ratio > threshold and any(c.isalpha() for c in line)
def extract_notes():
if not os.path.exists(DB_PATH):
print(f"Database not found at {DB_PATH}")
return
os.makedirs(EXPORT_DIR, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Get mapping of Z_PK to note titles from ZICCLOUDSYNCINGOBJECT
try:
cursor.execute("""
SELECT Z_PK, ZTITLE1 FROM ZICCLOUDSYNCINGOBJECT WHERE ZTITLE1 IS NOT NULL
""")
pk_to_title = {row[0]: row[1] for row in cursor.fetchall()}
except Exception as e:
print(f"Error reading titles: {e}")
conn.close()
return
# Now get note data from ZICNOTEDATA
try:
cursor.execute("""
SELECT ZNOTE, ZDATA FROM ZICNOTEDATA WHERE ZDATA IS NOT NULL
""")
notes = cursor.fetchall()
except Exception as e:
print(f"Error reading note data: {e}")
conn.close()
return
h = html2text.HTML2Text()
h.ignore_links = True
for idx, (znote, zdata) in enumerate(notes, 1):
title = pk_to_title.get(znote, f'note_{idx}')
filename = clean_filename(title) or f'note_{idx}'
file_path = os.path.join(EXPORT_DIR, f"{filename}.txt")
if not zdata:
continue
# Try to decompress as gzip
html_content = try_decompress_gzip(zdata)
if html_content is None:
# Fallback: try to decode as utf-8
try:
html_content = zdata.decode('utf-8', errors='replace')
except Exception:
html_content = str(zdata)
plain_text = h.handle(html_content)
# Filter out mostly gibberish lines
cleaned_lines = [l for l in plain_text.splitlines() if is_mostly_printable(l)]
cleaned_text = "\n".join(cleaned_lines)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(cleaned_text)
print(f"Exported: {file_path}")
conn.close()
print(f"Exported {len(notes)} notes to {EXPORT_DIR}/")
if __name__ == "__main__":
extract_notes()