mirror of
https://github.com/KingHWJ/WeReadBoard.git
synced 2026-11-03 18:24:58 +08:00
1. 数据源切换至 MySQL: - app.py 从 SQLite 切换到 PyMySQL,所有 API 读取 MySQL weread_dashboard 数据库 - 新增 db_required 装饰器统一管理数据库连接和异常处理 - 新增 /api/health 健康检查接口,检测 MySQL 连接状态 - MySQL 连接失败时返回 HTTP 503 和告警信息 - 修复 fetched_at 字段 Unix 时间戳到 MySQL TIMESTAMP 的兼容问题 2. 前端数据库连接告警: - 页面加载时优先调用 /api/health 检查 MySQL 连接 - 连接失败时显示固定告警横幅,阻止后续数据加载 - CSS 告警横幅样式(黄色背景、下滑动画) 3. 代码中文注释: - app.py:所有函数、API 接口、工具函数添加完整中文文档注释 - fetch_data.py:所有 SQLite 建表语句添加列级中文注释 - migrate_to_mysql.py:保留已有中文注释 4. 技术文档与元数据文档: - docs/TECHNICAL_DOC.md:项目概述、系统架构、API 设计、部署指南 - docs/METADATA_DOC.md:18 张表完整字段说明、索引、表关系、数据类型规范
600 lines
24 KiB
Python
600 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
微信读书数据采集脚本
|
||
从微信读书 API 拉取数据并存入本地 SQLite 数据库
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import sqlite3
|
||
import time
|
||
import urllib.request
|
||
|
||
API_URL = "https://i.weread.qq.com/api/agent/gateway"
|
||
API_KEY = os.environ.get("WEREAD_API_KEY", "")
|
||
SKILL_VERSION = "1.0.3"
|
||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db")
|
||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
|
||
def api_call(api_name, **kwargs):
|
||
"""调用微信读书 API"""
|
||
body = {"api_name": api_name, "skill_version": SKILL_VERSION}
|
||
body.update(kwargs)
|
||
req = urllib.request.Request(
|
||
API_URL,
|
||
data=json.dumps(body).encode("utf-8"),
|
||
headers={
|
||
"Authorization": f"Bearer {API_KEY}",
|
||
"Content-Type": "application/json",
|
||
},
|
||
method="POST",
|
||
)
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
return json.loads(resp.read().decode("utf-8"))
|
||
|
||
|
||
def init_db(db_path):
|
||
"""
|
||
初始化 SQLite 数据库表结构。
|
||
创建所有数据表(如果不存在),包括:
|
||
- 基础数据表:书架、阅读统计、笔记本等(采集时写入)
|
||
- 按需数据表:书籍详情、划线、想法等(用户点击时写入)
|
||
"""
|
||
conn = sqlite3.connect(db_path)
|
||
c = conn.cursor()
|
||
|
||
# ===== 基础数据表(采集脚本写入) =====
|
||
|
||
# 书架书籍表 - 存储用户书架中的所有书籍信息
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS books (
|
||
book_id TEXT PRIMARY KEY, -- 书籍唯一标识
|
||
title TEXT, -- 书名
|
||
author TEXT, -- 作者
|
||
cover TEXT, -- 封面图 URL
|
||
category TEXT, -- 分类(如 "文学-古代诗词")
|
||
finish_reading INTEGER DEFAULT 0, -- 是否读完 (1=读完, 0=未读完)
|
||
read_update_time INTEGER, -- 最近阅读时间 (Unix 时间戳)
|
||
secret INTEGER DEFAULT 0, -- 是否私密书籍 (1=私密)
|
||
is_top INTEGER DEFAULT 0, -- 是否置顶 (1=置顶)
|
||
update_time INTEGER, -- 书籍信息更新时间 (Unix 时间戳)
|
||
deep_link TEXT -- 微信读书 App 深度链接
|
||
)
|
||
""")
|
||
|
||
# 阅读统计概要表 - 存储总体/年度阅读统计数据
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS reading_stats (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
mode TEXT, -- 统计维度: overall(总体)/annually(年度)
|
||
period TEXT, -- 周期标识: "overall" 或 "2026"
|
||
total_read_time INTEGER, -- 总阅读时长 (秒)
|
||
read_days INTEGER, -- 有效阅读天数
|
||
day_average_read_time REAL, -- 日均阅读时长 (秒)
|
||
regist_time INTEGER, -- 用户注册时间 (Unix 时间戳)
|
||
raw_json TEXT -- 原始 API 返回 JSON (完整备份)
|
||
)
|
||
""")
|
||
|
||
# 阅读时长时间序列表 - 按年/月/日分桶的阅读时长数据
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS read_times (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
mode TEXT, -- 统计维度 (含 _daily 后缀表示每日数据)
|
||
period TEXT, -- 周期标识
|
||
bucket_timestamp INTEGER, -- 分桶起始时间戳 (Unix 时间戳)
|
||
duration INTEGER -- 该时段阅读时长 (秒)
|
||
)
|
||
""")
|
||
|
||
# 阅读时长排行表 - 阅读时间最久的书籍排名
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS read_longest (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
mode TEXT, -- 统计维度
|
||
period TEXT, -- 周期标识
|
||
book_id TEXT, -- 书籍 ID
|
||
title TEXT, -- 书名
|
||
author TEXT, -- 作者
|
||
cover TEXT, -- 封面图 URL
|
||
read_time INTEGER, -- 阅读时长 (秒)
|
||
tags TEXT -- 标签 (JSON 数组格式)
|
||
)
|
||
""")
|
||
|
||
# 偏好分类表 - 用户阅读的书籍分类统计
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS prefer_categories (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
mode TEXT, -- 统计维度
|
||
period TEXT, -- 周期标识
|
||
category_id INTEGER, -- 分类 ID
|
||
category_title TEXT, -- 分类名称 (如 "文学")
|
||
parent_category_title TEXT, -- 父分类名称 (如 "精品小说")
|
||
val REAL, -- 偏好权重值
|
||
reading_time INTEGER, -- 该分类阅读时长 (秒)
|
||
reading_count INTEGER -- 该分类阅读本数
|
||
)
|
||
""")
|
||
|
||
# 偏好作者表 - 用户阅读最多的作者统计
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS prefer_authors (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
mode TEXT, -- 统计维度
|
||
period TEXT, -- 周期标识
|
||
author_id INTEGER, -- 作者 ID
|
||
name TEXT, -- 作者名称
|
||
count INTEGER, -- 阅读该作者的书本数
|
||
read_time TEXT -- 阅读时长 (格式化字符串)
|
||
)
|
||
""")
|
||
|
||
# 24小时阅读时段分布表 - 一天中每个小时的阅读时长
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS prefer_time (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
mode TEXT, -- 统计维度
|
||
period TEXT, -- 周期标识
|
||
hour_index INTEGER, -- 小时 (0-23)
|
||
duration INTEGER -- 该小时阅读时长 (秒)
|
||
)
|
||
""")
|
||
|
||
# 笔记本概览表 - 有笔记的书籍及其笔记统计
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS notebooks (
|
||
book_id TEXT PRIMARY KEY, -- 书籍 ID
|
||
title TEXT, -- 书名
|
||
author TEXT, -- 作者
|
||
cover TEXT, -- 封面图 URL
|
||
review_count INTEGER DEFAULT 0, -- 想法/点评数
|
||
note_count INTEGER DEFAULT 0, -- 划线数
|
||
bookmark_count INTEGER DEFAULT 0, -- 书签数
|
||
reading_progress INTEGER DEFAULT 0, -- 阅读进度 (0-100)
|
||
marked_status INTEGER DEFAULT 0, -- 标记状态 (1=读完)
|
||
sort INTEGER -- 排序值 (微信读书返回的排序字段)
|
||
)
|
||
""")
|
||
|
||
# 阅读统计摘要项表 - App 首页展示的统计摘要
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS read_stat_items (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
mode TEXT, -- 统计维度
|
||
period TEXT, -- 周期标识
|
||
stat_name TEXT, -- 统计项名称 (读过/读完/阅读/笔记)
|
||
counts TEXT, -- 统计值文案 (如 "404本")
|
||
scheme TEXT -- App 跳转链接
|
||
)
|
||
""")
|
||
|
||
# 书单/归档表 - 用户创建的书单
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS archives (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT, -- 书单名称
|
||
book_ids TEXT -- 包含的书籍 ID 列表 (JSON 数组)
|
||
)
|
||
""")
|
||
|
||
# 数据采集元信息表 - 记录采集时间等元数据
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS fetch_meta (
|
||
key TEXT PRIMARY KEY, -- 元信息键名 (如 "last_fetch_time")
|
||
value TEXT -- 元信息值
|
||
)
|
||
""")
|
||
|
||
# ===== 按需数据表(用户点击时实时获取并写入) =====
|
||
|
||
# 书籍详细信息表 - 来自微信读书 /book/info 接口
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS book_details (
|
||
book_id TEXT PRIMARY KEY, -- 书籍 ID
|
||
title TEXT, -- 书名
|
||
author TEXT, -- 作者
|
||
translator TEXT, -- 译者
|
||
cover TEXT, -- 封面图 URL
|
||
intro TEXT, -- 书籍简介
|
||
category TEXT, -- 分类
|
||
publisher TEXT, -- 出版社
|
||
publish_time TEXT, -- 出版时间
|
||
isbn TEXT, -- ISBN 号
|
||
word_count INTEGER, -- 总字数
|
||
new_rating REAL, -- 评分 (百分制)
|
||
new_rating_count INTEGER, -- 评分人数
|
||
fetched_at INTEGER -- 数据获取时间 (Unix 时间戳)
|
||
)
|
||
""")
|
||
|
||
# 阅读进度表 - 来自微信读书 /book/getprogress 接口
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS book_progress (
|
||
book_id TEXT PRIMARY KEY, -- 书籍 ID
|
||
progress INTEGER, -- 阅读进度百分比 (0-100)
|
||
chapter_uid INTEGER, -- 当前阅读章节 UID
|
||
chapter_offset INTEGER, -- 当前章节内偏移量
|
||
update_time INTEGER, -- 最后阅读时间 (Unix 时间戳)
|
||
record_reading_time INTEGER, -- 累计阅读时长 (秒)
|
||
finish_time INTEGER, -- 读完时间 (Unix 时间戳)
|
||
is_start_reading INTEGER, -- 是否已开始阅读 (1=是)
|
||
fetched_at INTEGER -- 数据获取时间 (Unix 时间戳)
|
||
)
|
||
""")
|
||
|
||
# 划线内容表 - 来自微信读书 /book/bookmarklist 接口
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS book_highlights (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
book_id TEXT, -- 书籍 ID
|
||
bookmark_id TEXT, -- 划线唯一 ID
|
||
chapter_uid INTEGER, -- 所在章节 UID
|
||
mark_text TEXT, -- 划线原文内容
|
||
create_time INTEGER, -- 创建时间 (Unix 时间戳)
|
||
type INTEGER, -- 划线类型
|
||
range_text TEXT, -- 位置范围
|
||
color_style INTEGER, -- 划线颜色样式
|
||
fetched_at INTEGER -- 数据获取时间 (Unix 时间戳)
|
||
)
|
||
""")
|
||
|
||
# 章节信息表 - 来自微信读书 /book/bookmarklist 接口的 chapters 字段
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS book_chapters (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
book_id TEXT, -- 书籍 ID
|
||
chapter_uid INTEGER, -- 章节 UID
|
||
chapter_idx INTEGER, -- 章节序号
|
||
title TEXT, -- 章节标题
|
||
UNIQUE(book_id, chapter_uid) -- 联合唯一约束
|
||
)
|
||
""")
|
||
|
||
# 个人想法/点评表 - 来自微信读书 /review/list/mine 接口
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS book_reviews (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
book_id TEXT, -- 书籍 ID
|
||
review_id TEXT, -- 想法唯一 ID
|
||
content TEXT, -- 想法内容文本
|
||
create_time INTEGER, -- 创建时间 (Unix 时间戳)
|
||
star INTEGER, -- 评分 (0-5, -1=无评分)
|
||
chapter_name TEXT, -- 所在章节名称
|
||
is_finish INTEGER, -- 是否书评 (1=书评)
|
||
fetched_at INTEGER -- 数据获取时间 (Unix 时间戳)
|
||
)
|
||
""")
|
||
|
||
# 推荐书籍表 - 来自微信读书 /book/recommend 接口
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS recommended_books (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
book_id TEXT, -- 书籍 ID
|
||
title TEXT, -- 书名
|
||
author TEXT, -- 作者
|
||
cover TEXT, -- 封面图 URL
|
||
intro TEXT, -- 书籍简介
|
||
category TEXT, -- 分类
|
||
reason TEXT, -- 推荐理由
|
||
reading_count INTEGER, -- 在读人数
|
||
new_rating REAL, -- 评分 (百分制)
|
||
new_rating_count INTEGER, -- 评分人数
|
||
rating_title TEXT, -- 评分标签 (神作/力荐等)
|
||
fetched_at INTEGER -- 数据获取时间 (Unix 时间戳)
|
||
)
|
||
""")
|
||
|
||
# 搜索结果表 - 来自微信读书 /store/search 接口
|
||
c.execute("""
|
||
CREATE TABLE IF NOT EXISTS search_results (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
keyword TEXT, -- 搜索关键词
|
||
book_id TEXT, -- 书籍 ID
|
||
title TEXT, -- 书名
|
||
author TEXT, -- 作者
|
||
cover TEXT, -- 封面图 URL
|
||
intro TEXT, -- 书籍简介
|
||
publisher TEXT, -- 出版社
|
||
category TEXT, -- 分类
|
||
reading_count INTEGER, -- 在读人数
|
||
new_rating REAL, -- 评分 (百分制)
|
||
new_rating_count INTEGER, -- 评分人数
|
||
rating_title TEXT, -- 评分标签
|
||
search_idx INTEGER, -- 搜索序号
|
||
fetched_at INTEGER -- 数据获取时间 (Unix 时间戳)
|
||
)
|
||
""")
|
||
|
||
conn.commit()
|
||
return conn
|
||
|
||
|
||
def fetch_shelf(conn):
|
||
"""拉取书架数据"""
|
||
print(" 拉取书架数据...")
|
||
data = api_call("/shelf/sync")
|
||
c = conn.cursor()
|
||
|
||
books = data.get("books", [])
|
||
for book in books:
|
||
c.execute(
|
||
"""INSERT OR REPLACE INTO books
|
||
(book_id, title, author, cover, category, finish_reading,
|
||
read_update_time, secret, is_top, update_time, deep_link)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(
|
||
book.get("bookId"),
|
||
book.get("title", ""),
|
||
book.get("author", ""),
|
||
book.get("cover", ""),
|
||
book.get("category", ""),
|
||
book.get("finishReading", 0),
|
||
book.get("readUpdateTime", 0),
|
||
book.get("secret", 0),
|
||
book.get("isTop", 0),
|
||
book.get("updateTime", 0),
|
||
book.get("deepLink", ""),
|
||
),
|
||
)
|
||
|
||
# 保存书单
|
||
archives = data.get("archive", [])
|
||
c.execute("DELETE FROM archives")
|
||
for arch in archives:
|
||
c.execute(
|
||
"INSERT INTO archives (name, book_ids) VALUES (?, ?)",
|
||
(arch.get("name", ""), json.dumps(arch.get("bookIds", []))),
|
||
)
|
||
|
||
conn.commit()
|
||
mp_flag = "yes" if data.get("mp") else "no"
|
||
print(f" 书架: {len(books)} 本电子书, {len(data.get('albums', []))} 个专辑, MP={mp_flag}, {len(archives)} 个书单")
|
||
|
||
|
||
def fetch_readdata(conn, mode, period_label):
|
||
"""拉取阅读统计数据"""
|
||
print(f" 拉取阅读统计 [{mode}]...")
|
||
params = {"mode": mode}
|
||
if mode != "overall":
|
||
params["baseTime"] = 0
|
||
data = api_call("/readdata/detail", **params)
|
||
c = conn.cursor()
|
||
|
||
total_read_time = data.get("totalReadTime", 0)
|
||
read_days = data.get("readDays", 0)
|
||
day_avg = data.get("dayAverageReadTime", 0)
|
||
regist_time = data.get("registTime", 0)
|
||
|
||
c.execute(
|
||
"""INSERT INTO reading_stats
|
||
(mode, period, total_read_time, read_days, day_average_read_time,
|
||
regist_time, raw_json)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||
(
|
||
mode,
|
||
period_label,
|
||
total_read_time,
|
||
read_days,
|
||
day_avg,
|
||
regist_time,
|
||
json.dumps(data, ensure_ascii=False),
|
||
),
|
||
)
|
||
|
||
# 保存 readTimes 分桶数据
|
||
read_times = data.get("readTimes", {})
|
||
for ts, duration in read_times.items():
|
||
c.execute(
|
||
"INSERT INTO read_times (mode, period, bucket_timestamp, duration) VALUES (?, ?, ?, ?)",
|
||
(mode, period_label, int(ts), duration),
|
||
)
|
||
|
||
# 保存 dailyReadTimes(如果有)
|
||
daily_times = data.get("dailyReadTimes", {})
|
||
for ts, duration in daily_times.items():
|
||
c.execute(
|
||
"INSERT INTO read_times (mode, period, bucket_timestamp, duration) VALUES (?, ?, ?, ?)",
|
||
(mode + "_daily", period_label, int(ts), duration),
|
||
)
|
||
|
||
# 保存阅读最久的书籍
|
||
for item in data.get("readLongest", []):
|
||
book = item.get("book", {})
|
||
album = item.get("albumInfo", {})
|
||
c.execute(
|
||
"""INSERT INTO read_longest
|
||
(mode, period, book_id, title, author, cover, read_time, tags)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(
|
||
mode,
|
||
period_label,
|
||
book.get("bookId") or album.get("albumId", ""),
|
||
book.get("title") or album.get("name", ""),
|
||
book.get("author") or album.get("authorName", ""),
|
||
book.get("cover") or album.get("cover", ""),
|
||
item.get("readTime", 0),
|
||
json.dumps(item.get("tags", []), ensure_ascii=False),
|
||
),
|
||
)
|
||
|
||
# 保存偏好分类
|
||
for cat in data.get("preferCategory", []):
|
||
c.execute(
|
||
"""INSERT INTO prefer_categories
|
||
(mode, period, category_id, category_title, parent_category_title,
|
||
val, reading_time, reading_count)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(
|
||
mode,
|
||
period_label,
|
||
cat.get("categoryId", 0),
|
||
cat.get("categoryTitle", ""),
|
||
cat.get("parentCategoryTitle", ""),
|
||
cat.get("val", 0),
|
||
cat.get("readingTime", 0),
|
||
cat.get("readingCount", 0),
|
||
),
|
||
)
|
||
|
||
# 保存偏好作者
|
||
for author in data.get("preferAuthor", []):
|
||
c.execute(
|
||
"""INSERT INTO prefer_authors
|
||
(mode, period, author_id, name, count, read_time)
|
||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||
(
|
||
mode,
|
||
period_label,
|
||
author.get("authorId", 0),
|
||
author.get("name", ""),
|
||
author.get("count", 0),
|
||
author.get("readTime", ""),
|
||
),
|
||
)
|
||
|
||
# 保存24小时时段分布
|
||
prefer_time = data.get("preferTime", [])
|
||
# preferTime 从6点开始到次日5点,映射到 hour_index
|
||
for i, duration in enumerate(prefer_time):
|
||
hour = (6 + i) % 24
|
||
c.execute(
|
||
"INSERT INTO prefer_time (mode, period, hour_index, duration) VALUES (?, ?, ?, ?)",
|
||
(mode, period_label, hour, duration),
|
||
)
|
||
|
||
# 保存统计摘要项
|
||
for stat in data.get("readStat", []):
|
||
c.execute(
|
||
"""INSERT INTO read_stat_items
|
||
(mode, period, stat_name, counts, scheme)
|
||
VALUES (?, ?, ?, ?, ?)""",
|
||
(
|
||
mode,
|
||
period_label,
|
||
stat.get("stat", ""),
|
||
stat.get("counts", ""),
|
||
stat.get("scheme", ""),
|
||
),
|
||
)
|
||
|
||
conn.commit()
|
||
print(f" [{mode}] 总时长: {total_read_time}秒 ({total_read_time//3600}小时), 阅读天数: {read_days}")
|
||
|
||
|
||
def fetch_notebooks(conn):
|
||
"""拉取笔记本概览(分页拉取全部)"""
|
||
print(" 拉取笔记本数据...")
|
||
c = conn.cursor()
|
||
c.execute("DELETE FROM notebooks")
|
||
|
||
count = 100
|
||
last_sort = None
|
||
total = 0
|
||
|
||
while True:
|
||
params = {"count": count}
|
||
if last_sort is not None:
|
||
params["lastSort"] = last_sort
|
||
data = api_call("/user/notebooks", **params)
|
||
|
||
books = data.get("books", [])
|
||
for item in books:
|
||
book = item.get("book", {})
|
||
c.execute(
|
||
"""INSERT OR REPLACE INTO notebooks
|
||
(book_id, title, author, cover, review_count, note_count,
|
||
bookmark_count, reading_progress, marked_status, sort)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(
|
||
item.get("bookId", ""),
|
||
book.get("title", ""),
|
||
book.get("author", ""),
|
||
book.get("cover", ""),
|
||
item.get("reviewCount", 0),
|
||
item.get("noteCount", 0),
|
||
item.get("bookmarkCount", 0),
|
||
item.get("readingProgress", 0),
|
||
item.get("markedStatus", 0),
|
||
item.get("sort", 0),
|
||
),
|
||
)
|
||
total += 1
|
||
|
||
if data.get("hasMore", 0) == 1 and books:
|
||
last_sort = books[-1].get("sort")
|
||
else:
|
||
break
|
||
time.sleep(0.3)
|
||
|
||
conn.commit()
|
||
print(f" 笔记本: {total} 本有笔记的书")
|
||
|
||
|
||
def main():
|
||
if not API_KEY:
|
||
print("错误: 请先设置 WEREAD_API_KEY 环境变量")
|
||
print(" export WEREAD_API_KEY=wrk-xxxxxxxx")
|
||
return
|
||
|
||
print("=" * 50)
|
||
print("微信读书数据采集")
|
||
print("=" * 50)
|
||
|
||
# 初始化数据库
|
||
print(f"\n[1/4] 初始化数据库: {DB_PATH}")
|
||
conn = init_db(DB_PATH)
|
||
|
||
# 清空旧数据(保留表结构)
|
||
for table in [
|
||
"reading_stats", "read_times", "read_longest", "prefer_categories",
|
||
"prefer_authors", "prefer_time", "read_stat_items",
|
||
]:
|
||
conn.execute(f"DELETE FROM {table}")
|
||
conn.commit()
|
||
|
||
# 拉取书架
|
||
print("\n[2/4] 拉取书架数据")
|
||
fetch_shelf(conn)
|
||
|
||
# 拉取阅读统计(overall + 当前年)
|
||
print("\n[3/4] 拉取阅读统计数据")
|
||
fetch_readdata(conn, "overall", "overall")
|
||
current_year = time.localtime().tm_year
|
||
fetch_readdata(conn, "annually", str(current_year))
|
||
|
||
# 拉取笔记本
|
||
print("\n[4/4] 拉取笔记本数据")
|
||
fetch_notebooks(conn)
|
||
|
||
# 记录采集时间
|
||
c = conn.cursor()
|
||
c.execute(
|
||
"INSERT OR REPLACE INTO fetch_meta (key, value) VALUES (?, ?)",
|
||
("last_fetch_time", time.strftime("%Y-%m-%d %H:%M:%S")),
|
||
)
|
||
conn.commit()
|
||
|
||
# 统计
|
||
c.execute("SELECT COUNT(*) FROM books")
|
||
book_count = c.fetchone()[0]
|
||
c.execute("SELECT COUNT(*) FROM notebooks")
|
||
notebook_count = c.fetchone()[0]
|
||
c.execute("SELECT SUM(total_read_time) FROM reading_stats WHERE mode='overall'")
|
||
total_time = c.fetchone()[0] or 0
|
||
|
||
print("\n" + "=" * 50)
|
||
print("采集完成!")
|
||
print(f" 书架书籍: {book_count} 本")
|
||
print(f" 笔记本: {notebook_count} 本")
|
||
print(f" 总阅读时长: {total_time // 3600} 小时 {(total_time % 3600) // 60} 分钟")
|
||
print(f" 数据库: {DB_PATH}")
|
||
print("=" * 50)
|
||
|
||
conn.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|