将小米笔记(i.mi.com)的所有笔记导出为 Obsidian Markdown 文件。
直接把下面这段话发给 Claude Code,它会自动帮你完成所有操作:
帮我按照 https://github.com/chen1guan/xiaomi-notes-export 这个仓库的说明,把小米笔记导出到 F:\shuju\小米笔记
pip install playwright
playwright install chromium将下方代码保存为 export.py:
"""
小米笔记导出 → Obsidian
从 i.mi.com 获取所有笔记并保存为 Markdown
依赖: pip install playwright && playwright install chromium
用法: python export.py [--output OUTPUT_DIR] [--browser edge|chrome]
"""
import os
import re
import json
import sys
import time
import argparse
from pathlib import Path
from datetime import datetime
from collections import Counter
# ─── 工具函数 ───────────────────────────────────────────────
def log(msg):
print(msg, flush=True)
def safe_filename(name, max_len=80):
"""清理文件名中的非法字符"""
name = re.sub(r'[\\/:*?"<>|\n\r\t]', '_', name)
name = re.sub(r'_+', '_', name)
name = name.strip('. _')
if len(name) > max_len:
name = name[:max_len]
return name or "无标题"
def html_to_markdown(html):
"""HTML → Markdown"""
if not html:
return ""
m = html
# 标题
for tag, prefix in [('h1', '# '), ('h2', '## '), ('h3', '### ')]:
m = re.sub(rf'<{tag}[^>]*>(.*?)</{tag}>', prefix + r'\1\n\n', m, flags=re.I)
# 行内样式
m = re.sub(r'<strong[^>]*>(.*?)</strong>', r'**\1**', m, flags=re.I)
m = re.sub(r'<b[^>]*>(.*?)</b>', r'**\1**', m, flags=re.I)
m = re.sub(r'<em[^>]*>(.*?)</em>', r'*\1*', m, flags=re.I)
m = re.sub(r'<i[^>]*>(.*?)</i>', r'*\1*', m, flags=re.I)
m = re.sub(r'<del[^>]*>(.*?)</del>', r'~~\1~~', m, flags=re.I)
m = re.sub(r'<code[^>]*>(.*?)</code>', r'`\1`', m, flags=re.I)
# 列表
m = re.sub(r'<li[^>]*>(.*?)</li>', r'- \1\n', m, flags=re.I)
m = re.sub(r'</?[uo]l[^>]*>', '\n', m, flags=re.I)
# 段落 / 换行
m = re.sub(r'<br\s*/?>', '\n', m, flags=re.I)
m = re.sub(r'<p[^>]*>(.*?)</p>', r'\1\n\n', m, flags=re.I)
# 链接 / 图片
m = re.sub(r'<a[^>]*href="([^"]*)"[^>]*>(.*?)</a>', r'[\2](\1)', m, flags=re.I)
m = re.sub(r'<img[^>]*src="([^"]*)"[^>]*/?>', r'', m, flags=re.I)
# 引用
m = re.sub(r'<blockquote[^>]*>(.*?)</blockquote>', r'> \1\n', m, flags=re.I)
# 移除剩余标签
m = re.sub(r'<[^>]+>', '', m)
# HTML 实体
for entity, char in [(' ', ' '), ('&', '&'), ('<', '<'), ('>', '>'), ('"', '"'), (''', "'")]:
m = m.replace(entity, char)
# 清理空行
m = re.sub(r'\n{3,}', '\n\n', m)
return m.strip()
def format_timestamp(ts):
"""Unix 毫秒时间戳 → 可读字符串"""
if not ts:
return ""
try:
if isinstance(ts, (int, float)):
if ts > 1e12:
ts = ts / 1000
return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
except:
pass
return str(ts)
# ─── 浏览器启动 ─────────────────────────────────────────────
def launch_browser(playwright, browser_type="edge"):
"""启动浏览器,使用用户的登录状态"""
user_data = os.path.expanduser(
r"~\AppData\Local\Microsoft\Edge\User Data"
if browser_type == "edge"
else r"~\AppData\Local\Google\Chrome\User Data"
)
if not os.path.exists(user_data):
raise FileNotFoundError(f"未找到 {browser_type} 用户数据目录: {user_data}")
# 关闭浏览器(释放 profile 锁)
exe = "msedge" if browser_type == "edge" else "chrome"
os.system(f"taskkill /f /im {exe}.exe >nul 2>&1")
time.sleep(2)
ctx = playwright.chromium.launch_persistent_context(
user_data,
headless=False,
channel="msedge" if browser_type == "edge" else "chrome",
args=["--disable-blink-features=AutomationControlled"],
viewport={"width": 1200, "height": 800},
ignore_default_args=["--enable-automation"],
)
return ctx
def wait_for_login(page, timeout=300):
"""如果需要登录,等待用户完成"""
if "account" in page.url or "login" in page.url.lower():
log("⚠️ 请在弹出的浏览器窗口中登录小米账号...")
log(" 登录后会自动继续...")
try:
page.wait_for_url("**/note/**", timeout=timeout * 1000)
except:
pass
page.wait_for_load_state("networkidle", timeout=30000)
page.wait_for_timeout(3000)
log("✅ 登录成功")
# ─── API 调用 ───────────────────────────────────────────────
def api_fetch(page, url):
"""通过浏览器上下文调用 API(自动携带 cookies)"""
return page.evaluate("""(url) => {
return fetch(url, {
credentials: 'include',
headers: { 'Accept': 'application/json' }
}).then(r => r.json()).catch(e => ({ error: e.message }));
}""", url)
def fetch_all_entries(page):
"""使用 syncTag 游标获取所有笔记条目"""
log("\n📡 获取笔记列表...")
# 第一次请求
data = api_fetch(page, "https://i.mi.com/note/full?pageNo=1&pageSize=200")
if "error" in data:
log(f"❌ API 错误: {data['error']}")
return []
entries = data.get("data", {}).get("entries", [])
sync_tag = data.get("data", {}).get("syncTag", "")
seen_ids = set()
all_entries = []
for e in entries:
eid = e.get("id")
if eid and eid not in seen_ids:
seen_ids.add(eid)
all_entries.append(e)
log(f" 首批: {len(all_entries)} 条")
# 用 syncTag 游标继续获取
no_new_count = 0
while sync_tag and no_new_count < 3:
data = api_fetch(page, f"https://i.mi.com/note/full?syncTag={sync_tag}&pageSize=200")
if "error" in data:
break
entries = data.get("data", {}).get("entries", [])
new_tag = data.get("data", {}).get("syncTag", "")
new_count = 0
for e in entries:
eid = e.get("id")
if eid and eid not in seen_ids:
seen_ids.add(eid)
all_entries.append(e)
new_count += 1
if new_count == 0:
no_new_count += 1
else:
no_new_count = 0
log(f" +{new_count} 条 (共 {len(all_entries)})")
if new_tag == sync_tag:
break
sync_tag = new_tag
time.sleep(0.3)
log(f"📊 共获取 {len(all_entries)} 条笔记")
return all_entries
def fetch_note_detail(page, note_id):
"""获取单条笔记的完整内容"""
data = api_fetch(page, f"https://i.mi.com/note/note/{note_id}")
if data and "data" in data:
return data["data"]
return None
# ─── 解析与保存 ─────────────────────────────────────────────
def parse_entry(entry):
"""解析笔记条目,提取标题等元数据"""
title = ""
try:
ei = entry.get("extraInfo", "")
if ei:
extra = json.loads(ei) if isinstance(ei, str) else ei
title = extra.get("title", "")
except:
pass
return {
"id": entry.get("id"),
"title": title,
"snippet": entry.get("snippet", ""),
"created": entry.get("createDate", 0),
"modified": entry.get("modifyDate", 0),
}
def save_note_markdown(output_dir, note):
"""将笔记保存为带 frontmatter 的 Markdown 文件"""
title = re.sub(r'[\n\r]+', ' ', (note.get("title") or "无标题").strip())
markdown = note.get("markdown", "")
created = format_timestamp(note.get("created"))
modified = format_timestamp(note.get("modified"))
# YAML frontmatter
fm = "---\n"
fm += f'title: "{title}"\n'
if created:
fm += f"created: {created}\n"
if modified:
fm += f"modified: {modified}\n"
fm += "source: 小米笔记\n---\n\n"
content = fm + f"# {title}\n\n{markdown}"
# 文件名(处理重名)
filename = safe_filename(title) + ".md"
filepath = output_dir / filename
counter = 1
while filepath.exists():
filepath = output_dir / f"{safe_filename(title)}_{counter}.md"
counter += 1
filepath.write_text(content, encoding="utf-8")
return filepath
# ─── 主流程 ─────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="小米笔记导出到 Obsidian")
parser.add_argument("--output", "-o", default=r"F:\shuju\小米笔记",
help="输出目录 (默认: F:\\shuju\\小米笔记)")
parser.add_argument("--browser", "-b", default="edge", choices=["edge", "chrome"],
help="浏览器 (默认: edge)")
parser.add_argument("--no-detail", action="store_true",
help="跳过获取详情(只导出标题和摘要,速度更快)")
args = parser.parse_args()
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
log("🚀 小米笔记导出工具")
log(f"📁 输出目录: {output_dir}")
log(f"🌐 浏览器: {args.browser}")
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
log("\n🌐 启动浏览器...")
ctx = launch_browser(p, args.browser)
page = ctx.pages[0] if ctx.pages else ctx.new_page()
log("🔗 打开小米笔记...")
page.goto("https://i.mi.com/note/h5#/", wait_until="networkidle", timeout=60000)
page.wait_for_timeout(3000)
wait_for_login(page)
log(f"📍 当前页面: {page.url}")
# 获取所有笔记条目
entries = fetch_all_entries(page)
if not entries:
log("❌ 未获取到笔记")
ctx.close()
return
# 日期分布
ym = Counter()
for e in entries:
try:
d = datetime.fromtimestamp(e.get("createDate", 0) / 1000)
ym[d.strftime("%Y-%m")] += 1
except:
pass
log("\n📅 日期分布:")
for k in sorted(ym.keys()):
log(f" {k}: {ym[k]} 条")
# 获取详情并保存
log(f"\n📖 开始保存 {len(entries)} 条笔记...")
saved = 0
failed = 0
notes_data = []
for i, entry in enumerate(entries):
meta = parse_entry(entry)
# 获取详情
full_content = ""
if not args.no_detail and meta["id"]:
try:
detail = fetch_note_detail(page, meta["id"])
if detail:
full_content = (detail.get("content", "")
or detail.get("text", "")
or detail.get("body", ""))
if not meta["title"]:
meta["title"] = detail.get("title", "")
if detail.get("createDate"):
meta["created"] = detail["createDate"]
if detail.get("modifyDate"):
meta["modified"] = detail["modifyDate"]
except:
failed += 1
# 限速
if (i + 1) % 100 == 0:
log(f" 📖 {i+1}/{len(entries)} (失败:{failed})")
time.sleep(0.5)
elif (i + 1) % 20 == 0:
time.sleep(0.2)
# 组装内容
raw = full_content or meta["snippet"]
markdown = html_to_markdown(raw) if ("<" in raw and ">" in raw) else raw
title = (meta["title"] or meta["snippet"][:50] or f"笔记{i+1}").strip()
note = {
"title": title,
"markdown": markdown,
"created": meta["created"],
"modified": meta["modified"],
}
notes_data.append(note)
save_note_markdown(output_dir, note)
saved += 1
# JSON 备份
backup_path = output_dir / "_backup.json"
with open(backup_path, "w", encoding="utf-8") as f:
json.dump(notes_data, f, ensure_ascii=False, indent=2)
with_content = sum(1 for n in notes_data if len(n.get("markdown", "")) > 10)
log(f"\n{'='*45}")
log(f"🎉 导出完成!")
log(f" 📝 总计: {saved} 条笔记")
log(f" 📄 有内容: {with_content} 条")
log(f" ❌ 失败: {failed} 条")
log(f" 📁 保存位置: {output_dir}")
log(f" 💾 JSON 备份: {backup_path}")
ctx.close()
if __name__ == "__main__":
main()python export.py| 参数 | 说明 | 默认值 |
|---|---|---|
--output / -o |
输出目录 | F:\shuju\小米笔记 |
--browser / -b |
浏览器 (edge 或 chrome) |
edge |
--no-detail |
只导出标题和摘要,不获取全文(更快) | 否 |
- Windows 系统
- Python 3.8+
- Edge 或 Chrome 浏览器
- 浏览器中已登录小米账号(i.mi.com)
- 脚本自动关闭浏览器并用 Playwright 启动(复用你的登录状态)
- 打开小米笔记页面,如需登录会提示
- 通过 API 分页获取所有笔记
- 逐条获取详情,转换 HTML → Markdown
- 保存为带 YAML frontmatter 的
.md文件 - 同时生成
_backup.json备份