feat: schema upgrade + import 159 books from reading list

- db.py: add group, isbn, priority, read_reason, read_order, cover_blob fields
- Import 159 books from booklist.md with group/priority/status metadata
- Include books.db in repo (personal use)
This commit is contained in:
kai
2026-03-25 14:55:23 +08:00
parent 8c8095d9ac
commit 69c34d4faf
5 changed files with 801 additions and 8 deletions
+279
View File
@@ -0,0 +1,279 @@
#!/usr/bin/env python3
"""一次性脚本:从用户 Markdown 书单解析并导入 SQLite。"""
import re
import sys
import os
# 让 import db 能找到项目模块
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
os.chdir(os.path.dirname(os.path.abspath(__file__)))
import db
# ── 分组映射 ──────────────────────────────────────────────────────────
GROUP_MAP = {
"历史学": "历史学",
"伊朗": "历史学",
"中东": "历史学",
"政治哲学": "政治哲学",
"社会思想": "政治哲学",
"科学": "科学&进化",
"进化": "科学&进化",
"人生意义": "心理&思维",
"心理学": "心理&思维",
"经济学": "经济&金融",
"金融": "经济&金融",
"文学": "文学",
"传记": "文学",
"古典诗学": "古典诗学",
"文化": "古典诗学",
"投资": "经济&金融",
"交易": "经济&金融",
"思维": "心理&思维",
"工具": "心理&思维",
"科技": "科技&AI",
"AI": "科技&AI",
"赫拉利": "科技&AI",
"数学": "数学&科学史",
"科学史": "数学&科学史",
"伯克利": "推荐书单",
"中国史": "中国史",
"其他": "其他",
"已读书目": "已读归档",
"阅读优先级": "",
}
def infer_group(section_title):
for key, group in GROUP_MAP.items():
if key in section_title:
return group
return ""
def parse_status(content):
if "" in content and "已读" in content:
return "read"
if "🔖" in content or "在读" in content:
return "reading"
return "to-read"
def extract_scores(content):
douban = None
gr = None
m = re.search(r'豆瓣[^0-9]*?(\d\.\d)', content)
if m:
douban = float(m.group(1))
m = re.search(r'GR[^0-9]*?(\d\.\d+)', content)
if m:
gr = float(m.group(1))
return douban, gr
def extract_author(content):
# 尝试 · 作者 · 模式
m = re.search(r'·\s*([^·\n]{2,30}?)\s*·', content)
if m:
author = m.group(1).strip()
skip_words = ['豆瓣', 'GR', '评分', '文学', '历史', '科技',
'哲学', '经济', '科学', '传记', '政治',
'社会', '心理', '宇宙', '物理', '数学',
'', '在读', '已读', '待读', 'FT', 'NYT',
'年度', '美国', '中国', '伊朗', '金融',
'制度', '方志远', '经济制裁', '复杂',
'海权', '越南', '中东', '法律', '清代']
if not any(kw in author for kw in skip_words):
return author
# 尝试 [] 内国籍后的作者: [美] 作者名
m = re.search(r'\[[\u4e00-\u9fff]+\]\s*([\u4e00-\u9fff·A-Za-z ]{2,20})', content)
if m:
return m.group(1).strip()
return None
def clean_title(raw):
return raw.replace("**", "").strip()
def parse_booklist(text):
books = []
seen_titles = set()
current_group = ""
current_priority = ""
lines = text.splitlines()
i = 0
while i < len(lines):
line = lines[i].rstrip()
# ## 大标题
if line.startswith("## "):
section = line.lstrip("# ").strip()
g = infer_group(section)
if g:
current_group = g
# 优先级检测
if "⭐⭐⭐" in section or "强烈推荐" in section:
current_priority = "⭐⭐⭐"
elif "⭐⭐" in section or "值得读" in section:
current_priority = "⭐⭐"
elif "" in section or "可选" in section:
current_priority = ""
else:
current_priority = ""
i += 1
continue
# ⭐ 优先级行
if re.match(r'^⭐⭐⭐\s', line):
current_priority = "⭐⭐⭐"
i += 1
continue
if re.match(r'^⭐⭐\s', line) and not line.startswith("⭐⭐⭐"):
current_priority = "⭐⭐"
i += 1
continue
if re.match(r'^⭐\s', line) and not line.startswith("⭐⭐"):
current_priority = ""
i += 1
continue
# ### 子分组
if line.startswith("### "):
sub = line.lstrip("# ").strip()
sub_g = infer_group(sub)
if sub_g:
current_group = sub_g
i += 1
continue
# 书目行
m = re.match(r'^-\s*\[([ x/])\]\s*(.*)', line)
if not m:
i += 1
continue
content = m.group(2).strip()
# 提取书名
title_match = re.search(r'\*\*(.+?)\*\*', content)
if not title_match:
i += 1
continue
title = clean_title(title_match.group(1))
if title in seen_titles:
i += 1
continue
seen_titles.add(title)
status = parse_status(content)
douban, gr = extract_scores(content)
author = extract_author(content)
# 收集推荐理由
reasons = []
j = i + 1
while j < len(lines):
nl = lines[j]
if nl.startswith(" ") and not nl.strip().startswith("- ["):
reason = nl.strip().lstrip("- ")
if reason.startswith("推荐理由:") or reason.startswith("推荐理由:"):
reason = reason.split("", 1)[-1].split(":", 1)[-1].strip()
if reason and len(reason) > 2:
reasons.append(reason)
j += 1
else:
break
read_reason = " ".join(reasons) if reasons else ""
# group 推断
group = current_group
if not group:
for key, g in [("中国史", "中国史"), ("历史", "历史学"),
("科技", "科技&AI"), ("AI", "科技&AI"),
("文学", "文学"), ("哲学", "政治哲学"),
("经济", "经济&金融"), ("科学", "科学&进化"),
("心理", "心理&思维"), ("传记", "文学"),
("物理", "科学&进化"), ("宇宙", "科学&进化"),
("金融", "经济&金融"), ("投资", "经济&金融")]:
if key in content:
group = g
break
books.append({
"title": title,
"author": author,
"status": status,
"douban_score": douban,
"goodreads_score": gr,
"group": group,
"priority": current_priority,
"read_reason": read_reason,
"format": "paper",
"tags": "",
})
i = j
continue
return books
def main():
booklist_path = os.path.join(os.path.dirname(__file__), "booklist.md")
if not os.path.exists(booklist_path):
print("❌ 未找到 booklist.md,请将书单保存到项目目录")
sys.exit(1)
with open(booklist_path, "r", encoding="utf-8") as f:
text = f.read()
books = parse_booklist(text)
db.init_db()
# 清空(一次性操作)
conn = db._connect()
existing = conn.execute("SELECT COUNT(*) FROM books").fetchone()[0]
if existing > 0:
print(f"⚠️ 数据库已有 {existing} 条记录,将先清空")
conn.execute("DELETE FROM books")
conn.execute("DELETE FROM sqlite_sequence WHERE name='books'")
conn.commit()
conn.close()
# 批量插入
inserted = 0
for b in books:
book_id = db.add_book(
b["title"],
author=b.get("author"),
status=b["status"],
douban_score=b.get("douban_score"),
goodreads_score=b.get("goodreads_score"),
group=b.get("group", ""),
priority=b.get("priority", ""),
read_reason=b.get("read_reason", ""),
format=b["format"],
tags=b.get("tags", ""),
)
icon = {"read": "", "reading": "🔖", "to-read": "📖"}.get(b["status"], "📕")
g = b.get("group", "")
p = b.get("priority", "")
print(f" {icon} [{book_id:>3}] 《{b['title']}》 [{g}] {p}")
inserted += 1
print(f"\n✅ 导入完成:共 {inserted} 本书")
stats = db.get_stats()
print(f" 状态: {dict(stats['by_status'])}")
if stats.get("by_group"):
print(f" 分组: {dict(stats['by_group'])}")
if __name__ == "__main__":
main()