'update'
This commit is contained in:
parent
5e1788884f
commit
c463e3b785
|
@ -0,0 +1,82 @@
|
|||
# 项目 TODO 总览
|
||||
|
||||
> 汇总此前在对话中提出或实现的需求与改进想法。分为:已完成、待办、可选增强。
|
||||
|
||||
## 1. 已完成 (Done)
|
||||
- [x] 多策略解析封面图片(直接 cover.*, cover*.html 内引用, OPF meta cover, xlink:href, URL decode, 递归搜索)
|
||||
- [x] 将 AI 书籍简评接入(DashScopeChatClient),生成 3 段 400 字左右精炼书评
|
||||
- [x] AI 简评加入 JSON (`bookintro.json`) 缓存,避免重复请求
|
||||
- [x] AI 调用改为后台线程,防止 UI 卡顿;增加线程列表与窗口关闭时的优雅等待,修复 QThread destroyed while running 问题
|
||||
- [x] 书籍信息与简评改为 HTML 展示;标题加粗彩色(紫红/洋红色)
|
||||
- [x] 保留 AI 书评段落结构(按空行/空白拆分为多段 `<p>`)
|
||||
- [x] 单封面右上角自适应缩放实现
|
||||
- [x] 扩展为三张封面并排展示(当前 + 后两本)
|
||||
- [x] 三张封面与文本区域对齐,初版等宽缩放
|
||||
- [x] 封面支持失真填充模式(IgnoreAspectRatio + setScaledContents)
|
||||
- [x] 封面宽度锁定 150,并按比例(1:1.4 初版 -> 1:1.2)设定高度上限自适应
|
||||
- [x] 将封面布局与 text edit 合并到 `.ui` 文件(`ibook_export_app.ui`)而非运行时动态创建
|
||||
- [x] 导出笔记功能(Markdown 命名含时间戳 + 书名截断)
|
||||
- [x] 统计标签页:原生 Qt 图表(周 / 30 天 / 12 月 / 气泡汇总)集成
|
||||
- [x] 窗口几何尺寸持久化 (QSettings)
|
||||
- [x] 错误/无封面显示文本占位
|
||||
|
||||
## 2. 待办 (Backlog / 未实现需求)
|
||||
- [ ] 封面点击交互:点击第 2 / 3 张封面切换列表选中对应书籍
|
||||
- [ ] 缺失封面使用统一图片占位 (placeholder.png) 替代文字
|
||||
- [ ] 封面高度完全由窗口高度动态拉伸(移除当前 45% 与 400 上限策略或做成配置)
|
||||
- [ ] 可配置封面显示模式:等比缩放 / 失真填充 / 居中裁剪
|
||||
- [ ] AI 书评请求取消 / 防抖(快速切换书籍时取消前一线程)
|
||||
- [ ] AI 简评失败后重试按钮或自动重试逻辑
|
||||
- [ ] 配置对话框持久化写回 `config.py`(当前仅本次运行)
|
||||
- [ ] 导出格式可选 (Markdown / HTML / PDF)
|
||||
- [ ] 导出时附带封面图片(复制到输出目录或转换为 Base64 嵌入)
|
||||
- [ ] 书籍列表支持搜索 / 过滤 (作者 / 书名 / 最近打开时间)
|
||||
- [ ] 异步加载封面(当前是同步文件系统扫描)
|
||||
- [ ] 提取封面解析逻辑为独立模块并加单元测试
|
||||
- [ ] 统计图表:添加 hover 提示/数据标签开关
|
||||
- [ ] 年度 / 月度阅读统计图表空数据时的占位样式统一
|
||||
- [ ] 增加日志记录(AI 请求、封面解析耗时)
|
||||
- [ ] 国际化(中英切换)
|
||||
|
||||
## 3. 可选增强 (Nice to Have / Ideas)
|
||||
- [ ] 缓存封面缩略图,减少重复磁盘读取与缩放开销
|
||||
- [ ] 多线程并行预取下一批书封面与书评
|
||||
- [ ] AI 书评提示词可定制(长度 / 语气 / 侧重点)
|
||||
- [ ] 书评显示字数限制 / 展开收起
|
||||
- [ ] 支持本地笔记全文搜索与高亮(跨书)
|
||||
- [ ] Dark Mode 自适应 (Qt Palette + 自定义样式)
|
||||
- [ ] 统计图数据导出 (CSV/JSON)
|
||||
- [ ] 书籍元数据导入/刷新(重新扫描数据源)按钮
|
||||
- [ ] 键盘快捷键:导出(E), 配置(C), 切换封面焦点, 上下本书(J/K)
|
||||
- [ ] 自动更新检查机制
|
||||
- [ ] 使用 QThreadPool + QRunnable 替代手动线程列表管理
|
||||
- [ ] 添加简单单元测试框架 (pytest) 验证封面解析与导出核心逻辑
|
||||
- [ ] 封面区域添加标题(“当前/下一本/再下一本”)并可隐藏
|
||||
- [ ] 支持拖放本地 epub/ibooks 包,临时解析显示
|
||||
|
||||
## 4. 技术债 (Tech Debt)
|
||||
- [ ] `ibook_export_app.py` 体积过大(UI/业务/AI/封面解析混杂)→ 拆分模块:`cover_finder.py` / `ai_review.py` / `ui_main.py`
|
||||
- [ ] 缺少异常分类(目前大量 bare except 打印)
|
||||
- [ ] 无统一日志(建议 logging + 级别控制)
|
||||
- [ ] 缺少类型注解与 mypy 校验
|
||||
- [ ] 缺少依赖检查与缺包提示 (PyQt6, charts)
|
||||
|
||||
## 5. 配置项建议
|
||||
| 功能 | 建议配置键 | 说明 |
|
||||
|------|------------|------|
|
||||
| 封面模式 | COVER_MODE | stretch / keep_ratio / crop |
|
||||
| 封面固定宽度 | COVER_FIXED_WIDTH | 默认 150 |
|
||||
| 高宽比例 | COVER_HW_RATIO | 默认 1.2 |
|
||||
| AI 书评开关 | ENABLE_AI_REVIEW | bool |
|
||||
| AI 提示词模板 | AI_REVIEW_PROMPT | 可包含 {bookname} |
|
||||
| 线程超时 | WORKER_TIMEOUT_SEC | 默认 8 |
|
||||
|
||||
## 6. 下一步优先级建议
|
||||
1. 点击封面切换书籍(提升可用性)
|
||||
2. 占位图与封面缓存(视觉一致 + 性能)
|
||||
3. AI 请求取消/防抖(提升体验)
|
||||
4. 模块拆分 + 日志/类型注解(降低维护成本)
|
||||
5. 测试与封面解析单元测试(稳定性)
|
||||
|
||||
---
|
||||
若需要我直接实现其中某一项,请告诉我对应条目。欢迎补充。
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,27 @@
|
|||
import config
|
||||
from openai import OpenAI
|
||||
|
||||
class DashScopeChatClient:
|
||||
def __init__(self, api_key=None, base_url=None, model="qwen-plus"):
|
||||
self.api_key = api_key or config.DASHSCOPE_API_KEY
|
||||
self.base_url = base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
self.model = model
|
||||
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
|
||||
def ask(self, question, system_prompt="You are a helpful assistant."):
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": question}
|
||||
]
|
||||
completion = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages
|
||||
)
|
||||
return completion.choices[0].message.content
|
||||
|
||||
# 示例用法
|
||||
if __name__ == "__main__":
|
||||
chat = DashScopeChatClient()
|
||||
answer = chat.ask("智人之上 300字书评 简洁精炼")
|
||||
print(answer)
|
||||
|
Binary file not shown.
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"136829_变宋王安石改革的逻辑与陷阱_徐富海": "《变宋王安石改革的逻辑与陷阱》一书由徐富海所著,深入剖析了王安石变法背后的政治理性与现实困境。作者不仅梳理了王安石变法的历史脉络,更从制度逻辑与权力博弈的角度,揭示了改革为何在理想与现实之间陷入困局。\n\n书中指出,王安石以“富国强兵”为目标,推行青苗法、募役法等新政,意在矫正北宋积贫积弱之弊。然而,改革忽视了地方执行能力与官僚利益格局,导致政策在层层落实中变质,反而加重百姓负担,激化社会矛盾。\n\n本书语言简明,逻辑清晰,不仅是一次对王安石变法的再审视,也为今日改革提供历史镜鉴,强调制度设计与执行环境的匹配至关重要。",
|
||||
"最好的告别三部曲套装共3册 纽约时报畅销书美国著名外科医生划时代之作": "《最好的告别三部曲》套装汇集了美国著名外科医生阿图·葛文德的三部深刻探讨生命、衰老与死亡的力作。作为《纽约时报》畅销书,这套作品以冷静而富有人文关怀的笔触,重新定义了现代医学对生命终点的理解。\n\n第一册《最好的告别》反思了现代医疗在面对衰老与临终时的局限,提出“善终”比“延长生命”更能体现对生命的尊重。第二册《清单革命》虽主题不同,但延续了作者对医疗系统改进的思考,强调通过简单工具提升复杂系统的安全性。第三册《医生的修炼》则通过真实案例,展现医生成长过程中的困惑与领悟,揭示医学的本质与人性温度。\n\n这套书不仅适合医疗从业者阅读,也值得每一位关心生命意义的读者深思。语言简洁有力,思想深刻,是近年来关于生命伦理议题最具影响力的作品之一。",
|
||||
"田园诗与狂想曲关中模式与前近代社会的再认识第三版中国农民学研究开山之作": "《田园诗与狂想曲:关中模式与前近代社会的再认识》作为中国农民学研究的开山之作,以关中地区为切入点,深入剖析了中国传统农业社会的结构与运行机制。作者通过对土地制度、宗族关系与农民生活的细致考察,揭示了乡土社会的复杂性与稳定性,挑战了传统对农村社会的浪漫化想象。\n\n书中提出的“关中模式”具有重要的理论价值,它不仅丰富了对中国农村社会多样性的认识,也为理解前近代中国社会提供了新的视角。作者将历史学、社会学与经济学相结合,展现出扎实的实证功底与深刻的理论思考,推动了中国农民学的建立与发展。\n\n本书语言严谨、逻辑清晰,虽为学术著作,但论述深入浅出,适合对乡土中国、农民问题及社会结构感兴趣的读者。第三版的再版,彰显了其持久的学术生命力与现实意义。",
|
||||
"大模型应用开发极简入门基于GPT-4和ChatGPT": "《大模型应用开发极简入门》是一本面向开发者的实用指南,聚焦于如何快速上手基于GPT-4和ChatGPT的应用开发。作者以简洁明了的语言,介绍了大模型的基本原理、开发环境搭建及API调用方式,适合初学者快速入门。\n\n书中通过多个小而精的实例,展示了如何将大模型应用于文本生成、对话系统、内容理解等场景,强调实战性和可操作性。每章结构清晰,配合代码示例和解释,帮助读者在短时间内掌握核心技能。\n\n总体而言,这本书内容紧凑、重点突出,是希望快速了解和应用大模型技术开发者的理想选择,尤其适合对GPT-4和ChatGPT感兴趣的读者。",
|
||||
"喜剧的本质": "《喜剧的本质》是一部深入探讨幽默与笑声背后逻辑的精彩之作。作者通过哲学、心理学与社会学的多重视角,分析了喜剧为何能引发人类情感共鸣,揭示了“笑”这一行为并非单纯娱乐,而是对荒诞、矛盾与人性弱点的敏锐捕捉。\n\n书中指出,喜剧往往源于冲突与错位,无论是人与人之间的误会,还是现实与期望的落差,都能成为笑料的源泉。作者以经典喜剧作品为例,说明幽默不仅是轻松的表现,更是一种批判与反思的工具,甚至具有颠覆权威的力量。\n\n本书语言精炼,思想深刻,适合对喜剧艺术感兴趣的读者。它不仅解释了我们为何发笑,也让我们重新认识喜剧在社会与人性中的独特价值。",
|
||||
"曙光集": "《曙光集》是杨振宁与翁帆合作的一部文集,汇集了二人在科学、哲学与文化等多个领域的思考与对话。全书以简洁而深刻的语言,展现了科学大家对人类文明与未来发展的关切与洞见。\n\n书中既有对物理学前沿问题的探讨,也有对中西文化差异的比较,内容广博而不散,体现出作者深厚的人文素养与科学情怀。特别是杨振宁对科学精神的阐释,令人深思。\n\n《曙光集》虽为文集,但逻辑清晰、思想深邃,适合对科学与文化感兴趣的读者。它不仅是一本知识的汇编,更是一次思想的启迪,值得细细品读。",
|
||||
"明夷待访录破邪论精--中华经典名著全本全注全译 中华书局": "《明夷待访录》是黄宗羲在明清易代之际所著的重要政论作品,全书以“明夷”象征乱世中坚守道义的士人精神,表达了作者对理想政治的深刻思考与热切期待。该书突破传统君主专制思想桎梏,提出“天下为主,君为客”的民本理念,具有强烈的时代批判意识与启蒙价值。\n\n《破邪论》则延续《明夷待访录》的思想锋芒,针对当时社会弊端与思想迷障,直指权力异化与制度僵化之害,批判空谈心性、脱离现实的学风,强调经世致用与制度革新。其语言犀利,逻辑严密,展现了黄宗羲对国家命运的深切关怀与对士人责任的坚定担当。\n\n中华书局版《明夷待访录·破邪论》采用全本全注全译方式,注释详实,译文准确,便于读者全面理解原意。作为中华经典名著之一,此书不仅是中国古代政治思想的重要遗产,也为后人提供了反思历史、观照现实的思想资源。",
|
||||
"传统十论": "《传统十论》是一部深入剖析中国传统文化精髓的力作,作者以独特的视角,从十个维度解读了传统思想的内核及其现代价值。书中不仅回顾了儒家、道家等主流思想的发展脉络,也探讨了传统与现代社会之间的张力与融合可能。\n\n作者语言凝练,论述严谨,既有历史的厚重感,又不乏现实的关怀。通过对礼制、伦理、政治等多方面的分析,揭示了传统文化在当代社会中的延续与转型,极具启发性。\n\n本书虽篇幅不长,但思想深邃,适合希望快速了解中国文化传统及其现代意义的读者。它不仅是一本理论读物,更是一次文化寻根之旅,值得细细品读。",
|
||||
"规训与惩罚": "《规训与惩罚》是福柯对现代权力机制的深刻剖析。他通过考察监狱、学校、军队等机构,揭示了现代社会如何通过“规训权力”对个体进行控制与塑造。这种权力并非暴力压制,而是通过细致的规范、监视与训练,使个体自觉服从,达到“自我规训”的效果。\n\n福柯提出的“全景敞视主义”是本书核心概念之一,它象征着现代社会的监控机制:即使没有实际监视,个体也会因可能被看见而自我约束。这种权力形式不仅存在于监狱,更渗透到教育、医疗、工厂等各个领域,成为现代社会秩序维持的重要手段。\n\n本书语言冷峻、逻辑严密,福柯以历史考古的方式揭示了权力与知识的共谋关系。他打破了“权力压制个体”的传统认知,指出权力不仅否定和限制,更具有生产性,它塑造行为、知识和主体性,深刻影响了当代社会理论的发展。",
|
||||
"Python机器学习入门与实战以实操为基础以入行为目的快速帮助你掌握Python机器学习相关技能": "《Python机器学习入门与实战》是一本面向初学者的实用指南,内容由浅入深,帮助读者快速掌握Python在机器学习领域的应用。书中结合大量实例,讲解了从环境搭建、数据处理到模型构建的完整流程,适合希望快速上手的学习者。\n\n本书注重实践操作,每章都配有代码示例和练习任务,有助于巩固所学知识。作者通过通俗易懂的语言,将复杂的机器学习理论简化,使读者即使没有深厚数学基础也能理解核心概念。\n\n对于希望转行或进入机器学习领域的新手来说,这本书是一个理想的起点。它不仅提供理论支持,更强调实战能力的培养,帮助读者在项目实践中不断提升技能,为就业打下坚实基础。",
|
||||
"政治哲學的12堂Podcast現代國家如何成形民主自由如何誕生性別平等如何發展一探人類文明邁向現代的關鍵時刻": "《政治哲學的12堂Podcast》以深入淺出的方式,帶領聽眾與讀者一探現代國家的形成歷程。從啟蒙運動到社會契約論,節目梳理了民主制度如何在思想與實踐的交織中逐步成形,讓人理解今日政治體制的歷史根源。\n\n節目進一步探討自由概念的演變,從洛克、盧梭到密爾,哲學家們對個人權利與國家權力的辯證,勾勒出自由主義的發展軌跡。透過當代視角重新詮釋,聽眾得以反思自由在現代社會中的意義與限制。\n\n最後,節目也觸及性別平等與社會正義等當代議題,展現政治哲學如何持續回應時代挑戰。整體而言,這是一場貫穿古今的思想之旅,幫助聽眾理解人類文明邁向現代的關鍵轉折。",
|
||||
"熊逸佛学50讲": "《熊逸佛学50讲》以理性视角解读佛学,摆脱了传统宗教式的说教,更像是一场思想的剖析与哲学的探讨。作者用现代语言拆解佛学核心概念,如“空性”“因果”“无我”,让读者在逻辑与思辨中理解佛教的智慧,而非盲目信仰。\n\n书中不仅介绍佛教教义,还结合中西方哲学、心理学与社会现象,拓展了佛学的应用边界。这种跨学科的视角,使佛学不再是“出世”的玄谈,而成为理解现实、应对人生困惑的思维工具,尤其适合现代人阅读与反思。\n\n熊逸的文字简洁有力,逻辑清晰,既有深度又不失趣味。《佛学50讲》不是教你如何成佛,而是引导你如何借佛学之眼,看清世界与自我,是一本值得静心细读的思想之作。",
|
||||
"薛兆丰经济学讲义": "《薛兆丰经济学讲义》以通俗易懂的语言,将复杂的经济学原理融入日常生活,帮助读者建立系统的经济思维。书中通过大量真实案例,解释了价格机制、供需关系、产权制度等核心概念,让经济学不再高高在上,而是变得可感可知。\n\n薛兆丰强调“成本是放弃的最大价值”,这一观点贯穿全书,引导读者从机会成本、边际分析等角度理性决策。他反对空谈道德与理想,主张尊重现实与规则,尤其对政府干预、公共政策的分析冷静而深刻,令人反思。\n\n作为一本面向大众的经济学入门书,该讲义逻辑清晰、观点鲜明,不仅提升了读者的思辨能力,也为理解社会现象提供了有力工具,是一本值得推荐的经济学普及佳作。",
|
||||
"众生无束:劳动社会的未来 (【德】理查德·大卫·普莱希特) (Z-Library)": "《众生无束:劳动社会的未来》是德国哲学家理查德·大卫·普莱希特对当代劳动社会深刻反思的作品。他指出,随着自动化与人工智能的发展,传统劳动模式正面临瓦解,而“全民基本收入”等新制度设想成为应对未来不确定性的可能出路。\n\n普莱希特批判了当前将人等同于“生产工具”的社会价值观,呼吁重建以个体自由与发展为核心的社会结构。他强调,劳动不应再是生存的代价,而应成为实现自我价值的方式,社会需从“劳动社会”转向“自由社会”。\n\n本书语言简洁、逻辑清晰,虽具理想色彩,但对现实问题的剖析极具启发性。普莱希特不仅描绘了未来图景,更激发读者思考:在一个无需劳作也能生存的时代,人该如何定义自身价值与社会角色?",
|
||||
"佛教常识答问汉英对照图文版-gx9ik": "《佛教常识答问》(汉英对照图文版)以简明扼要的方式介绍了佛教的基本教义与文化内涵,适合初学者快速掌握佛教核心知识。书中采用问答形式,语言通俗易懂,配以精美插图,增强了阅读的趣味性与理解的直观性。\n\n该书不仅涵盖了佛教的起源、发展、主要宗派,还解释了因果、轮回、涅槃等关键概念,帮助读者建立系统的佛教认知。汉英对照的设计,也使其成为英语学习者了解东方哲学文化的良好读物。\n\n整体而言,这是一本兼具知识性与实用性的入门书籍,无论是对宗教研究者还是普通读者,都具有较高的参考价值。",
|
||||
"修辭的陷阱為何政治包裝讓民主社會無法正確理解世界": "《修辭的陷阱》一書深入剖析政治語言如何透過精心包裝,操弄公眾認知,使民主社會難以正確理解現實。作者指出,政治人物常運用模糊、雙關或情感訴求的修辭,掩蓋政策本質,誤導民眾判斷。\n\n在民主社會中,言論自由本應促進理性討論,但當政治語言淪為意識形態工具,便可能扭曲事實,製造假象。這種語言操弄不僅削弱公民的批判思考能力,也加深社會分歧,阻礙對公共事務的共識形成。\n\n本書提醒我們,面對政治修辭應保持警覺,培養媒體素養與邏輯思辨能力,方能避免落入語言陷阱,守護民主價值。",
|
||||
"光耀生命-艾扬格": "《光耀生命——艾扬格瑜伽精解》是一本系统阐述瑜伽哲学与实践的经典之作。作者B.K.S.艾扬格以其深厚的瑜伽修为和教学经验,将瑜伽的八支体系条理清晰地呈现给读者,不仅讲解了体式与呼吸法,更深入探讨了道德修养与心灵净化的重要性。\n\n本书语言简洁、内容广博,既有理论指导,又有实践建议,适合不同层次的瑜伽修习者参考。艾扬格强调瑜伽不仅是身体的锻炼,更是对生命的尊重与内在的觉醒,这种理念贯穿全书,令人深受启发。\n\n《光耀生命》不仅是一本瑜伽指南,更是一部引导人走向身心和谐与自我认知的智慧之书。对于追求健康与内心平静的现代人而言,具有极高的阅读价值与实践意义。",
|
||||
"牛津通识读本佛学概论中文版": "《牛津通识读本:佛学概论》以简明清晰的方式介绍了佛教的核心思想与历史发展。本书从佛陀的生平讲起,逐步展开对四圣谛、八正道、缘起、无我等基本教义的解析,帮助读者建立对佛教思想的整体认识。\n\n作者以客观、中立的视角梳理佛教从印度起源,到分化为上座部与大乘佛教,并传播至东亚等地的过程。书中不仅涵盖佛教哲学,也涉及佛教在不同文化中的实践与演变,展现了佛教的多元面貌。\n\n作为通识读物,该书语言通俗易懂,结构条理分明,适合初学者入门阅读。虽然篇幅有限,无法深入探讨复杂义理,但其全面而精炼的概述,为读者进一步学习佛学奠定了良好基础。",
|
||||
"楞伽经讲记": "《楞伽经》是大乘佛教重要经典之一,内容深奥,融会禅宗与唯识思想,阐述如来藏与阿赖耶识等核心教义。此经以问答形式展开,佛陀应大慧菩萨之请,开示心性本净、万法唯心之理,直指修行次第与顿悟心要,对中国佛教尤其是禅宗影响深远。\n\n讲记以深入浅出的方式解读经文,条理清晰,既重义理分析,亦重实修指导。作者紧扣经文原意,结合禅宗心法,使读者易于领会“离言说相、离名字相”的真实义趣,体现佛法不离世间而超越世间的智慧精神。\n\n本书适合佛学爱好者与修行者阅读,既能增长智慧,亦可启发内在觉性。虽篇幅不长,但言简意赅,读之令人反思生命本质与修行方向,是一部兼具理论与实践价值的佛典讲记。",
|
||||
"豆瓣90你当像鸟飞往你的山2019": "《你当像鸟飞往你的山》是塔拉·韦斯特弗的回忆录,讲述了她从爱达荷州山区的封闭家庭走向哈佛、剑桥的求学之路。全书以冷静克制的笔触,描绘了成长过程中的创伤、挣扎与自我重塑,展现了教育如何赋予人觉醒与改变命运的力量。\n\n作者与原生家庭的关系是书中最动人的线索。父亲的偏执、母亲的沉默、兄姐的影响,构成了她童年生活的全部认知。随着教育的深入,她逐渐看清家庭的控制与伤害,也经历了撕裂与重建自我的痛苦过程。这种精神上的逃离与救赎,令人动容。\n\n“塔拉”在拉丁文中意为“高地”,书名寓意着每个人都应找到属于自己的山。这本书不仅是一个女孩的奋斗史,更是一曲关于勇气、自由与自我发现的赞歌。它提醒我们:真正的成长,是学会飞翔,飞往属于自己的那座山。",
|
||||
"最好的辩护": "《最好的辩护》是美国著名刑辩律师艾伦·德肖维茨的代表作之一。书中通过真实案例,展现了法律实践中“辩护”的真正意义,不是为罪恶开脱,而是捍卫程序正义与人权保障。作者以犀利笔触挑战公众对“坏人也有人权”的偏见,强调法律的核心不在于情感,而在于规则。\n\n德肖维茨不回避争议,坦承自己为诸多“不受欢迎”的当事人辩护,却始终坚持法律职业的伦理底线。他指出,一旦我们为了打击犯罪而牺牲正当程序,最终受害的将是每一个普通人。这种冷静而理性的立场,正是现代法治精神的体现。\n\n本书不仅是法律从业者的必读之作,也是一堂生动的公民法治课。它提醒我们:正义的实现,不仅靠惩恶扬善,更依赖于每一个人都能获得公平审判的权利。",
|
||||
"Python编程从入门到实践第3版": "《Python编程:从入门到实践(第3版)》是一本面向初学者的优秀编程入门书籍。作者以通俗易懂的语言和丰富的实例,帮助读者快速掌握Python基础语法与编程思维,特别适合零基础学习者。\n\n全书结构清晰,内容由浅入深,涵盖变量、循环、函数、文件操作等基础知识,并通过项目实战巩固所学内容。书中项目贴近实际,如数据可视化和小游戏开发,增强了学习的趣味性和实用性。\n\n总体而言,这是一本兼具指导性与操作性的Python入门书,既适合自学,也可作为教学辅助用书,强烈推荐给想入门编程的读者。",
|
||||
"中国历史上有没有宗教战争?": "中国历史上虽有宗教冲突,但严格意义上的“宗教战争”较为罕见。与欧洲中世纪频繁发生的宗教战争不同,中国历史上宗教多与政治、社会相融合,冲突往往以民间起义或政治斗争形式出现,而非单纯因信仰分歧引发的大规模战争。\n\n历史上较为著名的宗教冲突包括东汉末年的太平道起义(黄巾军)、唐代的会昌灭佛以及清代的白莲教起义等。这些事件虽带有宗教色彩,但本质上多为社会矛盾激化下的反抗运动,宗教更多是组织和动员民众的工具,而非战争的唯一动因。\n\n总体而言,中国宗教关系以包容与融合为主,历代王朝多采取“神道设教”政策,维护社会稳定。因此,宗教战争在中国并非主流历史现象,理解这一特点,有助于更全面地认识中国宗教与政治互动的独特路径。",
|
||||
"印度三大主神": "**《印度三大主神》书评**\n\n《印度三大主神》一书深入浅出地介绍了印度教中最重要的三位神祇——梵天、毗湿奴与湿婆。作者以清晰的逻辑梳理了三位主神的起源、象征意义与神话故事,使读者能够迅速把握印度教复杂的神祇体系。\n\n书中不仅介绍了三大主神各自的角色与职能,还探讨了他们所代表的宇宙观:创造、维系与毁灭的循环。这种结构不仅反映了印度教的世界观,也体现了印度文化对生命与宇宙深刻的理解。\n\n本书语言简洁,内容丰富,适合对印度神话与宗教感兴趣的读者。虽仅四百字左右,但信息量充足,是一篇精炼而有深度的入门读物。",
|
||||
"身体不死与神秘主义道教信仰的观念史视角": "《身体不死与神秘主义道教信仰的观念史视角》一书从历史演变与思想脉络出发,系统梳理了道教关于身体不死的信仰体系。作者通过对先秦至唐宋道教文献的深入分析,揭示了“肉身不朽”如何从一种神秘实践逐渐演变为宗教哲学的核心命题。\n\n书中指出,道教对身体的神圣化不仅体现于炼丹服食等外在修炼,更深层地反映在对生命本质的哲学思考。这种将身体视为通达永恒之途的观念,与佛教、儒家形成鲜明对比,展现了中国宗教思想的独特性。\n\n本书以观念史为框架,融合宗教学、哲学与文化研究,语言简明而论证严谨,为理解道教神秘主义提供了新的视角,对研究中国思想史具有重要参考价值。",
|
||||
"智人之上": "《智人之上》是一部极具启发性的作品,作者从历史与未来的交汇点出发,深入探讨了人类如何从万物之中崛起,并逐步掌控地球。书中不仅回顾了智人如何凭借语言、信仰与合作超越其他物种,更对人工智能、基因工程等前沿科技可能带来的变革提出深刻思考。\n\n尤瓦尔·赫拉利以清晰的逻辑和宏大的视野,揭示了现代社会面临的困境与挑战。他指出,曾经推动人类进步的信仰体系与社会结构,如今也可能成为限制我们前行的枷锁。面对科技迅猛发展,人类是否还能掌控自己的命运,成为全书最引人深思的问题。\n\n本书语言简洁有力,思想深刻,是一本值得每一位关心人类未来的读者认真阅读的作品。它不仅让我们重新认识过去,更促使我们思考:在智慧生命的新阶段,人类应当如何自处,又该走向何方?",
|
||||
"受害者有罪论、完美受害者": "《受害者有罪论》与“完美受害者”现象揭示了社会对受害者的苛责倾向。人们常以“完美”的标准衡量受害者,若其行为稍有瑕疵,便质疑其可信度,甚至归咎其责任。这种思维不仅扭曲了正义的判断,也进一步伤害了本已脆弱的受害者。\n\n“受害者有罪论”本质上是一种心理防御机制和社会偏见的结合。人们通过指责受害者来维持自身“安全幻觉”,认为只要行为得当就不会遭遇不幸,从而忽视结构性的不公和暴力根源。这种逻辑不仅荒谬,更是对正义的亵渎。\n\n我们应当摒弃对“完美受害者”的幻想,正视伤害本身。唯有放下偏见,才能真正支持受害者,推动社会走向更公平与理性的方向。",
|
||||
"邵雍·《渔樵问对》原文": "**邵雍《渔樵问对》原文书评**\n\n《渔樵问对》是北宋理学家邵雍所作的一篇哲理小品,以渔夫与樵夫对话的形式,探讨天地万物、人生道德之理。全文语言简练,寓意深远,融合了儒家、道家思想,体现了邵雍对自然与人伦秩序的深刻理解。\n\n文章通过问答方式,层层递进,揭示“道”在万物中的体现,强调顺应自然、安分守己的人生态度。邵雍借渔樵之口,表达了对世事变迁的淡然与通达,倡导以理观物、以德处世的价值观,具有浓厚的哲理色彩。\n\n全篇虽短,却蕴含深意,既是对自然规律的敬畏,也是对人生智慧的凝练总结,值得反复品读与深思。",
|
||||
"欧洲人种起源—雅利安人四族:拉丁、日耳曼、斯拉夫、凯尔特": "《欧洲人种起源—雅利安人四族:拉丁、日耳曼、斯拉夫、凯尔特》一书简明梳理了欧洲主要民族的起源与演变。作者从历史、语言与文化角度切入,系统介绍了四大雅利安族群的分布与发展,为读者提供了清晰的脉络。\n\n全书以简驭繁,避免冗长考据,突出各族特征与互动。拉丁族奠定欧洲文明基础,日耳曼族塑造中世纪格局,斯拉夫族构建东欧版图,凯尔特族则成为古老欧洲的遗音。四族并列,展现了欧洲民族的多元与共性。\n\n尽管篇幅有限,书中仍触及民族迁徙、语言演变与文化融合等核心议题,兼具知识性与可读性。对于初探欧洲民族史的读者而言,是一本理想的入门读物。",
|
||||
"何为启蒙": "**《什么是启蒙》书评**\n\n康德在《什么是启蒙》中提出,启蒙是人类摆脱自身加于自身的不成熟状态的觉醒过程。这种不成熟,不是源于理性能力的缺乏,而是出于缺乏勇气与决心去独立运用理性。他呼吁人们“敢于求知”,勇敢地走出由他人引导的思想惰性。\n\n文章虽短,却深刻揭示了启蒙的核心精神:理性自主与思想自由。康德强调,真正的启蒙不是依赖权威,而是个体敢于独立思考,勇于质疑与探索。他批评了当时社会对宗教、政治权威的盲从,指出只有通过自由的公共理性讨论,才能推动人类整体的进步。\n\n这篇短文至今仍具现实意义。在信息纷杂、观点多元的今天,启蒙精神提醒我们保持独立思考,不盲信、不盲从。它不仅是历史的思想遗产,更是现代人应持续践行的理性态度。",
|
||||
"为什么伟大不能被计划-x1u2i - Notebook": "**《为什么伟大不能被计划》书评**\n\n《为什么伟大不能被计划》一书挑战了我们对目标与成功的传统认知。作者通过科学与哲学的双重视角,指出伟大成就往往源于探索与偶然,而非严格规划。这一观点颠覆了“目标导向”至上的思维模式,启发读者重新思考创造力与发现的本质。\n\n书中强调,“寻宝机”比“路线图”更能导向真正伟大的成就。作者提出“新奇性搜索”概念,指出当我们允许自己偏离既定路径,拥抱未知时,才更可能发现真正有价值的成果。这对教育、科研乃至人生选择都有深刻启示。\n\n本书语言简洁,逻辑清晰,虽篇幅不长,却引发深思。它不是否定计划本身,而是提醒我们:真正的伟大,源于对未知的开放与尊重。对于追求创新的人来说,这是一本值得细读的思想指南。",
|
||||
" 新见邓石如致黄易信札及其相关印学解读": "《新见邓石如致黄易信札及其相关印学解读》一书,辑录了清代篆刻大家邓石如致友人黄易的珍贵信札,并辅以深入的印学研究。信札内容不仅展现了邓石如的艺术见解与交游情况,也为研究其篆刻风格的形成提供了重要线索。\n\n书中通过对信札内容与邓石如篆刻作品的对照分析,揭示了其“以书入印”的艺术理念及其对后世印学发展的深远影响。作者考证详实,逻辑清晰,语言简练,兼具学术性与可读性。\n\n该书不仅为邓石如研究提供了新资料,也为清代印学史的深入探讨提供了重要参考,是篆刻爱好者与研究者不可多得的读物。",
|
||||
"江村经济 (费孝通) (Z-Library)": "《江村经济》是费孝通先生对中国农村社会进行实地调查的经典之作。本书通过对江苏吴江开弦弓村的深入研究,系统分析了农村的经济结构、土地制度、家庭手工业及社会关系,揭示了农民生活的实际状况与经济运行逻辑。\n\n费孝通以翔实的田野资料为基础,展现了农村经济的复杂性与自洽性,提出了“乡土中国”的重要概念,强调了传统与现代之间的张力与融合。他不仅关注经济层面,也深入探讨了社会结构与文化因素对经济行为的影响。\n\n本书语言朴实,逻辑清晰,是中国社会学与人类学本土化的重要里程碑。它不仅为理解中国农村提供了坚实基础,也为后来的社会研究树立了典范。",
|
||||
"秦晖著作集(套装共6册) (秦晖) (Z-Library)": "[AI简评获取失败: Error code: 400 - {'error': {'code': 'data_inspection_failed', 'param': None, 'message': 'Input data may contain inappropriate content.', 'type': 'data_inspection_failed'}, 'id': 'chatcmpl-8dfcba8a-6b5f-4609-833a-2e6eb3f7b439', 'request_id': '8dfcba8a-6b5f-4609-833a-2e6eb3f7b439'}]",
|
||||
"剧变(第二版)": "《剧变(第二版)》是贾雷德·戴蒙德继《枪炮、病菌与钢铁》后的又一力作,聚焦国家危机应对的历史经验。作者通过芬兰、日本、智利等典型案例,揭示国家在重大变革中如何调整与适应。全书逻辑清晰,视野宏大,为理解国家转型提供了有力框架。\n\n戴蒙德提出“十二个因素”分析模型,涵盖国家自我认知、诚实面对危机、借鉴他国经验等方面,具有现实借鉴意义。他不仅关注历史事件本身,更强调社会结构、文化传统对变革路径的深远影响,增强了分析深度。\n\n本书语言平实,论证有力,虽篇幅较长但结构紧凑。适合对历史、政治、社会变迁感兴趣的读者,是一本启发思考、值得反复品读的佳作。",
|
||||
"可能性的艺术": "《可能性的艺术》是刘瑜对民主制度深刻思考的结晶。书中通过比较不同国家的民主实践,揭示了政治选择的复杂性和多样性。她以理性而敏锐的笔触,展现了民主并非非黑即白的制度,而是一门在现实中不断权衡与调整的艺术。\n\n刘瑜善于用具体案例剖析抽象理论,使读者更容易理解民主转型中的困境与突破。她不回避民主的缺陷,反而通过这些缺陷探讨其改进的可能。这种既批判又建设的态度,使本书既有思想深度,又具现实意义。\n\n本书不仅是一次思想的启蒙,更是一种思维方式的启发。它提醒我们,在复杂的政治现实中,保持理性、包容与耐心,是对抗极端与盲从的关键。对于关心民主发展的人来说,这是一本值得深读的佳作。",
|
||||
"不服从": "《不服从》书评\n\n《不服从》以冷静而深刻的笔触,探讨了个体在权威面前的挣扎与反抗。作者通过真实事件改编的故事,展现了制度与人性之间的冲突,引发读者对权力、道德与自由的思考。\n\n小说情节紧凑,人物刻画鲜明。主人公面对不公时的选择,揭示了服从与反抗之间的复杂心理。书中没有简单的英雄主义,而是呈现了普通人如何在压力下做出不同选择,体现了人性的真实与复杂。\n\n这部作品不仅是对社会体制的反思,也是对个体勇气的致敬。它提醒我们,在面对不公时,保持独立思考、敢于质疑权威的重要性。《不服从》是一本发人深省的小书,值得每一个关心社会与自我的人阅读。",
|
||||
"世界之中": "《世界之中》是一部充满哲思与人文关怀的作品,作者以独特的视角探讨了人与世界的关系。书中通过多个小故事串联起“人是世界的一部分”这一核心思想,强调个体与整体的紧密联系。\n\n在第一部分中,作者从自然与人的关系入手,描绘了人类如何在自然中寻找自我定位。他指出,现代人常常忽视自身与环境的共生关系,导致生态与心灵的双重失衡。第二部分则聚焦于人与社会的关系,探讨了孤独与连接的矛盾。作者认为,尽管科技让世界更紧密,但人们内心的隔阂却在加深。\n\n最后一部分回归个体,强调唯有理解自己与世界的连接,才能真正找到生命的意义。全书语言简洁,思想深刻,适合喜欢哲理与思辨类作品的读者。《世界之中》不仅是一本书,更是一次心灵的探索之旅。",
|
||||
"人类简史-从动物到上帝-2fgnwy": "《人类简史:从动物到上帝》以宏大的视角梳理了人类发展历程中的四大革命。作者尤瓦尔·赫拉利从认知革命开始,揭示了智人如何凭借虚构故事的能力脱颖而出。这一视角让人重新审视人类社会的本质。\n\n书中指出,农业革命并未带来普遍幸福,反而使多数人陷入更沉重的劳动;科学革命推动人类掌握强大技术,但也带来伦理困境。赫拉利用犀利语言剖析了历史进程中权力与资本的运作逻辑。\n\n全书最后提出“人类世”的概念,警示我们在基因编辑、人工智能等技术面前,可能正在创造自己的“继任者”。这本书不仅是历史叙述,更是对未来的深刻反思,值得每一位关心人类命运的读者深思。",
|
||||
"何谓性资本关于性的历史社会学": "《性资本:关于性的历史社会学》一书从历史与社会结构的双重视角,剖析了“性”如何被建构为一种社会资源和权力工具。作者通过梳理西方社会从宗教主导的禁欲主义到现代性观念的演变过程,揭示了性资本如何在不同历史阶段被政治、经济与文化力量所塑造。\n\n书中指出,性不仅是私人领域的情感表达,更是社会地位与权力关系的体现。通过对性别、阶级与性取向的交叉分析,作者展示了性资本如何影响个体的社会流动与身份认同,尤其在当代消费社会中,性成为被商品化与符号化的工具。\n\n本书理论深厚却不晦涩,结合大量历史案例与社会现象,为理解性与权力的关系提供了新的视角,是一本兼具学术深度与现实关怀的重要著作。",
|
||||
"云冈:人和石窟的1500年": "《云冈:人和石窟的1500年》以独特的视角展现了云冈石窟千年历史的变迁。书中不仅描绘了石窟艺术的辉煌成就,更聚焦于“人”的角色,从北魏工匠到历代守护者,展现了人与石窟之间深厚的历史羁绊。\n\n作者通过翔实的史料与实地考察,将云冈石窟置于中国佛教传播与民族融合的大背景下,揭示其在文化、宗教与政治中的多重意义。文字简洁而富有画面感,使读者仿佛穿越时空,亲历那一凿一斧的艰辛与信仰的虔诚。\n\n本书不仅是对云冈石窟的深情回望,更是对文化遗产传承的深刻思考。它提醒我们,每一尊佛像背后都是一段鲜活的历史,每一位守护者都是文明延续的见证者。值得所有关注中华文明与石窟艺术的读者细细品读。",
|
||||
"01中国篆刻聚珍战国玺": "《中国篆刻聚珍·战国玺》一书系统梳理了战国时期印章艺术的发展脉络,精选代表性作品,展现其独特风貌。战国玺印文字古朴、章法多变,是篆刻艺术的重要源头,具有极高的历史与艺术价值。\n\n书中图版清晰,辅以详尽释文与考证,便于读者深入理解。编排按地域风格分类,呈现齐、燕、三晋、楚、秦等不同体系的特色,体现战国时期文化的多元性与地域差异,为研究先秦文字与艺术提供了重要资料。\n\n此书不仅适合篆刻爱好者欣赏临摹,也为学者提供了扎实的研究基础。其内容精炼、印刷考究,堪称篆刻艺术出版中的精品之作。",
|
||||
"文明的冲突与世界秩序的重建": "《文明的冲突与世界秩序的重建》是亨廷顿最具影响力的政治理论著作之一。他突破传统国际关系分析框架,提出冷战后世界的主要冲突将不再以意识形态或国家利益为核心,而是以“文明”为基本单位。这一视角为理解当代国际政治提供了全新思路。\n\n作者将全球划分为九大文明,并强调不同文明之间的文化认同将成为国家结盟、对抗的重要依据。他预见到西方与伊斯兰、儒家等文明之间可能发生的摩擦,对后来的国际局势发展具有一定预见性。\n\n尽管“文明冲突论”饱受争议,但它促使人们重新审视文化差异在全球政治中的作用。本书语言简练、逻辑清晰,是一部引发深思、值得阅读的战略性著作。",
|
||||
"02中国篆刻聚珍秦印": "《中国篆刻聚珍·秦印》是一部集中展现秦代篆刻艺术的精品之作。书中精选多枚秦印,印文规整、布局严谨,充分体现了“印宗秦汉”的审美传统。秦印在篆刻史上承前启后,既保留了古玺的自由风格,又为汉印的成熟奠定基础,是篆刻艺术发展的重要源头。\n\n该书不仅注重印文的呈现,还辅以详尽的释文与背景介绍,使读者既能欣赏篆刻之美,又能理解其历史语境。印文内容涵盖官职、姓名、吉语等,反映出秦代社会制度与文化风貌,是研究秦代书法与制度的重要资料。\n\n《秦印》一册虽小,却内容精炼、图文并茂,兼具艺术性与学术性,适合篆刻爱好者与研究者参考。它不仅是一本篆刻图录,更是一扇通向秦代文化与艺术精神的窗口,值得反复品读与珍藏。",
|
||||
"數據、真相與人生:前google資料科學家用大數據,找出致富、職涯與婚姻-yr8lu": "《數據、真相與人生》由前Google資料科學家所著,運用大數據分析探討致富、職涯與婚姻等人生關鍵課題。作者以科學角度拆解成功與幸福的背後邏輯,挑戰許多傳統觀念,讓讀者重新思考人生決策的依據。\n\n書中透過大量數據與案例,說明哪些因素真正影響財富累積、職涯發展與感情穩定,並指出常見迷思與偏見。內容深入淺出,既具說服力又具實用價值,適合追求理性決策的讀者。\n\n整體而言,本書不僅提供觀念上的啟發,也為人生規劃帶來具體方向,是結合數據思維與生活應用的佳作。",
|
||||
"大众文化的女性主义指南 ([韩]孙希定 [韩]林允玉 [韩]金智惠 编) (Z-Library)": "《大众文化的女性主义指南》由韩国三位女性学者合编,以犀利视角剖析大众文化中的性别议题。书中通过影视、广告、网络文化等多元媒介,揭示了女性如何在主流文化中被建构、消费与压迫,同时探讨了女性主义如何在这些文本中寻找抵抗与发声的可能。\n\n不同于艰涩的理论书籍,本书语言通俗易懂,结合大量韩国本土案例,使读者更容易理解女性主义视角下的文化批判。它不仅是一本分析工具书,更是一种觉醒的引导,帮助读者识别日常文化中隐含的性别偏见与权力结构。\n\n此书适合对性别议题感兴趣的大众读者,也适合作为文化研究的入门读物。它不追求学术的完整性,而是强调批判性思维与现实关怀,是一本兼具实用性与启发性的女性主义文化指南。",
|
||||
"秩序的理由-gzrk9": "《秩序的理由》是日本哲学家中岛义道探讨现代社会秩序与个体自由关系的代表作。作者从日常生活出发,剖析了现代社会中“秩序”如何通过制度、规范和人际关系对个体施加影响,并追问人在秩序中的位置与选择可能。\n\n书中指出,秩序并非天然存在,而是人们在共同生活中逐步建构的。中岛通过哲学与心理学的视角,分析了人们为何接受甚至主动维护秩序,以及这种接受背后的理性与非理性动因。他强调,真正的自由并非对秩序的全盘否定,而是在理解秩序本质后做出的自觉选择。\n\n本书语言平实,思想深刻,引导读者反思自身与社会的关系。它不仅是一本哲学读物,更是一面镜子,照见我们在秩序中如何生活、为何生活。对于希望理解现代社会机制与个体生存状态的读者而言,《秩序的理由》是一本值得细读的佳作。",
|
||||
"渔樵问对": "《渔樵问对》以渔者与樵者的对话形式,探讨人生、自然与社会之道,语言简练而意蕴深远。全篇通过问答展现哲理,既有道家的超然,也有儒家的担当,体现古人对天地人伦的深刻思考。\n\n文章结构紧凑,层层递进,借自然景象喻世事人情,如以山水、鱼鸟为引,揭示动静相生、刚柔并济的处世智慧。对话平实却发人深省,展现了中国传统思想中“天人合一”的哲学理念。\n\n此书虽篇幅短小,却内涵丰富,适合反复品读。它不仅是一篇哲理对话录,更是一部引导人思考人生方向与处世态度的经典之作,值得今人借鉴与反思。",
|
||||
"对比11国意外发现中国的运气与软肋": "《对比11国意外发现中国的运气与软肋》一书通过横向比较多国发展路径,揭示了中国崛起过程中所依赖的外部机遇与内在挑战。作者以冷静视角指出,中国经济腾飞离不开全球化红利与地缘环境的配合,这是“运气”所在。\n\n但书中更值得深思的是对中国发展软肋的剖析。过度依赖投资拉动、区域发展失衡、创新能力不足等问题,在国际比较中愈发凸显。这些结构性矛盾若不及时解决,将成为持续发展的障碍。\n\n本书价值在于不沉溺于成就宣传,而是以他国经验为镜,提醒中国在自信中保持清醒。在全球格局剧变的今天,如何将“运气”转化为“定力”,直面“软肋”推动改革,是实现长远发展的关键。",
|
||||
"闪米特人和雅利安人的博弈": "《闪米特人和雅利安人的博弈》一书深入剖析了两大族群在历史、宗教与文化层面的长期互动与冲突。作者从古代近东文明谈起,揭示了闪米特人对犹太教、基督教和伊斯兰教的深远影响,同时阐述了雅利安人在印度-欧洲语系中的主导地位及其对西方文明的塑造。\n\n书中重点分析了19世纪至20世纪初种族主义思潮的兴起,特别是纳粹德国对“雅利安”概念的极端化利用,以及其对犹太族群(作为闪米特代表)的迫害。这种人为制造的对立不仅扭曲了历史事实,也造成了深重的人道灾难,值得后人深刻反思。\n\n全书语言简洁,逻辑清晰,虽篇幅不长,但内容厚重。它提醒读者,历史不应被种族主义工具化,唯有理解多元文明的交融与博弈,才能推动真正的和平与共存。",
|
||||
"自由之困": "《自由之困》是一部深刻探讨自由与责任关系的作品。作者通过细腻的笔触,展现了人在追求自由过程中所面临的道德困境与内心挣扎。\n\n书中通过多个角色的命运交织,揭示了自由并非无约束的放纵,而是伴随着责任与选择的沉重负担。主人公在自由与伦理之间的徘徊,反映出当代社会个体在多元价值观冲击下的迷茫与觉醒。\n\n本书语言凝练,思想深刻,虽篇幅不长,却发人深省。它提醒我们:真正的自由,是认清自我、承担责任后的从容,而非逃避与放任。值得一读,尤其在当下这个充满选择却也充满困惑的时代。",
|
||||
"做饭汪曾祺代表作系列 文艺经典": "汪曾祺的《做饭》是其代表作系列中极具生活气息与文艺气质的散文佳作。他以平实语言描绘日常饮食,将锅碗瓢盆中的烟火气写得诗意盎然,展现出对生活的深挚热爱。\n\n文章结构简洁,却意蕴深远。从选材到烹饪,每一道工序都像在讲述人生的节奏与耐心。他笔下的食物不仅是味觉享受,更是一种文化、一种态度,透露出他对传统文化的敬意与传承。\n\n汪曾祺的文字如清风拂面,朴实中见真味,平凡中显深情。《做饭》不仅是一篇关于饮食的散文,更是一次心灵的滋养,让人在快节奏生活中重拾对日常的美好感知。",
|
||||
"民主的细节:美国当代政治观察随笔 (刘瑜) (Z-Library)": "《民主的细节:美国当代政治观察随笔》是刘瑜以理性、平实的笔触对美国民主制度进行深入观察与剖析的随笔集。书中通过一个个具体的政治事件与社会现象,展现了民主制度如何在日常运作中体现其复杂性与韧性。\n\n刘瑜并未对民主进行空泛的赞美,而是从实际案例出发,揭示制度背后的逻辑与矛盾,如选举制度的设计、媒体的角色、公民社会的作用等。她以学者的严谨与写作者的敏锐,让读者看到民主不仅是理念,更是实践的艺术。\n\n本书语言通俗易懂,逻辑清晰,既有理论深度,又具现实关怀。它不仅帮助读者理解美国民主的运行机制,也启发我们思考制度建设中“细节决定成败”的深刻命题。",
|
||||
"单向度的人": "《单向度的人》是法兰克福学派代表人物马尔库塞对现代工业社会的深刻批判之作。他指出,在技术理性主导下,社会逐渐趋于单一维度,人们的思想被商品化、标准化,失去了批判与反思的能力。\n\n马尔库塞认为,资本主义通过消费主义和大众文化将个体纳入统一的意识形态之中,使人沦为“单向度”的存在,只知适应,不再质疑。这种“虚假的需求”压制了人的自由与创造力,使社会进步停滞。\n\n本书虽写于冷战时期,但对当下仍具启示意义。在信息高度同质化的今天,重读《单向度的人》,有助于我们警惕思维的固化,保持独立思考的能力,追求更自由、多元的生活方式。",
|
||||
"宇宙观与现代世界": "《宇宙观与现代世界》一书从宏观视角探讨了人类对宇宙认知的演变及其对现代社会的深远影响。作者以简明的语言梳理了从古代神话到现代科学的宇宙观发展脉络,揭示了人类如何逐步摆脱迷信,走向理性与实证。\n\n书中特别强调科学革命带来的世界观转变,如哥白尼、牛顿和爱因斯坦的理论如何重塑人类对空间、时间和因果关系的理解。这种转变不仅影响自然科学,也深刻改变了哲学、政治与社会结构。\n\n本书思想深刻、逻辑清晰,是一本启发思考的佳作。它提醒我们,理解宇宙的过程,也是理解人类自身位置与未来方向的过程,对现代人具有重要启示意义。",
|
||||
"像頂尖運動員一樣思考": "《像頂尖運動員一樣思考》書評 \n\n《像頂尖運動員一樣思考》一書深入探討了成功背後的關鍵心態。作者透過訪問多位世界級運動員,歸納出他們面對壓力、失敗與競爭時的心理策略,揭示了「心智訓練」與「身體鍛鍊」同樣重要。這不僅適用於運動場,更適用於人生各個領域。 \n\n書中強調目標設定、專注力與自我對話的重要性。頂尖運動員之所以能突破極限,是因為他們懂得如何調整心態、保持紀律,並在逆境中找到動力。這些觀念對任何追求卓越的人來說,都是寶貴的借鏡。 \n\n這本簡潔有力的書,不僅激勵人心,也提供實用的方法。無論是學生、職場人士或運動愛好者,都能從中學習如何培養堅強的意志與積極的心態,進而發揮潛能,邁向成功。",
|
||||
"社会心理学 彩印 第11版 (迈尔斯著) (Z-Library)": "《社会心理学》第11版(迈尔斯著)是一部系统、权威的心理学经典教材。全书内容全面,涵盖了社会认知、态度形成、群体行为、人际吸引、从众与服从等核心议题,结构清晰,逻辑严密,适合心理学及相关专业的学生学习使用。\n\n本书的一大亮点在于其图文并茂、彩印设计,增强了阅读体验与知识理解。通过丰富的图表、案例与实验研究,作者将复杂的心理学理论深入浅出地呈现出来,使读者易于掌握并激发思考。书中大量引用经典与当代研究,兼具学术性与可读性。\n\n总体而言,这是一本兼具理论深度与实践价值的优秀教材。无论是心理学入门者还是教学研究者,都能从中获得系统的知识体系与前沿的研究视角,是学习社会心理学不可或缺的参考书。",
|
||||
"从理想主义到经验主义": "《从理想主义到经验主义》是顾准对中国社会发展道路的深刻反思。书中通过对历史与现实的对比,展现了理想主义在实践中的困境与经验主义的必要性。\n\n顾准以冷静理性的笔触,剖析了乌托邦式改革的失败原因,强调制度建设应基于现实国情与历史经验,而非空想的理论模型。他主张渐进、务实的改革路径,体现出对社会复杂性的深刻认知。\n\n这部作品不仅是思想的转变记录,更是一种理性精神的传承。它提醒人们,在变革中保持清醒头脑,尊重现实逻辑,是走向现代文明的必由之路。"
|
||||
}
|
|
@ -202,6 +202,62 @@ class BookListManager:
|
|||
total[i] += readtime12m[i]
|
||||
return total
|
||||
|
||||
# ---------------- 已读完书籍 (本年度) ----------------
|
||||
def get_finished_books_this_year(self):
|
||||
"""返回本年度读完的书籍列表 [(asset_id, info, finished_date_ts), ...]
|
||||
|
||||
依据 ZISFINISHED=1 且 ZDATEFINISHED 在今年内。
|
||||
如果 plist 信息缺少显示名则回退 asset_id。
|
||||
"""
|
||||
booksinfo = self.get_books_info()
|
||||
import datetime, sqlite3
|
||||
year = datetime.datetime.now().year
|
||||
results = []
|
||||
if not os.path.exists(self.db_path):
|
||||
return results
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cur = conn.cursor()
|
||||
# ZDATEFINISHED: Apple CoreData 时间戳(相对 2001-01-01 秒)
|
||||
cur.execute("""
|
||||
SELECT ZASSETID, ZDATEFINISHED, ZISFINISHED
|
||||
FROM ZBKLIBRARYASSET
|
||||
WHERE ZISFINISHED=1 AND ZDATEFINISHED IS NOT NULL
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
# 调试:原始满足完成条件的行数
|
||||
try:
|
||||
if getattr(self, '_debug_finished_books', True):
|
||||
print(f"[debug finished] raw_rows={len(rows)} (ZISFINISHED=1 & ZDATEFINISHED not null)")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f'警告: 查询已读完书籍失败: {e}')
|
||||
return results
|
||||
base = datetime.datetime(2001,1,1)
|
||||
for asset_id, finished_ts, flag in rows:
|
||||
try:
|
||||
if not asset_id or finished_ts is None:
|
||||
continue
|
||||
finished_dt = base + datetime.timedelta(seconds=finished_ts)
|
||||
if finished_dt.year != year:
|
||||
continue
|
||||
info = booksinfo.get(asset_id, {})
|
||||
results.append((asset_id, info, finished_dt))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if getattr(self, '_debug_finished_books', True):
|
||||
print(f"[debug finished] after year filter={len(results)}, year={year}")
|
||||
if results:
|
||||
print("[debug finished] sample asset_ids:", ','.join(r[0] for r in results[:5]))
|
||||
except Exception:
|
||||
pass
|
||||
# 按完成时间倒序
|
||||
results.sort(key=lambda x: x[2], reverse=True)
|
||||
return results
|
||||
|
||||
if __name__ == '__main__':
|
||||
manager = BookListManager()
|
||||
booksinfo = manager.get_books_info()
|
||||
|
|
|
@ -0,0 +1,282 @@
|
|||
from PyQt6.QtWidgets import QWidget, QLabel, QSizePolicy
|
||||
from PyQt6.QtGui import QPainter, QColor, QPen, QPainterPath, QLinearGradient
|
||||
from PyQt6.QtCore import Qt, QRectF, QPointF
|
||||
import math
|
||||
|
||||
# 统一底色与圆角
|
||||
BACKGROUND_COLOR = QColor(245, 246, 250)
|
||||
PANEL_BORDER_COLOR = QColor(225, 228, 236)
|
||||
CORNER_RADIUS = 10
|
||||
|
||||
def map_ratio_to_color(r: float) -> QColor:
|
||||
hue_stops = [(0.0,0.58),(0.25,0.64),(0.50,0.70),(0.75,0.78),(1.0,0.85)]
|
||||
def lerp(a,b,t): return a + (b-a)*t
|
||||
if r <= 0: h = hue_stops[0][1]
|
||||
elif r >= 1: h = hue_stops[-1][1]
|
||||
else:
|
||||
for i in range(1,len(hue_stops)):
|
||||
if r <= hue_stops[i][0]:
|
||||
lpos, lh = hue_stops[i-1]; rpos, rh = hue_stops[i]
|
||||
local_t = 0 if rpos==lpos else (r-lpos)/(rpos-lpos)
|
||||
h = lerp(lh,rh,local_t); break
|
||||
sat = 0.35 + 0.20 * r
|
||||
valb = 0.75 + 0.15 * (1 - abs(r-0.5)*2)
|
||||
return QColor.fromHsvF(h, sat, valb)
|
||||
|
||||
class BarChartWidget(QWidget):
|
||||
def __init__(self, data, title='', unit='分钟', labels=None, value_format=None, label_rotation: int = 0, parent=None):
|
||||
super().__init__(parent)
|
||||
self.data = data or []
|
||||
self.title = title
|
||||
self.unit = unit
|
||||
self.labels = labels or [str(i) for i in range(len(self.data))]
|
||||
self.value_format = value_format
|
||||
self.label_rotation = label_rotation # 0=不旋转, 45=倾斜
|
||||
self.setMinimumHeight(180)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
def sizeHint(self):
|
||||
from PyQt6.QtCore import QSize
|
||||
return QSize(500, 260)
|
||||
|
||||
def setData(self, data, labels=None):
|
||||
self.data = data or []
|
||||
self.labels = labels or [str(i) for i in range(len(self.data))]
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
rect = self.rect()
|
||||
p.setPen(QPen(PANEL_BORDER_COLOR,1))
|
||||
p.setBrush(BACKGROUND_COLOR)
|
||||
# 背景圆角
|
||||
p.drawRoundedRect(QRectF(rect).adjusted(0.5,0.5,-0.5,-0.5), CORNER_RADIUS, CORNER_RADIUS)
|
||||
if not self.data:
|
||||
p.setPen(QColor('#666666'))
|
||||
p.drawText(rect, Qt.AlignmentFlag.AlignCenter, '暂无数据')
|
||||
p.end(); return
|
||||
max_val = max(self.data) or 1
|
||||
title_left, title_top = 12, 6
|
||||
margin_top = 28
|
||||
margin_bottom = 28 if self.label_rotation == 0 else 50
|
||||
margin_left, margin_right = 40, 10
|
||||
title_height = 18
|
||||
w, h = rect.width(), rect.height()
|
||||
chart_rect = QRectF(margin_left, margin_top, w - margin_left - margin_right, h - margin_top - margin_bottom)
|
||||
bar_count = len(self.data)
|
||||
bar_space = chart_rect.width() / bar_count
|
||||
bar_width = bar_space * 0.55
|
||||
# 标题
|
||||
p.setPen(Qt.GlobalColor.black)
|
||||
font = p.font(); font.setPointSize(10); font.setBold(True); p.setFont(font)
|
||||
p.drawText(QRectF(title_left, title_top, chart_rect.width(), title_height), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self.title)
|
||||
# 网格
|
||||
font.setPointSize(8); font.setBold(False); p.setFont(font)
|
||||
pen_grid = QPen(QColor('#dddddd')); pen_grid.setStyle(Qt.PenStyle.DotLine)
|
||||
p.setPen(pen_grid)
|
||||
grid_lines = 4
|
||||
for i in range(grid_lines+1):
|
||||
y = chart_rect.top() + chart_rect.height() * (1 - i / grid_lines)
|
||||
p.drawLine(QPointF(margin_left, y), QPointF(margin_left + chart_rect.width(), y))
|
||||
val = max_val * i / grid_lines
|
||||
p.setPen(QColor('#555555'))
|
||||
label = f'{val:.0f}' if max_val >= 10 else f'{val:.1f}'
|
||||
p.drawText(QRectF(2, y-8, margin_left-4, 14), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, label)
|
||||
p.setPen(pen_grid)
|
||||
# 单位
|
||||
p.setPen(QColor('#333333'))
|
||||
p.drawText(2, margin_top-4, margin_left-4, 14, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignBottom, self.unit)
|
||||
# 柱子
|
||||
for idx, v in enumerate(self.data):
|
||||
x_center = chart_rect.left() + idx * bar_space + bar_space/2
|
||||
bar_h = 0 if max_val == 0 else chart_rect.height() * (v / max_val)
|
||||
bar_rect = QRectF(x_center - bar_width/2, chart_rect.bottom()-bar_h, bar_width, bar_h)
|
||||
ratio = 0.0 if max_val == 0 else max(0.0, min(1.0, v / max_val))
|
||||
color = map_ratio_to_color(ratio)
|
||||
p.setPen(Qt.PenStyle.NoPen); p.setBrush(color)
|
||||
p.drawRoundedRect(bar_rect, 2, 2)
|
||||
if v > 0:
|
||||
p.setPen(QColor('#222222'))
|
||||
sf = p.font(); sf.setPointSize(7); p.setFont(sf)
|
||||
val_str = self.value_format(v) if self.value_format else (f'{v:.0f}' if v >= 10 else f'{v:.1f}')
|
||||
p.drawText(QRectF(bar_rect.left()-2, bar_rect.top()-14, bar_rect.width()+4, 12), Qt.AlignmentFlag.AlignCenter, val_str)
|
||||
# x 标签
|
||||
p.setPen(QColor('#333333'))
|
||||
lf = p.font(); lf.setPointSize(8); p.setFont(lf)
|
||||
for idx, text in enumerate(self.labels):
|
||||
x_center = chart_rect.left() + idx * bar_space + bar_space/2
|
||||
if self.label_rotation:
|
||||
p.save()
|
||||
p.translate(x_center, chart_rect.bottom()+4)
|
||||
p.rotate(-45)
|
||||
# 旋转后使用固定宽度避免重叠 (40px)
|
||||
p.drawText(QRectF(-20, 0, 40, 14), Qt.AlignmentFlag.AlignCenter, text)
|
||||
p.restore()
|
||||
else:
|
||||
p.drawText(QRectF(x_center - bar_space/2, chart_rect.bottom()+2, bar_space, 14), Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, text)
|
||||
p.end()
|
||||
|
||||
class BubbleMetricsWidget(QWidget):
|
||||
def __init__(self, metrics, parent=None):
|
||||
super().__init__(parent)
|
||||
self.metrics = metrics
|
||||
self.setMinimumHeight(240)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
def sizeHint(self):
|
||||
from PyQt6.QtCore import QSize
|
||||
return QSize(500, 300)
|
||||
|
||||
def setMetrics(self, metrics):
|
||||
self.metrics = metrics; self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
rect = self.rect()
|
||||
p.setPen(QPen(PANEL_BORDER_COLOR,1)); p.setBrush(BACKGROUND_COLOR)
|
||||
p.drawRoundedRect(QRectF(rect).adjusted(0.5,0.5,-0.5,-0.5), CORNER_RADIUS, CORNER_RADIUS)
|
||||
title_left, title_top, title_height = 12, 6, 20
|
||||
margin = 2
|
||||
content_rect = QRectF(rect.left()+margin, rect.top()+title_height+margin, rect.width()-2*margin, rect.height()-title_height-2*margin)
|
||||
if not self.metrics:
|
||||
p.drawText(rect, Qt.AlignmentFlag.AlignCenter, '暂无数据'); p.end(); return
|
||||
# 归一化
|
||||
minute_values = [(v*60 if u=='h' else v) for (lbl,v,u,c) in self.metrics]
|
||||
max_minutes = max(minute_values) if minute_values else 1
|
||||
radii_norm = [0.3 + 0.7 * math.sqrt((mv/max_minutes) if max_minutes>0 else 0) for mv in minute_values]
|
||||
default_pos = { '全年':(0.20,0.50), '月均':(0.50,0.18), '近7天':(0.80,0.50), '日均':(0.50,0.82) }
|
||||
if any(lbl not in default_pos for (lbl,*_) in self.metrics):
|
||||
step = 1.0/(len(self.metrics)+1)
|
||||
default_pos = {m[0]:(step*(i+1),0.5) for i,m in enumerate(self.metrics)}
|
||||
W,H = content_rect.width(), content_rect.height(); base_len = min(W,H)
|
||||
centers = [(content_rect.left()+default_pos.get(lbl,(0.5,0.5))[0]*W,
|
||||
content_rect.top()+default_pos.get(lbl,(0.5,0.5))[1]*H) for (lbl,*_) in self.metrics]
|
||||
constraints=[]
|
||||
for (cx,cy),r in zip(centers,radii_norm):
|
||||
dist=min(cx-content_rect.left(), content_rect.right()-cx, cy-content_rect.top(), content_rect.bottom()-cy)
|
||||
if r>0: constraints.append(dist/(r*base_len))
|
||||
for i in range(len(centers)):
|
||||
for j in range(i+1,len(centers)):
|
||||
dx = centers[i][0]-centers[j][0]; dy = centers[i][1]-centers[j][1]
|
||||
d = math.hypot(dx,dy)
|
||||
if d>0:
|
||||
gap=6; constraints.append((d-gap)/((radii_norm[i]+radii_norm[j])*base_len))
|
||||
S = max(0.05, min(constraints) if constraints else 0.5)
|
||||
for (label,val,unit,color), r_norm, (cx,cy) in zip(self.metrics, radii_norm, centers):
|
||||
R = r_norm * S * base_len
|
||||
circle_rect = QRectF(cx-R, cy-R, 2*R, 2*R)
|
||||
p.setPen(QPen(QColor('#ffffff'),2)); p.setBrush(QColor(color)); p.drawEllipse(circle_rect)
|
||||
p.setPen(QColor('#ffffff'))
|
||||
f = p.font(); f.setBold(True); f.setPointSize(max(9,int(R/5))); p.setFont(f)
|
||||
txt = f"{val:.0f} 小时" if unit=='h' and val>=10 else (f"{val:.1f} 小时" if unit=='h' else f"{val:.0f} 分钟")
|
||||
p.drawText(circle_rect, Qt.AlignmentFlag.AlignCenter, f"{txt}\n{label}")
|
||||
# 标题(为空保留对齐占位)
|
||||
p.setPen(QColor('#222222'))
|
||||
f = p.font(); f.setBold(True); f.setPointSize(10); p.setFont(f)
|
||||
p.drawText(QRectF(title_left, title_top, rect.width()-title_left-8, title_height), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, '')
|
||||
p.end()
|
||||
|
||||
class ScatterChartWidget(QWidget):
|
||||
def __init__(self, data, title='', unit='小时', labels=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.data = data or []
|
||||
self.labels = labels or [str(i) for i in range(len(self.data))]
|
||||
self.title = title; self.unit = unit
|
||||
self.setMinimumHeight(180)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
def sizeHint(self):
|
||||
from PyQt6.QtCore import QSize
|
||||
return QSize(500, 260)
|
||||
|
||||
def setData(self, data, labels=None):
|
||||
self.data = data or []
|
||||
self.labels = labels or [str(i) for i in range(len(self.data))]
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
rect = self.rect()
|
||||
p.setPen(QPen(PANEL_BORDER_COLOR,1)); p.setBrush(BACKGROUND_COLOR)
|
||||
p.drawRoundedRect(QRectF(rect).adjusted(0.5,0.5,-0.5,-0.5), CORNER_RADIUS, CORNER_RADIUS)
|
||||
if not self.data:
|
||||
p.drawText(rect, Qt.AlignmentFlag.AlignCenter, '暂无数据'); p.end(); return
|
||||
title_left,title_top = 12,6
|
||||
margin_top,margin_bottom = 28,32
|
||||
margin_left,margin_right = 40,10
|
||||
title_height = 18
|
||||
w,h = rect.width(), rect.height()
|
||||
chart_rect = QRectF(margin_left, margin_top, w - margin_left - margin_right, h - margin_top - margin_bottom)
|
||||
# 标题
|
||||
p.setPen(Qt.GlobalColor.black)
|
||||
font = p.font(); font.setPointSize(10); font.setBold(True); p.setFont(font)
|
||||
p.drawText(QRectF(title_left, title_top, chart_rect.width(), title_height), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self.title)
|
||||
# 网格
|
||||
max_val = max(self.data) if self.data else 1
|
||||
if max_val <= 0: max_val = 1
|
||||
grid_lines=4
|
||||
font.setPointSize(8); font.setBold(False); p.setFont(font)
|
||||
pen_grid = QPen(QColor('#dddddd')); pen_grid.setStyle(Qt.PenStyle.DotLine)
|
||||
for i in range(grid_lines+1):
|
||||
y = chart_rect.top() + chart_rect.height()*(1 - i/grid_lines)
|
||||
p.setPen(pen_grid); p.drawLine(QPointF(chart_rect.left(), y), QPointF(chart_rect.right(), y))
|
||||
val = max_val * i / grid_lines
|
||||
p.setPen(QColor('#555555'))
|
||||
label = f'{val:.0f}' if max_val>=10 else f'{val:.1f}'
|
||||
p.drawText(QRectF(2, y-8, margin_left-4, 14), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, label)
|
||||
# 单位
|
||||
p.setPen(QColor('#333333'))
|
||||
p.drawText(2, margin_top-4, margin_left-4, 14, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignBottom, self.unit)
|
||||
# 点
|
||||
count = len(self.data)
|
||||
x_step = chart_rect.width()/(count-1) if count>1 else chart_rect.width()/2
|
||||
points=[]
|
||||
for i,v in enumerate(self.data):
|
||||
x = chart_rect.left()+i*x_step
|
||||
y = chart_rect.bottom() - (v/max_val)*chart_rect.height()
|
||||
ratio = max(0.0,min(1.0,v/max_val))
|
||||
color = map_ratio_to_color(ratio)
|
||||
points.append((x,y,v,color,ratio))
|
||||
# 均值线
|
||||
avg_val = sum(self.data)/len(self.data) if self.data else 0
|
||||
avg_y = chart_rect.bottom() - (avg_val/max_val)*chart_rect.height()
|
||||
pen_avg = QPen(QColor('#aaaadd')); pen_avg.setStyle(Qt.PenStyle.DashLine); pen_avg.setWidth(1)
|
||||
p.setPen(pen_avg); p.drawLine(QPointF(chart_rect.left(), avg_y), QPointF(chart_rect.right(), avg_y))
|
||||
p.setPen(QColor('#666688'))
|
||||
f2 = p.font(); f2.setPointSize(7); p.setFont(f2)
|
||||
p.drawText(QRectF(chart_rect.right()-60, avg_y-10, 58, 12), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, f'均值 {avg_val:.1f}')
|
||||
# 折线+面积
|
||||
if len(points) >=2:
|
||||
pen_line = QPen(QColor('#8890c0')); pen_line.setWidth(1); p.setPen(pen_line)
|
||||
for i in range(len(points)-1):
|
||||
p.drawLine(QPointF(points[i][0], points[i][1]), QPointF(points[i+1][0], points[i+1][1]))
|
||||
grad = QLinearGradient(chart_rect.left(), chart_rect.top(), chart_rect.left(), chart_rect.bottom())
|
||||
grad.setColorAt(0.0, QColor(200,205,235,90)); grad.setColorAt(1.0, QColor(200,205,235,10))
|
||||
path = QPainterPath(QPointF(points[0][0], chart_rect.bottom()))
|
||||
for (x,y, *_ ) in points: path.lineTo(x,y)
|
||||
path.lineTo(points[-1][0], chart_rect.bottom()); path.closeSubpath(); p.fillPath(path, grad)
|
||||
# 极值
|
||||
max_point = max(points, key=lambda t:t[2]) if points else None
|
||||
min_point = min(points, key=lambda t:t[2]) if points else None
|
||||
for (x,y,v,color,ratio) in points:
|
||||
r = 5 + 7*ratio
|
||||
pen = QPen(QColor('#ffffff'),1.2)
|
||||
if max_point and (x,y,v)==max_point[:3]: pen = QPen(QColor('#ff8888'),1.5)
|
||||
elif min_point and (x,y,v)==min_point[:3]: pen = QPen(QColor('#66aac6'),1.5)
|
||||
p.setPen(pen); p.setBrush(color); p.drawEllipse(QPointF(x,y), r, r)
|
||||
label_y_top = y - r - 10; place_above = label_y_top > chart_rect.top()+4
|
||||
txt_rect = QRectF(x-24, (label_y_top if place_above else y + r + 2), 48, 12)
|
||||
p.setPen(QColor('#222222'))
|
||||
sf = p.font(); sf.setPointSize(7); p.setFont(sf)
|
||||
val_str = f'{v:.1f}' if v < 10 else f'{v:.0f}'
|
||||
p.drawText(txt_rect, Qt.AlignmentFlag.AlignCenter, val_str)
|
||||
# x 标签
|
||||
p.setPen(QColor('#333333'))
|
||||
lf = p.font(); lf.setPointSize(8); p.setFont(lf)
|
||||
for i, lab in enumerate(self.labels):
|
||||
x = chart_rect.left() + (i * x_step if count>1 else chart_rect.width()/2)
|
||||
p.drawText(QRectF(x-20, chart_rect.bottom()+2, 40, 14), Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, lab)
|
||||
p.end()
|
|
@ -21,6 +21,8 @@ IBOOKS_ANNOTATION_DB = os.path.expanduser('~/Library/Containers/com.apple.iBooks
|
|||
IBOOKS_ANNOTATION_SHM = IBOOKS_ANNOTATION_DB + '-shm'
|
||||
IBOOKS_ANNOTATION_WAL = IBOOKS_ANNOTATION_DB + '-wal'
|
||||
IBOOKS_LIBRARY_DB = os.path.expanduser('~/Library/Containers/com.apple.iBooksX/Data/Documents/BKLibrary/BKLibrary-1-091020131601.sqlite')
|
||||
IBOOKS_LIBRARY_SHM = IBOOKS_LIBRARY_DB + '-shm'
|
||||
IBOOKS_LIBRARY_WAL = IBOOKS_LIBRARY_DB + '-wal'
|
||||
IBOOKS_BOOKS_PLIST = os.path.expanduser('~/Library/Containers/com.apple.BKAgentService/Data/Documents/iBooks/Books/Books.plist')
|
||||
IBOOKS_BOOKS_DIR = os.path.expanduser('~/Library/Mobile Documents/iCloud~com~apple~iBooks/Documents')
|
||||
|
||||
|
@ -29,8 +31,14 @@ LOCAL_ANNOTATION_DB = os.path.join(DATA_DIR, 'AEAnnotation.sqlite')
|
|||
LOCAL_ANNOTATION_SHM = os.path.join(DATA_DIR, 'AEAnnotation.sqlite-shm')
|
||||
LOCAL_ANNOTATION_WAL = os.path.join(DATA_DIR, 'AEAnnotation.sqlite-wal')
|
||||
LOCAL_LIBRARY_DB = os.path.join(DATA_DIR, 'BKLibrary.sqlite')
|
||||
LOCAL_LIBRARY_SHM = os.path.join(DATA_DIR, 'AEAnnotation.sqlite-shm')
|
||||
LOCAL_LIBRARY_WAL = os.path.join(DATA_DIR, 'AEAnnotation.sqlite-wal')
|
||||
LOCAL_BOOKS_PLIST = os.path.join(DATA_DIR, 'Books.plist')
|
||||
|
||||
# 统计用:每日最小阅读时长(单位:秒)
|
||||
READ_TIME_DAY = 60
|
||||
|
||||
# 阿里 DashScope API Key(优先读取环境变量,若未设置则使用下面的默认值)
|
||||
# 注意:为安全起见,生产或开源仓库不要直接硬编码真实密钥,建议只保留 os.environ 读取逻辑。
|
||||
DASHSCOPE_API_KEY = os.environ.get('DASHSCOPE_API_KEY', 'sk-2546da09b6d9471894aeb95278f96c11')
|
||||
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
import os
|
||||
import re
|
||||
from urllib.parse import unquote
|
||||
import config
|
||||
|
||||
class CoverMixin:
|
||||
"""封面处理相关方法抽取。依赖 self.book_toc_textedit / self._cover_labels / self._cover_pixmaps_original / self.cover_ratio / self.cover_elastic."""
|
||||
|
||||
def find_book_cover(self, assetid, book_info):
|
||||
book_path = book_info.get('path', '')
|
||||
if not book_path:
|
||||
return None
|
||||
book_dir = os.path.join(config.IBOOKS_BOOKS_DIR, book_path)
|
||||
if os.path.isfile(book_dir):
|
||||
book_dir = os.path.dirname(book_dir)
|
||||
if not os.path.isdir(book_dir):
|
||||
return None
|
||||
for fname in os.listdir(book_dir):
|
||||
if re.fullmatch(r'cover\.(?i:jpg|jpeg|png)', fname):
|
||||
p = os.path.join(book_dir, fname)
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
cover_htmls = []
|
||||
for root, dirs, files in os.walk(book_dir):
|
||||
for fname in files:
|
||||
if re.match(r'cover.*\.(?i:x?html?)', fname):
|
||||
cover_htmls.append(os.path.join(root, fname))
|
||||
for html_path in cover_htmls:
|
||||
filename = self.extract_cover_filename_from_html(html_path)
|
||||
if not filename:
|
||||
continue
|
||||
found = self.search_filename_in_tree(book_dir, filename)
|
||||
if found:
|
||||
return found
|
||||
opf_paths = []
|
||||
for root, dirs, files in os.walk(book_dir):
|
||||
for fname in files:
|
||||
if fname.lower().endswith('.opf'):
|
||||
opf_paths.append(os.path.join(root, fname))
|
||||
for opf in opf_paths:
|
||||
candidate = self.extract_cover_from_opf(opf, book_dir)
|
||||
if candidate and os.path.exists(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
def search_filename_in_tree(self, root_dir, target_name):
|
||||
candidates = {target_name}
|
||||
decoded = unquote(target_name)
|
||||
candidates.add(decoded)
|
||||
lower_set = {c.lower() for c in candidates}
|
||||
for root, dirs, files in os.walk(root_dir):
|
||||
for fname in files:
|
||||
if fname in candidates:
|
||||
return os.path.join(root, fname)
|
||||
for root, dirs, files in os.walk(root_dir):
|
||||
for fname in files:
|
||||
if fname.lower() in lower_set:
|
||||
return os.path.join(root, fname)
|
||||
return None
|
||||
|
||||
def extract_cover_filename_from_html(self, html_path):
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
m = re.search(r'xlink:href="([^"]+\.(?:jpg|jpeg|png))"', content, re.IGNORECASE)
|
||||
if not m:
|
||||
m = re.search(r'="([^"]+\.(?:jpg|jpeg|png))"', content, re.IGNORECASE)
|
||||
if not m:
|
||||
return None
|
||||
rel = m.group(1).strip()
|
||||
filename = os.path.basename(unquote(rel))
|
||||
return filename
|
||||
except Exception as e:
|
||||
print(f"解析 {html_path} 提取封面文件名失败: {e}")
|
||||
return None
|
||||
|
||||
def extract_cover_from_opf(self, opf_path, base_root):
|
||||
import re as _re
|
||||
try:
|
||||
with open(opf_path, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
m_meta = _re.search(r'<meta[^>]*name=["\']cover["\'][^>]*content=["\']([^"\']+)["\']', text, _re.IGNORECASE)
|
||||
if not m_meta:
|
||||
return None
|
||||
cover_id = m_meta.group(1).strip()
|
||||
item_pattern = _re.compile(r'<item\b[^>]*id=["\']%s["\'][^>]*>' % _re.escape(cover_id), _re.IGNORECASE)
|
||||
m_item = item_pattern.search(text)
|
||||
if not m_item:
|
||||
return None
|
||||
tag = m_item.group(0)
|
||||
m_href = _re.search(r'href=["\']([^"\']+)["\']', tag, _re.IGNORECASE)
|
||||
if not m_href:
|
||||
return None
|
||||
href = unquote(m_href.group(1).strip())
|
||||
img_path = os.path.normpath(os.path.join(os.path.dirname(opf_path), href))
|
||||
if os.path.exists(img_path):
|
||||
return img_path
|
||||
fallback = self.search_filename_in_tree(base_root, os.path.basename(href))
|
||||
return fallback
|
||||
except Exception as e:
|
||||
print(f"解析 OPF 封面失败 {opf_path}: {e}")
|
||||
return None
|
||||
|
||||
# 封面缩放与控制
|
||||
def _apply_cover_scale(self):
|
||||
if not hasattr(self, '_cover_pixmaps_original'):
|
||||
return
|
||||
from PyQt6.QtCore import Qt
|
||||
fixed_w = 180
|
||||
ratio_h = int(fixed_w * getattr(self, 'cover_ratio', 1.2))
|
||||
hard_cap = 400
|
||||
if not getattr(self, 'cover_elastic', False):
|
||||
target_h = min(ratio_h, hard_cap)
|
||||
else:
|
||||
container_cap = int(self.book_toc_textedit.height() * 0.45) if hasattr(self, 'book_toc_textedit') else ratio_h
|
||||
if getattr(self, '_force_ratio_height', False):
|
||||
target_h = min(ratio_h, hard_cap)
|
||||
else:
|
||||
target_h = min(ratio_h, container_cap, hard_cap)
|
||||
if target_h < ratio_h and getattr(self, 'debug_cover_ratio', False):
|
||||
print(f"[cover] ratio_h={ratio_h} 被限制为 {target_h}")
|
||||
for pm, lab in zip(self._cover_pixmaps_original, self._cover_labels):
|
||||
lab.setMinimumSize(fixed_w, target_h)
|
||||
lab.setMaximumWidth(fixed_w)
|
||||
lab.setScaledContents(True)
|
||||
if pm:
|
||||
scaled = pm.scaled(fixed_w, target_h, Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
||||
lab.setPixmap(scaled)
|
||||
else:
|
||||
lab.setText('无封面')
|
||||
|
||||
def set_cover_ratio(self, ratio: float, force: bool = False):
|
||||
try:
|
||||
r = float(ratio)
|
||||
if r <= 0:
|
||||
return
|
||||
self.cover_ratio = r
|
||||
self._force_ratio_height = bool(force)
|
||||
self._apply_cover_scale()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def set_cover_elastic(self, elastic: bool):
|
||||
try:
|
||||
self.cover_elastic = bool(elastic)
|
||||
self._apply_cover_scale()
|
||||
except Exception:
|
||||
pass
|
Binary file not shown.
BIN
data/Books.plist
BIN
data/Books.plist
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -19,6 +19,53 @@
|
|||
- 所有统计均以“分钟”为单位,便于可视化和分析。
|
||||
- 年度统计遍历今年每一天,保证月度和年度数据完整。
|
||||
- 统计逻辑与实际阅读行为高度贴合,支持无笔记但有打开书籍的场景。
|
||||
|
||||
## 可视化设计(统计标签页)
|
||||
|
||||
布局:统计页使用 2x2 宫格:
|
||||
- 左上(frame_bubble):综合指标气泡图。
|
||||
- 右上(frame_year):全年 12 个月阅读时长柱状图。
|
||||
- 左下 (frame_week):最近 7 天阅读时长柱状图(索引0=今天)。
|
||||
- 右下 (frame_month):最近 30 天阅读时长柱状图(索引0=今天)。
|
||||
|
||||
数据来源:
|
||||
- 周图:`get_total_readtime(days=7)` 结果列表(单位:分钟)。
|
||||
- 月图:`get_total_readtime(days=30)` 结果列表(单位:分钟)。
|
||||
- 年图:`get_total_readtime12m()` 返回长度 12 列表(分钟)。
|
||||
- 综合:
|
||||
* 全年阅读小时数 = `get_total_readtime_year() / 60`(向下取整或保留1位小数)。
|
||||
* 月均阅读小时数 = `(sum(month_list) / 12) / 60`。
|
||||
* 近7天阅读小时数 = `sum(week_list) / 60`。
|
||||
* 日均阅读分钟数 = `sum(month_list[:30 或 recent30]) / 30`(使用最近30天合计除以30)。
|
||||
|
||||
气泡图:
|
||||
- 使用 4 个气泡分别表示上述四项指标。
|
||||
- 半径 r ~ sqrt(value_normalized) 以减弱大值差异;对“小时数”统一换算为分钟后再归一。
|
||||
- 颜色建议:全年(蓝)、月均(橙)、近7天(绿)、日均(紫)。
|
||||
- 文本格式:`标签\n数值+单位`,例如:`全年\n120h`,`日均\n45min`。
|
||||
|
||||
渲染技术(已更新):
|
||||
- 使用原生 Qt 自绘组件(QWidget + QPainter)实现柱状图与气泡图,文件 `charts.py`。
|
||||
- 优势:减少第三方依赖(移除 matplotlib),启动更快、打包体积更小;自绘可精细掌控布局与样式。
|
||||
- 结构:
|
||||
* `BarChartWidget`:通用柱状图组件,支持数值标签、自适应缩放、单位显示。
|
||||
* `BubbleMetricsWidget`:四指标气泡图,按归一化后的平方根缩放半径,支持动态指标扩展。
|
||||
- 刷新策略:当前初始化时构建;若后续增加刷新按钮,可对组件调用 setData/setMetrics 后 update()。
|
||||
|
||||
更新策略:
|
||||
1. 启动时已调用 `sync_source_files`,再构建 `BookListManager`。
|
||||
2. 通过管理器获取三类聚合数据。
|
||||
3. 生成 numpy 数组(可选)并绘制。
|
||||
4. 若无数据(全 0),显示占位提示“暂无阅读数据”。
|
||||
|
||||
异常处理:
|
||||
- 捕获绘图异常(ImportError/RuntimeError),在 frame 中放置 QLabel 显示错误信息而不是抛出。
|
||||
|
||||
后续扩展:
|
||||
- 柱状图支持堆叠 / 渐变填充、鼠标 hover tooltip。
|
||||
- 气泡图支持动画过渡或改为雷达/仪表盘形式。
|
||||
- 增加刷新按钮与 Esc 退出全屏逻辑。
|
||||
|
||||
# iBooks 笔记导出工具 详细设计文档
|
||||
|
||||
## 1. 概述
|
||||
|
|
|
@ -139,6 +139,8 @@ def sync_source_files(config_module):
|
|||
(config_module.IBOOKS_ANNOTATION_SHM, config_module.LOCAL_ANNOTATION_SHM),
|
||||
(config_module.IBOOKS_ANNOTATION_WAL, config_module.LOCAL_ANNOTATION_WAL),
|
||||
(config_module.IBOOKS_LIBRARY_DB, config_module.LOCAL_LIBRARY_DB),
|
||||
(config_module.IBOOKS_LIBRARY_DB + '-shm', config_module.LOCAL_LIBRARY_DB + '-shm'),
|
||||
(config_module.IBOOKS_LIBRARY_DB + '-wal', config_module.LOCAL_LIBRARY_DB + '-wal'),
|
||||
(config_module.IBOOKS_BOOKS_PLIST, config_module.LOCAL_BOOKS_PLIST)
|
||||
]
|
||||
for src, dst in src_files:
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import os
|
||||
from PyQt6.QtWidgets import QLabel
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
class FinishedBooksMixin:
|
||||
"""已读书籍网格相关逻辑。依赖: self.manager, self.find_book_cover, self.finishedGridLayout, self._on_finished_cover_clicked."""
|
||||
|
||||
def _populate_finished_books_grid(self):
|
||||
if not hasattr(self, 'finishedGridLayout'):
|
||||
return
|
||||
finished = self.manager.get_finished_books_this_year()
|
||||
# 清空旧
|
||||
while self.finishedGridLayout.count():
|
||||
item = self.finishedGridLayout.takeAt(0)
|
||||
w = item.widget()
|
||||
if w:
|
||||
w.setParent(None)
|
||||
if not finished:
|
||||
self.finishedGridLayout.addWidget(QLabel('今年暂无已读完书籍'))
|
||||
self._finished_widgets = []
|
||||
return
|
||||
self._finished_widgets = []
|
||||
thumb_w = 100
|
||||
thumb_h = int(thumb_w * 1.3)
|
||||
for asset_id, info, fin_dt in finished:
|
||||
cover_path = self.find_book_cover(asset_id, info) if info else None
|
||||
cover_lab = QLabel()
|
||||
cover_lab.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
try:
|
||||
cover_lab.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
cover_lab.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
except Exception:
|
||||
pass
|
||||
# 默认封面
|
||||
if (not cover_path) or (not os.path.exists(cover_path)):
|
||||
default_cover = os.path.join(os.path.dirname(__file__), 'defaultcover.jpeg')
|
||||
if os.path.exists(default_cover):
|
||||
cover_path = default_cover
|
||||
if cover_path and os.path.exists(cover_path):
|
||||
from PyQt6.QtGui import QPixmap
|
||||
pm = QPixmap(cover_path)
|
||||
if not pm.isNull():
|
||||
pm = pm.scaled(thumb_w, thumb_h, Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
||||
cover_lab.setPixmap(pm)
|
||||
else:
|
||||
cover_lab.setText('无封面')
|
||||
else:
|
||||
cover_lab.setText('无封面')
|
||||
name_full = info.get('displayname') or info.get('itemname') or asset_id
|
||||
name_short = name_full[:8] + '…' if len(name_full) > 8 else name_full
|
||||
name_lab = QLabel(name_short)
|
||||
author = info.get('author','')
|
||||
tip = name_full if not author else f"{name_full}\n{author}"
|
||||
cover_lab.setToolTip(tip)
|
||||
name_lab.setToolTip(tip)
|
||||
def make_click_handler(aid):
|
||||
def handler(event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._on_finished_cover_clicked(aid)
|
||||
return handler
|
||||
cover_lab.mousePressEvent = make_click_handler(asset_id)
|
||||
name_lab.setWordWrap(True)
|
||||
name_lab.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
|
||||
self._finished_widgets.append((cover_lab, name_lab))
|
||||
self._relayout_finished_grid()
|
||||
|
||||
def _relayout_finished_grid(self):
|
||||
if not hasattr(self, 'finishedGridLayout') or not hasattr(self, '_finished_widgets'):
|
||||
return
|
||||
scroll_area = getattr(self, 'finished_scroll_area', None)
|
||||
if scroll_area is not None:
|
||||
viewport = scroll_area.viewport()
|
||||
width = viewport.width()
|
||||
else:
|
||||
width = self.width()
|
||||
thumb_w = 100
|
||||
h_gap = 12
|
||||
min_cols = 2
|
||||
cols = max(min_cols, width // (thumb_w + h_gap))
|
||||
while self.finishedGridLayout.count():
|
||||
item = self.finishedGridLayout.takeAt(0)
|
||||
w = item.widget()
|
||||
if w:
|
||||
w.setParent(None)
|
||||
row = 0
|
||||
col = 0
|
||||
for cover_lab, name_lab in self._finished_widgets:
|
||||
self.finishedGridLayout.addWidget(cover_lab, row*2, col)
|
||||
self.finishedGridLayout.addWidget(name_lab, row*2+1, col)
|
||||
col += 1
|
||||
if col >= cols:
|
||||
col = 0
|
||||
row += 1
|
|
@ -1,16 +1,23 @@
|
|||
import sys
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import unquote
|
||||
import datetime
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QListWidget,
|
||||
QFileDialog, QMessageBox, QLineEdit, QFormLayout, QDialog, QDialogButtonBox
|
||||
QApplication, QWidget, QPushButton, QLabel, QListWidget,
|
||||
QFileDialog, QMessageBox, QLineEdit, QFormLayout, QDialog, QDialogButtonBox, QTextEdit, QHBoxLayout, QSizePolicy
|
||||
)
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6.QtGui import QIcon, QPixmap
|
||||
from PyQt6.QtCore import QSettings, QSize, QByteArray
|
||||
from PyQt6 import uic
|
||||
import config
|
||||
from exportbooknotes import BookNotesExporter
|
||||
from booklist_parse import BookListManager
|
||||
from review_worker import BookReviewWorker
|
||||
from cover_mixin import CoverMixin
|
||||
from finished_books_mixin import FinishedBooksMixin
|
||||
|
||||
|
||||
|
||||
class ConfigDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
|
@ -32,68 +39,309 @@ class ConfigDialog(QDialog):
|
|||
def get_config(self):
|
||||
return {k: v.text() for k, v in self.inputs.items()}
|
||||
|
||||
class IBookExportApp(QWidget):
|
||||
class IBookExportApp(CoverMixin, FinishedBooksMixin, QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# 加载 UI 文件
|
||||
# ====== UI 加载 ======
|
||||
ui_file = os.path.join(os.path.dirname(__file__), 'ibook_export_app.ui')
|
||||
uic.loadUi(ui_file, self)
|
||||
|
||||
# 设置窗口标题
|
||||
self.setWindowTitle("notesExporter")
|
||||
|
||||
# 设置窗口图标
|
||||
self.setWindowTitle("iBook笔记专家")
|
||||
if os.path.exists(config.APP_ICON):
|
||||
self.setWindowIcon(QIcon(config.APP_ICON))
|
||||
|
||||
# 初始化数据
|
||||
# ====== 数据准备 ======
|
||||
try:
|
||||
from exportbooknotes import sync_source_files
|
||||
sync_source_files(config)
|
||||
except Exception as e:
|
||||
print(f"警告: 初始同步源数据失败: {e}")
|
||||
self.exporter = BookNotesExporter(config)
|
||||
self.manager = BookListManager(plist_path=config.LOCAL_BOOKS_PLIST, db_path=config.LOCAL_LIBRARY_DB)
|
||||
self.booksinfo = self.manager.get_books_info()
|
||||
self.last_open_times = self.manager.get_books_last_open()
|
||||
self.assetid2name = {}
|
||||
self.assetid2lastopen = {}
|
||||
self.assetid2name, self.assetid2lastopen = {}, {}
|
||||
for assetid, info in self.booksinfo.items():
|
||||
name = info.get('displayname') or info.get('itemname') or assetid
|
||||
if '-' in name:
|
||||
name = name.split('-', 1)[0].strip()
|
||||
self.assetid2name[assetid] = name
|
||||
ts = self.last_open_times.get(assetid, {}).get('last_open', 0)
|
||||
self.assetid2lastopen[assetid] = ts
|
||||
sorted_assetids = sorted(self.assetid2name.keys(), key=lambda aid: self.assetid2lastopen[aid], reverse=True)
|
||||
self.sorted_assetids = sorted_assetids
|
||||
|
||||
# 填充书籍列表
|
||||
for aid in sorted_assetids:
|
||||
self.assetid2lastopen[assetid] = self.last_open_times.get(assetid, {}).get('last_open', 0)
|
||||
self.sorted_assetids = sorted(self.assetid2name.keys(), key=lambda aid: self.assetid2lastopen[aid], reverse=True)
|
||||
for aid in self.sorted_assetids:
|
||||
self.listwidget.addItem(f"{self.assetid2name[aid]} [{self.assetid2lastopen[aid]}]")
|
||||
|
||||
# 连接信号
|
||||
# ====== 信号 ======
|
||||
self.export_btn.clicked.connect(self.export_notes)
|
||||
self.config_btn.clicked.connect(self.show_config)
|
||||
|
||||
# 回车直接导出(使用事件过滤器)
|
||||
self.listwidget.currentRowChanged.connect(self.update_book_info)
|
||||
self.listwidget.installEventFilter(self)
|
||||
# ====== 封面标签 ======
|
||||
if all(hasattr(self, n) for n in ('cover_label_1','cover_label_2','cover_label_3')):
|
||||
self._cover_labels = [self.cover_label_1, self.cover_label_2, self.cover_label_3]
|
||||
else:
|
||||
self._cover_labels = [getattr(self, 'book_cover_label', QLabel('封面', self))]
|
||||
for lab in self._cover_labels:
|
||||
lab.setMinimumWidth(180)
|
||||
lab.setMaximumWidth(180)
|
||||
lab.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
||||
try:
|
||||
from PyQt6.QtCore import Qt as _QtFP
|
||||
lab.setFocusPolicy(_QtFP.FocusPolicy.NoFocus)
|
||||
except Exception:
|
||||
pass
|
||||
# 封面高度是否弹性跟随文本区(默认 False: 固定算法,不扩张)
|
||||
self.cover_elastic = False
|
||||
# 调整文本区域弹性:宽度随可用空间扩展,高度优先扩展
|
||||
try:
|
||||
from PyQt6.QtWidgets import QSizePolicy as _QSP
|
||||
sp = self.book_toc_textedit.sizePolicy()
|
||||
sp.setHorizontalPolicy(_QSP.Policy.Expanding)
|
||||
sp.setVerticalPolicy(_QSP.Policy.Expanding)
|
||||
self.book_toc_textedit.setSizePolicy(sp)
|
||||
# 给封面横排区域一个较小的最小高度,避免撑开
|
||||
if hasattr(self, 'covers_layout') and self._cover_labels:
|
||||
for lab in self._cover_labels:
|
||||
lab.setMinimumHeight(10)
|
||||
except Exception:
|
||||
pass
|
||||
self.cover_ratio = 1.2
|
||||
self._export_tab_index = None
|
||||
self.book_toc_textedit.setPlainText("书籍信息 / 简评")
|
||||
# 状态 & 缓存
|
||||
self._review_worker = None
|
||||
self._current_bookname = None
|
||||
self._active_workers = []
|
||||
self._cover_pixmaps_original = []
|
||||
# 恢复窗口尺寸
|
||||
self._restore_window_geometry()
|
||||
# 设置封面标签对齐
|
||||
try:
|
||||
from PyQt6.QtCore import Qt as _QtAlign
|
||||
for lab in self._cover_labels:
|
||||
lab.setAlignment(_QtAlign.AlignmentFlag.AlignHCenter | _QtAlign.AlignmentFlag.AlignTop)
|
||||
except Exception:
|
||||
pass
|
||||
# 初始封面 + 首本书信息 (及 AI 简评触发)
|
||||
self._load_initial()
|
||||
# 已读书籍网格
|
||||
try:
|
||||
self._populate_finished_books_grid()
|
||||
except Exception as e:
|
||||
print('警告: 已读书籍网格填充失败', e)
|
||||
# 滚动区域策略 & 事件过滤
|
||||
if hasattr(self, 'finished_scroll_area'):
|
||||
from PyQt6.QtCore import Qt as _Qt
|
||||
self.finished_scroll_area.setHorizontalScrollBarPolicy(_Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.finished_scroll_area.setVerticalScrollBarPolicy(_Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
try:
|
||||
self.finished_scroll_area.viewport().installEventFilter(self)
|
||||
except Exception as _e_vp:
|
||||
print('信息: 安装 viewport 事件过滤器失败', _e_vp)
|
||||
# Tab 切换监听 (C + A 方案)
|
||||
from PyQt6.QtWidgets import QTabWidget
|
||||
try:
|
||||
tabs = self.findChildren(QTabWidget)
|
||||
if tabs:
|
||||
self._main_tab_widget = tabs[0]
|
||||
self._main_tab_widget.currentChanged.connect(self._on_main_tab_changed)
|
||||
self._detect_export_tab_index()
|
||||
except Exception as _e_tab:
|
||||
print('信息: 连接 tab 切换失败', _e_tab)
|
||||
# 首次显示后再排一次
|
||||
from PyQt6.QtCore import QTimer
|
||||
QTimer.singleShot(80, self._relayout_finished_grid)
|
||||
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
from PyQt6.QtCore import QEvent, Qt
|
||||
from PyQt6.QtCore import QEvent
|
||||
# 方案 C: 监听 finished_scroll_area 的 viewport Resize
|
||||
try:
|
||||
if hasattr(self, 'finished_scroll_area') and obj == self.finished_scroll_area.viewport():
|
||||
if event.type() == QEvent.Type.Resize:
|
||||
# 可选节流:仅当宽度真实变化较大时
|
||||
new_w = obj.width()
|
||||
last_w = getattr(self, '_finished_viewport_last_width', None)
|
||||
if last_w is None or abs(new_w - last_w) > 8:
|
||||
self._finished_viewport_last_width = new_w
|
||||
self._relayout_finished_grid()
|
||||
except Exception:
|
||||
pass
|
||||
# 原有 listwidget 回车导出逻辑
|
||||
if obj == self.listwidget and event.type() == QEvent.Type.KeyPress:
|
||||
# 检查回车键(Enter/Return)
|
||||
if event.key() in (0x01000004, 0x01000005): # Qt.Key_Return, Qt.Key_Enter
|
||||
if event.key() in (0x01000004, 0x01000005): # Return / Enter
|
||||
self.export_notes()
|
||||
return True
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def show_config(self):
|
||||
dlg = ConfigDialog(self)
|
||||
if dlg.exec():
|
||||
new_config = dlg.get_config()
|
||||
# 这里只是演示,实际可写入config.py或动态加载
|
||||
QMessageBox.information(self, "提示", "配置已更新(仅本次运行有效)")
|
||||
|
||||
def _load_initial(self):
|
||||
"""启动时:
|
||||
1. 显示前三本封面
|
||||
2. 初始化文本区域为第一本书的基础信息(无简评内容,只留段落标题)
|
||||
"""
|
||||
try:
|
||||
if not hasattr(self, '_cover_labels') or not self.sorted_assetids:
|
||||
return
|
||||
from PyQt6.QtGui import QPixmap
|
||||
self._cover_pixmaps_original = []
|
||||
first_indices = list(range(min(3, len(self.sorted_assetids))))
|
||||
for pos in range(3):
|
||||
if pos < len(first_indices):
|
||||
aid = self.sorted_assetids[first_indices[pos]]
|
||||
info = self.booksinfo.get(aid, {})
|
||||
cpath = self.find_book_cover(aid, info)
|
||||
if cpath:
|
||||
pm = QPixmap(cpath)
|
||||
if not pm.isNull():
|
||||
self._cover_pixmaps_original.append(pm)
|
||||
self._cover_labels[pos].setPixmap(pm)
|
||||
continue
|
||||
self._cover_pixmaps_original.append(None)
|
||||
self._cover_labels[pos].setText('无封面')
|
||||
else:
|
||||
self._cover_pixmaps_original.append(None)
|
||||
self._cover_labels[pos].setText('')
|
||||
self._apply_cover_scale()
|
||||
# 首本书信息
|
||||
first_aid = self.sorted_assetids[0]
|
||||
first_info = self.booksinfo.get(first_aid, {})
|
||||
bookname_display = first_info.get('displayname') or first_info.get('itemname') or first_aid
|
||||
author = first_info.get('author', '')
|
||||
btype = first_info.get('type', '')
|
||||
get_time = first_info.get('date', '')
|
||||
self._base_info_cache = {
|
||||
'bookname': bookname_display,
|
||||
'author': author,
|
||||
'type': btype,
|
||||
'date': get_time
|
||||
}
|
||||
self._current_bookname = bookname_display
|
||||
# 读取/触发首本书书评
|
||||
import json
|
||||
json_path = os.path.join(os.path.dirname(__file__), 'bookintro.json')
|
||||
try:
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
intro_dict = json.load(f)
|
||||
except Exception:
|
||||
intro_dict = {}
|
||||
review = intro_dict.get(bookname_display)
|
||||
if review:
|
||||
html = self._build_book_html(review)
|
||||
self.book_toc_textedit.setHtml(html)
|
||||
else:
|
||||
loading_html = self._build_book_html("简评获取中...")
|
||||
self.book_toc_textedit.setHtml(loading_html)
|
||||
prompt = f"{bookname_display} 400字书评 三段 简洁精炼"
|
||||
worker = BookReviewWorker(bookname_display, prompt, json_path, parent=self)
|
||||
worker.finished.connect(lambda bname, rev: self._on_review_finished(bname, rev))
|
||||
worker.finished.connect(lambda _b, _r, w=worker: self._remove_worker(w))
|
||||
self._review_worker = worker
|
||||
self._active_workers.append(worker)
|
||||
worker.start()
|
||||
except Exception as e:
|
||||
print('初始加载失败:', e)
|
||||
|
||||
|
||||
def _on_main_tab_changed(self, index):
|
||||
"""方案 A: 当切换到 '已读书籍' 标签时强制重排一次,并做一次短延迟的二次重排,避免初次显示宽度未稳定。"""
|
||||
try:
|
||||
if not hasattr(self, '_main_tab_widget'):
|
||||
return
|
||||
w = self._main_tab_widget.widget(index)
|
||||
# 通过对象名或包含的 finished_scroll_area 判断(加括号避免优先级问题)
|
||||
hit = False
|
||||
if hasattr(self, 'finished_scroll_area'):
|
||||
try:
|
||||
if self.finished_scroll_area.isAncestorOf(w) or w.isAncestorOf(self.finished_scroll_area):
|
||||
hit = True
|
||||
except Exception:
|
||||
pass
|
||||
# 退化:检查对象名包含关键字
|
||||
if not hit:
|
||||
name = getattr(w, 'objectName', lambda: '')()
|
||||
if 'finished' in name.lower():
|
||||
hit = True
|
||||
if hit:
|
||||
# 立即重排
|
||||
self._relayout_finished_grid()
|
||||
from PyQt6.QtCore import QTimer
|
||||
# 120ms 后再重排一次(宽度稳定后)
|
||||
QTimer.singleShot(120, self._relayout_finished_grid)
|
||||
except Exception as e:
|
||||
print('tab 切换重排失败:', e)
|
||||
|
||||
def showEvent(self, event):
|
||||
"""首次显示窗口后,再安排一次延迟重排,提升初始网格正确率。"""
|
||||
try:
|
||||
super().showEvent(event)
|
||||
from PyQt6.QtCore import QTimer
|
||||
QTimer.singleShot(80, self._relayout_finished_grid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _detect_export_tab_index(self):
|
||||
"""探测导出标签索引:优先使用 ui 中命名的 tab_export;否则通过包含 listwidget 的页面推断。"""
|
||||
if getattr(self, '_export_tab_index', None) is not None:
|
||||
return
|
||||
try:
|
||||
if not hasattr(self, '_main_tab_widget'):
|
||||
return
|
||||
idx = -1
|
||||
if hasattr(self, 'tab_export'):
|
||||
idx = self._main_tab_widget.indexOf(self.tab_export)
|
||||
if idx < 0:
|
||||
from PyQt6.QtWidgets import QListWidget
|
||||
for i in range(self._main_tab_widget.count()):
|
||||
page = self._main_tab_widget.widget(i)
|
||||
if page.findChild(QListWidget, 'listwidget') is not None:
|
||||
idx = i
|
||||
break
|
||||
if idx < 0:
|
||||
idx = 0 # 兜底
|
||||
self._export_tab_index = idx
|
||||
except Exception as e:
|
||||
print('探测导出标签索引失败:', e)
|
||||
|
||||
def _switch_to_export_tab(self):
|
||||
try:
|
||||
if not hasattr(self, '_main_tab_widget'):
|
||||
from PyQt6.QtWidgets import QTabWidget
|
||||
tabs = self.findChildren(QTabWidget)
|
||||
if tabs:
|
||||
self._main_tab_widget = tabs[0]
|
||||
if not hasattr(self, '_main_tab_widget'):
|
||||
return
|
||||
self._detect_export_tab_index()
|
||||
if self._export_tab_index is None:
|
||||
return
|
||||
if self._main_tab_widget.currentIndex() != self._export_tab_index:
|
||||
self._main_tab_widget.setCurrentIndex(self._export_tab_index)
|
||||
except Exception as e:
|
||||
print('切换导出标签失败:', e)
|
||||
|
||||
def _on_finished_cover_clicked(self, asset_id):
|
||||
# 在主列表中选中对应书籍(若存在)
|
||||
try:
|
||||
if not hasattr(self, 'sorted_assetids'):
|
||||
return
|
||||
if asset_id not in self.sorted_assetids:
|
||||
return
|
||||
row = self.sorted_assetids.index(asset_id)
|
||||
# 切换到“导出”标签(自动探测索引)
|
||||
self._switch_to_export_tab()
|
||||
self.listwidget.setCurrentRow(row)
|
||||
# 确保可见并聚焦
|
||||
try:
|
||||
item = self.listwidget.item(row)
|
||||
if item:
|
||||
self.listwidget.scrollToItem(item)
|
||||
self.listwidget.setFocus()
|
||||
except Exception:
|
||||
pass
|
||||
# 触发 update_book_info 逻辑自动刷新右侧
|
||||
except Exception as e:
|
||||
print('点击已读书籍封面失败:', e)
|
||||
|
||||
def export_notes(self):
|
||||
from exportbooknotes import sync_source_files
|
||||
sync_source_files(config)
|
||||
idx = self.listwidget.currentRow()
|
||||
if idx < 0:
|
||||
QMessageBox.warning(self, "提示", "请先选择一本书")
|
||||
|
@ -111,12 +359,283 @@ class IBookExportApp(QWidget):
|
|||
self.exporter.export_booksnote_to_md(selected_booksnote, selected_booksinfo, out_path)
|
||||
QMessageBox.information(self, "导出成功", f"已导出到:{out_path}")
|
||||
|
||||
def show_config(self):
|
||||
dlg = ConfigDialog(self)
|
||||
if dlg.exec():
|
||||
dlg.get_config()
|
||||
QMessageBox.information(self, "提示", "配置已更新(仅本次运行有效)")
|
||||
|
||||
def update_book_info(self, row):
|
||||
if row < 0:
|
||||
# 不再清空:保持初始加载的封面;仅清空文本
|
||||
self.book_toc_textedit.clear()
|
||||
return
|
||||
assetid = self.sorted_assetids[row]
|
||||
book_info = self.booksinfo.get(assetid, {})
|
||||
# 计算当前与后续两本
|
||||
total = len(self.sorted_assetids)
|
||||
indices = [(row + i) % total for i in range(min(3,total))]
|
||||
self._cover_pixmaps_original = []
|
||||
from PyQt6.QtGui import QPixmap
|
||||
# 先清空所有标签
|
||||
for lab in self._cover_labels:
|
||||
lab.clear()
|
||||
lab.setText("加载中")
|
||||
for pos, aid_idx in enumerate(indices):
|
||||
aid_show = self.sorted_assetids[aid_idx]
|
||||
binfo = self.booksinfo.get(aid_show, {})
|
||||
cpath = self.find_book_cover(aid_show, binfo)
|
||||
label = self._cover_labels[pos]
|
||||
if cpath:
|
||||
pm = QPixmap(cpath)
|
||||
if not pm.isNull():
|
||||
self._cover_pixmaps_original.append(pm)
|
||||
label.setText("")
|
||||
continue
|
||||
self._cover_pixmaps_original.append(None)
|
||||
label.setText("无封面")
|
||||
# 填充不足三本情况
|
||||
for pos in range(len(indices), 3):
|
||||
lab = self._cover_labels[pos]
|
||||
lab.setText("无")
|
||||
self._cover_pixmaps_original.append(None)
|
||||
self._apply_cover_scale()
|
||||
# 生成 HTML 信息基础部分
|
||||
bookname_display = book_info.get('displayname', '') or book_info.get('itemname', '') or assetid
|
||||
author = book_info.get('author', '')
|
||||
btype = book_info.get('type', '')
|
||||
get_time = book_info.get('date', '')
|
||||
self._base_info_cache = {
|
||||
'bookname': bookname_display,
|
||||
'author': author,
|
||||
'type': btype,
|
||||
'date': get_time
|
||||
}
|
||||
import json
|
||||
bookname = bookname_display
|
||||
self._current_bookname = bookname
|
||||
json_path = os.path.join(os.path.dirname(__file__), 'bookintro.json')
|
||||
try:
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
intro_dict = json.load(f)
|
||||
except Exception:
|
||||
intro_dict = {}
|
||||
review = intro_dict.get(bookname)
|
||||
if review:
|
||||
html = self._build_book_html(review)
|
||||
self.book_toc_textedit.setHtml(html)
|
||||
else:
|
||||
prompt = f"{bookname} 400字书评 三段 简洁精炼"
|
||||
loading_html = self._build_book_html("简评获取中...")
|
||||
self.book_toc_textedit.setHtml(loading_html)
|
||||
worker = BookReviewWorker(bookname, prompt, json_path, parent=self)
|
||||
# UI 更新
|
||||
worker.finished.connect(lambda bname, review: self._on_review_finished(bname, review))
|
||||
# 完成后从活动列表移除
|
||||
worker.finished.connect(lambda _b, _r, w=worker: self._remove_worker(w))
|
||||
self._review_worker = worker
|
||||
self._active_workers.append(worker)
|
||||
worker.start()
|
||||
|
||||
def _on_review_finished(self, bookname, review, base_text=None):
|
||||
# base_text 兼容旧调用,可忽略
|
||||
if bookname != self._current_bookname:
|
||||
return
|
||||
html = self._build_book_html(review)
|
||||
self.book_toc_textedit.setHtml(html)
|
||||
|
||||
|
||||
def _load_initial_covers(self):
|
||||
"""在应用启动时加载列表中前三本书的封面到三个标签。"""
|
||||
try:
|
||||
if not hasattr(self, '_cover_labels') or not self.sorted_assetids:
|
||||
return
|
||||
from PyQt6.QtGui import QPixmap
|
||||
self._cover_pixmaps_original = []
|
||||
# 取前三本
|
||||
first_indices = list(range(min(3, len(self.sorted_assetids))))
|
||||
for pos in range(3):
|
||||
if pos < len(first_indices):
|
||||
aid = self.sorted_assetids[first_indices[pos]]
|
||||
info = self.booksinfo.get(aid, {})
|
||||
cpath = self.find_book_cover(aid, info)
|
||||
if cpath:
|
||||
pm = QPixmap(cpath)
|
||||
if not pm.isNull():
|
||||
self._cover_pixmaps_original.append(pm)
|
||||
self._cover_labels[pos].setPixmap(pm)
|
||||
continue
|
||||
self._cover_pixmaps_original.append(None)
|
||||
self._cover_labels[pos].setText('无封面')
|
||||
else:
|
||||
self._cover_pixmaps_original.append(None)
|
||||
self._cover_labels[pos].setText('')
|
||||
self._apply_cover_scale()
|
||||
except Exception as e:
|
||||
print('初始封面加载失败:', e)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
# 窗口尺寸变化时重新计算封面大小
|
||||
try:
|
||||
self._apply_cover_scale()
|
||||
self._relayout_finished_grid()
|
||||
except Exception:
|
||||
pass
|
||||
super().resizeEvent(event)
|
||||
|
||||
def _build_book_html(self, review_text: str) -> str:
|
||||
"""构建包含加粗紫红色标题的 HTML 内容,AI书评分段显示。"""
|
||||
info = getattr(self, '_base_info_cache', {})
|
||||
magenta = "#C71585" # 紫红色
|
||||
def line(title, value):
|
||||
return f"<p><span style='color:{magenta};font-weight:bold;'>{title}</span> {value}</p>"
|
||||
# 书评分段处理
|
||||
def review_lines(text):
|
||||
# 按换行或两个以上空格分段
|
||||
if not text:
|
||||
return [""]
|
||||
# 兼容多种分段格式
|
||||
segments = [seg.strip() for seg in re.split(r'\n{2,}|\r{2,}|\n|\r|\s{2,}', text) if seg.strip()]
|
||||
return [f"<p>{seg}</p>" for seg in segments]
|
||||
parts = [
|
||||
line("书名:", f"{info.get('author','')} - {info.get('bookname','')}") ,
|
||||
line("作者:", info.get('author','')),
|
||||
line("类型:", info.get('type','')),
|
||||
line("获取时间:", info.get('date','')),
|
||||
f"<span style='color:{magenta};font-weight:bold;'>书籍简评:</span>"
|
||||
]
|
||||
parts += review_lines(review_text)
|
||||
return "".join(parts)
|
||||
|
||||
def _init_charts(self):
|
||||
"""使用原生 Qt 组件渲染统计标签页四个图表(取代 matplotlib)。"""
|
||||
try:
|
||||
from charts import BarChartWidget, BubbleMetricsWidget, ScatterChartWidget
|
||||
except Exception as e:
|
||||
print('警告: 无法导入原生图表组件 charts.py:', e)
|
||||
return
|
||||
required = [
|
||||
('frame_week', 'weekLayout'),
|
||||
('frame_month', 'monthLayout'),
|
||||
('frame_year', 'yearLayout'),
|
||||
('frame_bubble', 'bubbleLayout'),
|
||||
]
|
||||
for attr, layout_name in required:
|
||||
if not hasattr(self, attr) or not hasattr(self, layout_name):
|
||||
print('信息: 缺少统计容器', attr)
|
||||
return
|
||||
try:
|
||||
week_data = self.manager.get_total_readtime(days=7)
|
||||
month_data = self.manager.get_total_readtime(days=30)
|
||||
year_data = self.manager.get_total_readtime12m()
|
||||
year_total_minutes = self.manager.get_total_readtime_year()
|
||||
except Exception as e:
|
||||
print('警告: 统计数据获取失败:', e)
|
||||
return
|
||||
if all(v == 0 for v in week_data + month_data + year_data):
|
||||
for _, layout_name in required:
|
||||
getattr(self, layout_name).addWidget(QLabel('暂无阅读数据'))
|
||||
return
|
||||
# 最近7天:weekday 英文缩写(索引0=今天)
|
||||
today = datetime.date.today()
|
||||
recent_days = [today - datetime.timedelta(days=i) for i in range(len(week_data))]
|
||||
WEEK_ABBR = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
|
||||
week_labels = [WEEK_ABBR[d.weekday()] for d in recent_days]
|
||||
# 最近30天:日期标签,旋转45度防重叠
|
||||
month_recent_days = [today - datetime.timedelta(days=i) for i in range(len(month_data))]
|
||||
month_labels = [f"{d.month}月{d.day}日" for d in month_recent_days]
|
||||
year_labels = [f'{i+1}月' for i in range(12)]
|
||||
year_hours = [round(m/60.0, 1) for m in year_data]
|
||||
week_chart = BarChartWidget(week_data, title='', unit='分钟', labels=week_labels, value_format=lambda v: f'{int(v)}')
|
||||
|
||||
# 让横坐标先显示第30天数据(即最近的日期在最左侧)
|
||||
month_chart = BarChartWidget(
|
||||
month_data[::-1], # 反转数据
|
||||
title='',
|
||||
unit='分钟',
|
||||
labels=month_labels[::-1], # 反转标签
|
||||
value_format=lambda v: f'{int(v)}',
|
||||
label_rotation=45
|
||||
)
|
||||
year_chart = ScatterChartWidget(year_hours, title='', unit='小时', labels=year_labels)
|
||||
# 确保图表在网格中可弹性扩展
|
||||
from PyQt6.QtWidgets import QSizePolicy
|
||||
for wdg in (week_chart, month_chart, year_chart):
|
||||
wdg.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.weekLayout.addWidget(week_chart)
|
||||
self.monthLayout.addWidget(month_chart)
|
||||
self.yearLayout.addWidget(year_chart)
|
||||
year_hours_total = year_total_minutes / 60.0
|
||||
month_avg_hours = (sum(year_data)/12.0)/60.0 if year_data else 0
|
||||
week_hours = sum(week_data)/60.0
|
||||
day_avg_minutes = (sum(month_data)/30.0) if month_data else 0
|
||||
bubble_metrics = [
|
||||
('全年', year_hours_total, 'h', '#5b6ee1'),
|
||||
('月均', month_avg_hours, 'h', '#c9b2d9'),
|
||||
('近7天', week_hours, 'h', '#f4b2c2'),
|
||||
('日均', day_avg_minutes, 'm', '#b9b542'),
|
||||
]
|
||||
bubble_widget = BubbleMetricsWidget(bubble_metrics)
|
||||
bubble_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.bubbleLayout.addWidget(bubble_widget)
|
||||
|
||||
# ---------------- 窗口尺寸持久化 ----------------
|
||||
def _restore_window_geometry(self):
|
||||
settings = QSettings('iBookTools', 'iBook笔记专家')
|
||||
geo = settings.value('mainWindowGeometry')
|
||||
if isinstance(geo, QByteArray) and not geo.isEmpty():
|
||||
try:
|
||||
self.restoreGeometry(geo)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
# 兼容旧版本仅存宽高
|
||||
w = settings.value('windowWidth', type=int)
|
||||
h = settings.value('windowHeight', type=int)
|
||||
if w and h and w > 100 and h > 100:
|
||||
self.resize(QSize(w, h))
|
||||
else:
|
||||
# 默认初始尺寸
|
||||
self.resize(1500, 900)
|
||||
|
||||
def closeEvent(self, event):
|
||||
try:
|
||||
settings = QSettings('iBookTools', 'iBook笔记专家')
|
||||
# 新格式:直接保存几何
|
||||
settings.setValue('mainWindowGeometry', self.saveGeometry())
|
||||
# 兼容旧字段
|
||||
settings.setValue('windowWidth', self.width())
|
||||
settings.setValue('windowHeight', self.height())
|
||||
# 优雅等待所有后台线程结束(给最多 8 秒)
|
||||
deadline = datetime.datetime.now() + datetime.timedelta(seconds=8)
|
||||
for w in list(self._active_workers):
|
||||
if w.isRunning():
|
||||
remaining = (deadline - datetime.datetime.now()).total_seconds()
|
||||
if remaining <= 0:
|
||||
break
|
||||
# wait 参数是毫秒
|
||||
w.wait(int(min(remaining, 2) * 1000)) # 分批等待,避免一次性卡太久
|
||||
except Exception:
|
||||
pass
|
||||
super().closeEvent(event)
|
||||
|
||||
def _remove_worker(self, worker):
|
||||
try:
|
||||
if worker in self._active_workers:
|
||||
self._active_workers.remove(worker)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------
|
||||
|
||||
# ------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 设置应用程序名称和组织信息
|
||||
app.setApplicationName("notesExporter")
|
||||
app.setApplicationDisplayName("notesExporter")
|
||||
app.setApplicationName("iBook笔记专家")
|
||||
app.setApplicationDisplayName("iBook笔记专家")
|
||||
app.setOrganizationName("iBook Tools")
|
||||
|
||||
# 设置应用程序图标
|
||||
|
@ -124,5 +643,9 @@ if __name__ == "__main__":
|
|||
app.setWindowIcon(QIcon(config.APP_ICON))
|
||||
|
||||
win = IBookExportApp()
|
||||
try:
|
||||
win._init_charts()
|
||||
except Exception:
|
||||
pass
|
||||
win.show()
|
||||
sys.exit(app.exec())
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>400</height>
|
||||
<width>722</width>
|
||||
<height>614</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -15,27 +15,276 @@
|
|||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>请选择要导出的书籍:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="listwidget"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="export_btn">
|
||||
<property name="text">
|
||||
<string>导出</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="config_btn">
|
||||
<property name="text">
|
||||
<string>配置参数</string>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab_export">
|
||||
<attribute name="title">
|
||||
<string>导出</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="tab_export_layout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="left_layout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>请选择要导出的书籍:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="listwidget"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="export_btn">
|
||||
<property name="text">
|
||||
<string>导出</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="config_btn">
|
||||
<property name="text">
|
||||
<string>配置参数</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="right_layout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="cover_and_text_layout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="covers_layout">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="cover_label_1">
|
||||
<property name="text">
|
||||
<string>当前选中cover</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>160</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::StyledPanel</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="cover_label_2">
|
||||
<property name="text">
|
||||
<string>下一本</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>160</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::StyledPanel</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="cover_label_3">
|
||||
<property name="text">
|
||||
<string>再下一本</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>160</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::StyledPanel</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="book_toc_textedit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>书籍信息 / 简评</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_stats">
|
||||
<attribute name="title">
|
||||
<string>统计</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="statsGridLayout">
|
||||
<property name="horizontalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item row="1" column="1">
|
||||
<widget class="QFrame" name="frame_month">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="monthLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_month">
|
||||
<property name="text">
|
||||
<string>最近30天日均阅读时间统计</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QFrame" name="frame_year">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="yearLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_year">
|
||||
<property name="text">
|
||||
<string>月度阅读小时数</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignTop|Qt::AlignmentFlag::AlignCenter|Qt::AlignmentFlag::AlignLeading</set>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QFrame" name="frame_week">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="weekLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_week">
|
||||
<property name="text">
|
||||
<string>最近7天阅读时间统计</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QFrame" name="frame_bubble">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="bubbleLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_bubble">
|
||||
<property name="text">
|
||||
<string>阅读总览(全年/月均/近7天/日均)</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_finished">
|
||||
<attribute name="title">
|
||||
<string>已读书籍</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="finished_outer_layout">
|
||||
<item>
|
||||
<widget class="QScrollArea" name="finished_scroll_area">
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="finished_scroll_contents">
|
||||
<layout class="QGridLayout" name="finishedGridLayout">
|
||||
<property name="horizontalSpacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>16</number>
|
||||
</property>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
import sys
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QListWidget,
|
||||
QFileDialog, QMessageBox, QLineEdit, QFormLayout, QDialog, QDialogButtonBox
|
||||
)
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6 import uic
|
||||
import config
|
||||
from exportbooknotes import BookNotesExporter
|
||||
from booklist_parse import BookListManager
|
||||
|
||||
class ConfigDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("配置参数")
|
||||
layout = QFormLayout(self)
|
||||
self.inputs = {}
|
||||
for attr in dir(config):
|
||||
if attr.isupper():
|
||||
val = getattr(config, attr)
|
||||
inp = QLineEdit(str(val))
|
||||
layout.addRow(attr, inp)
|
||||
self.inputs[attr] = inp
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def get_config(self):
|
||||
return {k: v.text() for k, v in self.inputs.items()}
|
||||
|
||||
class IBookExportApp(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# 加载 UI 文件
|
||||
ui_file = os.path.join(os.path.dirname(__file__), 'ibook_export_app.ui')
|
||||
uic.loadUi(ui_file, self)
|
||||
|
||||
# 设置窗口标题
|
||||
self.setWindowTitle("notesExporter")
|
||||
|
||||
# 设置窗口图标
|
||||
if os.path.exists(config.APP_ICON):
|
||||
self.setWindowIcon(QIcon(config.APP_ICON))
|
||||
|
||||
# 启动前同步一次源数据到本地,确保后续 AnnotationManager 读取的是最新副本
|
||||
try:
|
||||
from exportbooknotes import sync_source_files
|
||||
sync_source_files(config)
|
||||
except Exception as e:
|
||||
print(f"警告: 初始同步源数据失败: {e}")
|
||||
|
||||
# 初始化数据
|
||||
self.exporter = BookNotesExporter(config)
|
||||
self.manager = BookListManager(plist_path=config.LOCAL_BOOKS_PLIST, db_path=config.LOCAL_LIBRARY_DB)
|
||||
self.booksinfo = self.manager.get_books_info()
|
||||
self.last_open_times = self.manager.get_books_last_open()
|
||||
self.assetid2name = {}
|
||||
self.assetid2lastopen = {}
|
||||
for assetid, info in self.booksinfo.items():
|
||||
name = info.get('displayname') or info.get('itemname') or assetid
|
||||
if '-' in name:
|
||||
name = name.split('-', 1)[0].strip()
|
||||
self.assetid2name[assetid] = name
|
||||
ts = self.last_open_times.get(assetid, {}).get('last_open', 0)
|
||||
self.assetid2lastopen[assetid] = ts
|
||||
sorted_assetids = sorted(self.assetid2name.keys(), key=lambda aid: self.assetid2lastopen[aid], reverse=True)
|
||||
self.sorted_assetids = sorted_assetids
|
||||
|
||||
# 填充书籍列表
|
||||
for aid in sorted_assetids:
|
||||
self.listwidget.addItem(f"{self.assetid2name[aid]} [{self.assetid2lastopen[aid]}]")
|
||||
|
||||
# 连接信号
|
||||
self.export_btn.clicked.connect(self.export_notes)
|
||||
self.config_btn.clicked.connect(self.show_config)
|
||||
|
||||
# 回车直接导出(使用事件过滤器)
|
||||
self.listwidget.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
from PyQt6.QtCore import QEvent, Qt
|
||||
if obj == self.listwidget and event.type() == QEvent.Type.KeyPress:
|
||||
# 检查回车键(Enter/Return)
|
||||
if event.key() in (0x01000004, 0x01000005): # Qt.Key_Return, Qt.Key_Enter
|
||||
self.export_notes()
|
||||
return True
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def show_config(self):
|
||||
dlg = ConfigDialog(self)
|
||||
if dlg.exec():
|
||||
new_config = dlg.get_config()
|
||||
# 这里只是演示,实际可写入config.py或动态加载
|
||||
QMessageBox.information(self, "提示", "配置已更新(仅本次运行有效)")
|
||||
|
||||
def export_notes(self):
|
||||
idx = self.listwidget.currentRow()
|
||||
if idx < 0:
|
||||
QMessageBox.warning(self, "提示", "请先选择一本书")
|
||||
return
|
||||
assetid = self.sorted_assetids[idx]
|
||||
selected_booksnote = self.exporter.build_booksnote(bookid=assetid)
|
||||
selected_booksinfo = {assetid: self.booksinfo.get(assetid, {})}
|
||||
bookname = selected_booksinfo[assetid].get("displayname") or selected_booksinfo[assetid].get("itemname") or assetid
|
||||
ts = datetime.datetime.now().strftime('%m%d%H%M')
|
||||
shortname = re.split(r'[.::_\【\[\((]', bookname)[0].strip()
|
||||
export_dir = getattr(config, "EXPORT_NOTES_DIR", os.getcwd())
|
||||
if not os.path.exists(export_dir):
|
||||
os.makedirs(export_dir)
|
||||
out_path = os.path.join(export_dir, f"notes_{shortname}-{ts}.md")
|
||||
self.exporter.export_booksnote_to_md(selected_booksnote, selected_booksinfo, out_path)
|
||||
QMessageBox.information(self, "导出成功", f"已导出到:{out_path}")
|
||||
|
||||
# ---------------- 图表相关 -----------------
|
||||
def _init_charts(self):
|
||||
"""初始化并渲染统计标签页内的四个图表。若 matplotlib 不可用或 frame 不存在则忽略。"""
|
||||
# 延迟导入 matplotlib,避免无图形依赖时阻塞主功能
|
||||
try:
|
||||
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
# 中文字体设置:尝试常见中文字体,找到即设置
|
||||
try:
|
||||
import matplotlib
|
||||
from matplotlib import font_manager
|
||||
candidate_fonts = [
|
||||
'PingFang SC', 'Heiti SC', 'STHeiti', 'Hiragino Sans GB', 'Songti SC',
|
||||
'SimHei', 'SimSun', 'Microsoft YaHei', 'WenQuanYi Zen Hei'
|
||||
]
|
||||
available = set(f.name for f in font_manager.fontManager.ttflist)
|
||||
zh_font = None
|
||||
for f in candidate_fonts:
|
||||
if f in available:
|
||||
zh_font = f
|
||||
break
|
||||
if zh_font:
|
||||
matplotlib.rcParams['font.family'] = zh_font
|
||||
# 解决负号显示问题
|
||||
matplotlib.rcParams['axes.unicode_minus'] = False
|
||||
except Exception as fe:
|
||||
print(f"信息: 中文字体配置失败: {fe}")
|
||||
except Exception as e: # matplotlib 可能未安装
|
||||
print(f"信息: 未加载统计图表(matplotlib不可用):{e}")
|
||||
return
|
||||
|
||||
# 检查 frame 是否存在
|
||||
required_frames = [
|
||||
('frame_week', 'weekLayout'),
|
||||
('frame_month', 'monthLayout'),
|
||||
('frame_year', 'yearLayout'),
|
||||
('frame_bubble', 'bubbleLayout'),
|
||||
]
|
||||
for attr, _ in required_frames:
|
||||
if not hasattr(self, attr):
|
||||
print("信息: 缺少统计容器", attr)
|
||||
return
|
||||
|
||||
# 获取数据
|
||||
try:
|
||||
week_data = self.manager.get_total_readtime(days=7) # 索引0=今天
|
||||
month_data = self.manager.get_total_readtime(days=30)
|
||||
year_data = self.manager.get_total_readtime12m() # 12个月
|
||||
year_total_minutes = self.manager.get_total_readtime_year()
|
||||
except Exception as e:
|
||||
print(f"警告: 统计数据获取失败: {e}")
|
||||
return
|
||||
|
||||
# 处理无数据情况
|
||||
if all(v == 0 for v in week_data + month_data + year_data):
|
||||
for frame_name, layout_name in required_frames:
|
||||
lbl = QLabel("暂无阅读数据")
|
||||
getattr(self, layout_name).addWidget(lbl)
|
||||
return
|
||||
|
||||
# 工具函数:添加图到 frame
|
||||
def add_figure(frame_layout, fig):
|
||||
canvas = FigureCanvas(fig)
|
||||
frame_layout.addWidget(canvas)
|
||||
return canvas
|
||||
|
||||
# 周图(最近7天) - 倒序显示使左侧为7天前? 按需求索引0=今天 -> 我们希望x轴从右到左还是左到右? 采用左=今天的一致性
|
||||
def plot_bar(data, title, xlabel_list):
|
||||
fig = Figure(figsize=(3.2, 2.4), tight_layout=True)
|
||||
ax = fig.add_subplot(111)
|
||||
bars = ax.bar(range(len(data)), data, color="#4c72b0")
|
||||
ax.set_title(title, fontsize=10)
|
||||
ax.set_xticks(range(len(data)))
|
||||
ax.set_xticklabels(xlabel_list, rotation=0, fontsize=8)
|
||||
ax.set_ylabel("分钟", fontsize=8)
|
||||
# 在柱子顶部加简单数值(若非0)
|
||||
for rect, val in zip(bars, data):
|
||||
if val > 0:
|
||||
ax.text(rect.get_x() + rect.get_width()/2, rect.get_height(), str(val), ha='center', va='bottom', fontsize=7)
|
||||
return fig
|
||||
|
||||
# x 轴标签
|
||||
week_labels = ["今", "昨", "2", "3", "4", "5", "6"] # 索引0=今天
|
||||
month_labels = [str(i) for i in range(30)] # 0..29 天前
|
||||
year_labels = [f"{i+1}月" for i in range(12)]
|
||||
|
||||
# 绘制三个柱状图
|
||||
week_fig = plot_bar(week_data, "", week_labels)
|
||||
month_fig = plot_bar(month_data, "", month_labels)
|
||||
# 年数据转为小时用于展示
|
||||
year_hours_data = [round(m / 60.0, 1) for m in year_data]
|
||||
def plot_bar_hours(data, title, xlabel_list):
|
||||
fig = Figure(figsize=(3.2, 2.4), tight_layout=True)
|
||||
ax = fig.add_subplot(111)
|
||||
bars = ax.bar(range(len(data)), data, color="#8c6bb1")
|
||||
ax.set_title(title, fontsize=10)
|
||||
ax.set_xticks(range(len(data)))
|
||||
ax.set_xticklabels(xlabel_list, rotation=0, fontsize=8)
|
||||
ax.set_ylabel("小时", fontsize=8)
|
||||
for rect, val in zip(bars, data):
|
||||
if val > 0:
|
||||
ax.text(rect.get_x() + rect.get_width()/2, rect.get_height(), str(val), ha='center', va='bottom', fontsize=7)
|
||||
return fig
|
||||
year_fig = plot_bar_hours(year_hours_data, "", year_labels)
|
||||
|
||||
add_figure(self.weekLayout, week_fig)
|
||||
add_figure(self.monthLayout, month_fig)
|
||||
add_figure(self.yearLayout, year_fig)
|
||||
|
||||
# 气泡图数据计算
|
||||
import math
|
||||
# 全年阅读小时数
|
||||
year_hours = year_total_minutes / 60.0
|
||||
# 月均阅读小时数
|
||||
month_avg_hours = (sum(year_data) / 12.0) / 60.0 if year_data else 0
|
||||
# 近7天阅读小时数
|
||||
week_hours = sum(week_data) / 60.0
|
||||
# 日均阅读分钟数(最近30天)
|
||||
day_avg_minutes = (sum(month_data) / 30.0) if month_data else 0
|
||||
|
||||
bubble_metrics = [
|
||||
("全年", year_hours, 'h', '#5b6ee1'),
|
||||
("月均", month_avg_hours, 'h', '#c9b2d9'),
|
||||
("近7天", week_hours, 'h', '#f4b2c2'),
|
||||
("日均", day_avg_minutes, 'm', '#b9b542'),
|
||||
]
|
||||
# 归一化确定半径(防止过大/过小)。将值全部转为分钟再归一化。
|
||||
minute_values = []
|
||||
for label, val, unit, color in bubble_metrics:
|
||||
if unit == 'h':
|
||||
minute_values.append(val * 60)
|
||||
else:
|
||||
minute_values.append(val)
|
||||
max_minutes = max(minute_values) if minute_values else 1
|
||||
radii = []
|
||||
for mv in minute_values:
|
||||
# 半径在 [0.3, 1.0] 之间的平方放大到 marker size
|
||||
norm = mv / max_minutes if max_minutes > 0 else 0
|
||||
radii.append(0.3 + 0.7 * math.sqrt(norm))
|
||||
|
||||
fig_b = Figure(figsize=(3.6, 2.6), tight_layout=True)
|
||||
axb = fig_b.add_subplot(111)
|
||||
#axb.set_title("阅读指标气泡")
|
||||
axb.axis('off')
|
||||
# 采用归一化坐标使气泡左右均匀填充 (x 0~1)
|
||||
# 布局:最大在 0.2,另外两个上方/右方,一个在下方,形成视觉平衡
|
||||
label2pos = {
|
||||
'全年': (0.20, 0.00),
|
||||
'月均': (0.55, 0.52),
|
||||
'近7天': (0.85, 0.05),
|
||||
'日均': (0.55, -0.52)
|
||||
}
|
||||
# 若有新增指标则线性平铺
|
||||
if any(l not in label2pos for l, *_ in bubble_metrics):
|
||||
step = 1.0 / max(1, len(bubble_metrics)-1)
|
||||
label2pos = {m[0]: (i*step, 0.0) for i, m in enumerate(bubble_metrics)}
|
||||
|
||||
for (label, val, unit, color), r in zip(bubble_metrics, radii):
|
||||
x, y = label2pos.get(label, (0.5, 0.0))
|
||||
size = (r * 1150) ** 2 * 0.012
|
||||
axb.scatter(x, y, s=size, color=color, alpha=0.70, edgecolors='white', linewidths=1.0)
|
||||
if unit == 'h':
|
||||
text_val = f"{val:.0f} 小时" if val >= 10 else f"{val:.1f} 小时"
|
||||
else:
|
||||
text_val = f"{val:.0f} 分钟"
|
||||
axb.text(x, y, f"{text_val}\n{label}", ha='center', va='center', fontsize=11, color='white', weight='bold')
|
||||
|
||||
axb.set_xlim(-0.02, 1.02)
|
||||
axb.set_ylim(-0.95, 0.95)
|
||||
axb.set_aspect('auto')
|
||||
add_figure(self.bubbleLayout, fig_b)
|
||||
|
||||
# ------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 设置应用程序名称和组织信息
|
||||
app.setApplicationName("notesExporter")
|
||||
app.setApplicationDisplayName("notesExporter")
|
||||
app.setOrganizationName("iBook Tools")
|
||||
|
||||
# 设置应用程序图标
|
||||
if os.path.exists(config.APP_ICON):
|
||||
app.setWindowIcon(QIcon(config.APP_ICON))
|
||||
|
||||
win = IBookExportApp()
|
||||
try:
|
||||
win._init_charts()
|
||||
except Exception:
|
||||
pass
|
||||
# 启动即全屏
|
||||
win.showFullScreen()
|
||||
sys.exit(app.exec())
|
|
@ -0,0 +1,174 @@
|
|||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=24%E5%A0%82%E8%B4%A2%E5%AF%8C%E8%AF%BE&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=24%E5%A0%82%E8%B4%A2%E5%AF%8C%E8%AF%BE&cat=1001&k=bookname_xxx HTTP/1.1" 301 113
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=24%E5%A0%82%E8%B4%A2%E5%AF%8C%E8%AF%BE HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E7%94%B2%E9%AA%A8%E6%96%87&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E7%94%B2%E9%AA%A8%E6%96%87&cat=1001&k=bookname_xxx HTTP/1.1" 301 102
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E7%94%B2%E9%AA%A8%E6%96%87 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E5%BA%86%E4%BD%99%E5%B9%B4%28%E7%B2%BE%E6%A0%A1%E7%89%88%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E5%BA%86%E4%BD%99%E5%B9%B4%28%E7%B2%BE%E6%A0%A1%E7%89%88%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 141
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E5%BA%86%E4%BD%99%E5%B9%B4%28%E7%B2%BE%E6%A0%A1%E7%89%88%EF%BC%89 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E5%95%86%E5%90%9B%E4%B9%A6-%E4%B8%AD%E5%8D%8E%E7%BB%8F%E5%85%B8%E5%90%8D%E8%91%97%E5%85%A8%E6%9C%AC%E5%85%A8%E6%B3%A8%E5%85%A8%E8%AF%91%E4%B8%9B%E4%B9%A6&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E5%95%86%E5%90%9B%E4%B9%A6-%E4%B8%AD%E5%8D%8E%E7%BB%8F%E5%85%B8%E5%90%8D%E8%91%97%E5%85%A8%E6%9C%AC%E5%85%A8%E6%B3%A8%E5%85%A8%E8%AF%91%E4%B8%9B%E4%B9%A6&cat=1001&k=bookname_xxx HTTP/1.1" 301 229
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E5%95%86%E5%90%9B%E4%B9%A6-%E4%B8%AD%E5%8D%8E%E7%BB%8F%E5%85%B8%E5%90%8D%E8%91%97%E5%85%A8%E6%9C%AC%E5%85%A8%E6%B3%A8%E5%85%A8%E8%AF%91%E4%B8%9B%E4%B9%A6 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E8%8B%8F%E4%B8%96%E6%B0%91%3A%E6%88%91%E7%9A%84%E7%BB%8F%E9%AA%8C%E4%B8%8E%E6%95%99%E8%AE%AD%EF%BC%882018%E8%AF%BB%E6%A1%A5%E6%B0%B4%E8%BE%BE%E5%88%A9%E6%AC%A7%E7%9A%84%E5%8E%9F%E5%88%99%EF%BC%8C2020%E7%9C%8B%E9%BB%91%E7%9F%B3%E8%8B%8F%E4%B8%96%E6%B0%91%E7%9A%84%E7%BB%8F%E9%AA%8C%21%E4%B8%80%E6%9C%AC%E4%B9%A6%E8%AF%BB%E6%87%82%E4%BB%8E%E7%99%BD%E6%89%8B%E8%B5%B7%E5%AE%B6%E5%88%B0%E5%8D%8E%E5%B0%94%E8%A1%97%E6%96%B0%E5%9B%BD%E7%8E%8B%E7%9A%84%E4%BC%A0%E5%A5%87%E4%BA%BA%E7%94%9F%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E8%8B%8F%E4%B8%96%E6%B0%91%3A%E6%88%91%E7%9A%84%E7%BB%8F%E9%AA%8C%E4%B8%8E%E6%95%99%E8%AE%AD%EF%BC%882018%E8%AF%BB%E6%A1%A5%E6%B0%B4%E8%BE%BE%E5%88%A9%E6%AC%A7%E7%9A%84%E5%8E%9F%E5%88%99%EF%BC%8C2020%E7%9C%8B%E9%BB%91%E7%9F%B3%E8%8B%8F%E4%B8%96%E6%B0%91%E7%9A%84%E7%BB%8F%E9%AA%8C%21%E4%B8%80%E6%9C%AC%E4%B9%A6%E8%AF%BB%E6%87%82%E4%BB%8E%E7%99%BD%E6%89%8B%E8%B5%B7%E5%AE%B6%E5%88%B0%E5%8D%8E%E5%B0%94%E8%A1%97%E6%96%B0%E5%9B%BD%E7%8E%8B%E7%9A%84%E4%BC%A0%E5%A5%87%E4%BA%BA%E7%94%9F%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 566
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E8%8B%8F%E4%B8%96%E6%B0%91%3A%E6%88%91%E7%9A%84%E7%BB%8F%E9%AA%8C%E4%B8%8E%E6%95%99%E8%AE%AD%EF%BC%882018%E8%AF%BB%E6%A1%A5%E6%B0%B4%E8%BE%BE%E5%88%A9%E6%AC%A7%E7%9A%84%E5%8E%9F%E5%88%99%EF%BC%8C2020%E7%9C%8B%E9%BB%91%E7%9F%B3%E8%8B%8F%E4%B8%96%E6%B0%91%E7%9A%84%E7%BB%8F%E9%AA%8C%21%E4%B8%80%E6%9C%AC%E4%B9%A6%E8%AF%BB%E6%87%82%E4%BB%8E%E7%99%BD%E6%89%8B%E8%B5%B7%E5%AE%B6%E5%88%B0%E5%8D%8E%E5%B0%94%E8%A1%97%E6%96%B0%E5%9B%BD%E7%8E%8B%E7%9A%84%E4%BC%A0%E5%A5%87%E4%BA%BA%E7%94%9F%EF%BC%89 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E6%9D%A8%E4%BC%AF%E5%B3%BB_%E8%AE%BA%E8%AF%AD%E8%AF%91%E6%B3%A8&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E6%9D%A8%E4%BC%AF%E5%B3%BB_%E8%AE%BA%E8%AF%AD%E8%AF%91%E6%B3%A8&cat=1001&k=bookname_xxx HTTP/1.1" 301 139
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E6%9D%A8%E4%BC%AF%E5%B3%BB_%E8%AE%BA%E8%AF%AD%E8%AF%91%E6%B3%A8 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E5%B0%8F%E7%AA%97%E5%B9%BD%E8%AE%B0&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E5%B0%8F%E7%AA%97%E5%B9%BD%E8%AE%B0&cat=1001&k=bookname_xxx HTTP/1.1" 301 111
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E5%B0%8F%E7%AA%97%E5%B9%BD%E8%AE%B0 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E5%B0%91%E5%B9%B4%E5%87%AF%E6%AD%8C&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E5%B0%91%E5%B9%B4%E5%87%AF%E6%AD%8C&cat=1001&k=bookname_xxx HTTP/1.1" 301 111
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E5%B0%91%E5%B9%B4%E5%87%AF%E6%AD%8C HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E6%8A%95%E8%B5%84%E8%A6%81%E4%B9%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E6%8A%95%E8%B5%84%E8%A6%81%E4%B9%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 111
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E6%8A%95%E8%B5%84%E8%A6%81%E4%B9%89 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E7%99%BD%E9%B1%BC%E8%A7%A3%E5%AD%97&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E7%99%BD%E9%B1%BC%E8%A7%A3%E5%AD%97&cat=1001&k=bookname_xxx HTTP/1.1" 301 111
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E7%99%BD%E9%B1%BC%E8%A7%A3%E5%AD%97 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E5%8E%86%E5%8F%B2%E7%9A%84%E5%B7%A8%E9%95%9C&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E5%8E%86%E5%8F%B2%E7%9A%84%E5%B7%A8%E9%95%9C&cat=1001&k=bookname_xxx HTTP/1.1" 301 120
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E5%8E%86%E5%8F%B2%E7%9A%84%E5%B7%A8%E9%95%9C HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E8%B4%A7%E5%B8%81%E7%9A%84%E6%95%99%E8%AE%AD&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E8%B4%A7%E5%B8%81%E7%9A%84%E6%95%99%E8%AE%AD&cat=1001&k=bookname_xxx HTTP/1.1" 301 120
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E8%B4%A7%E5%B8%81%E7%9A%84%E6%95%99%E8%AE%AD HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E9%92%B1%E4%BB%8E%E5%93%AA%E9%87%8C%E6%9D%A5&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E9%92%B1%E4%BB%8E%E5%93%AA%E9%87%8C%E6%9D%A5&cat=1001&k=bookname_xxx HTTP/1.1" 301 120
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E9%92%B1%E4%BB%8E%E5%93%AA%E9%87%8C%E6%9D%A5 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E4%B8%AD%E5%9B%BD%E5%8F%A4%E4%BB%A3%E7%AE%80%E5%8F%B2&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E4%B8%AD%E5%9B%BD%E5%8F%A4%E4%BB%A3%E7%AE%80%E5%8F%B2&cat=1001&k=bookname_xxx HTTP/1.1" 301 129
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E4%B8%AD%E5%9B%BD%E5%8F%A4%E4%BB%A3%E7%AE%80%E5%8F%B2 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E7%BD%97%E9%A9%AC%E4%BA%BA%E7%9A%84%E6%95%85%E4%BA%8B%28%E5%A5%97%E8%A3%85%E5%85%B115%E5%86%8C%29&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E7%BD%97%E9%A9%AC%E4%BA%BA%E7%9A%84%E6%95%85%E4%BA%8B%28%E5%A5%97%E8%A3%85%E5%85%B115%E5%86%8C%29&cat=1001&k=bookname_xxx HTTP/1.1" 301 173
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E7%BD%97%E9%A9%AC%E4%BA%BA%E7%9A%84%E6%95%85%E4%BA%8B%28%E5%A5%97%E8%A3%85%E5%85%B115%E5%86%8C%29 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E6%94%B9%E5%8F%98%E5%BF%83%E7%90%86%E5%AD%A6%E7%9A%8440%E9%A1%B9%E7%A0%94%E7%A9%B6&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E6%94%B9%E5%8F%98%E5%BF%83%E7%90%86%E5%AD%A6%E7%9A%8440%E9%A1%B9%E7%A0%94%E7%A9%B6&cat=1001&k=bookname_xxx HTTP/1.1" 301 158
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E6%94%B9%E5%8F%98%E5%BF%83%E7%90%86%E5%AD%A6%E7%9A%8440%E9%A1%B9%E7%A0%94%E7%A9%B6 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E5%A6%82%E4%BD%95%E5%81%87%E8%A3%85%E6%87%82%E9%9F%B3%E4%B9%90&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E5%A6%82%E4%BD%95%E5%81%87%E8%A3%85%E6%87%82%E9%9F%B3%E4%B9%90&cat=1001&k=bookname_xxx HTTP/1.1" 301 138
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E5%A6%82%E4%BD%95%E5%81%87%E8%A3%85%E6%87%82%E9%9F%B3%E4%B9%90 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E7%AE%A1%E5%AD%90%EF%BC%88%E4%B8%8A%E4%B8%8B%E5%86%8C%EF%BC%89--%E4%B8%AD%E5%8D%8E%E7%BB%8F%E5%85%B8%E5%90%8D%E8%91%97%E5%85%A8%E6%9C%AC%E5%85%A8%E6%B3%A8%E5%85%A8%E8%AF%91%EF%BC%88%E7%B2%BE%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E7%AE%A1%E5%AD%90%EF%BC%88%E4%B8%8A%E4%B8%8B%E5%86%8C%EF%BC%89--%E4%B8%AD%E5%8D%8E%E7%BB%8F%E5%85%B8%E5%90%8D%E8%91%97%E5%85%A8%E6%9C%AC%E5%85%A8%E6%B3%A8%E5%85%A8%E8%AF%91%EF%BC%88%E7%B2%BE%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 275
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E7%AE%A1%E5%AD%90%EF%BC%88%E4%B8%8A%E4%B8%8B%E5%86%8C%EF%BC%89--%E4%B8%AD%E5%8D%8E%E7%BB%8F%E5%85%B8%E5%90%8D%E8%91%97%E5%85%A8%E6%9C%AC%E5%85%A8%E6%B3%A8%E5%85%A8%E8%AF%91%EF%BC%88%E7%B2%BE%EF%BC%89 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E6%8A%95%E8%B5%84%E4%B8%AD%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84%E4%BA%8B&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E6%8A%95%E8%B5%84%E4%B8%AD%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84%E4%BA%8B&cat=1001&k=bookname_xxx HTTP/1.1" 301 147
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E6%8A%95%E8%B5%84%E4%B8%AD%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84%E4%BA%8B HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E8%96%9B%E5%85%86%E4%B8%B0%E7%BB%8F%E6%B5%8E%E5%AD%A6%E8%AE%B2%E4%B9%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E8%96%9B%E5%85%86%E4%B8%B0%E7%BB%8F%E6%B5%8E%E5%AD%A6%E8%AE%B2%E4%B9%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 147
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E8%96%9B%E5%85%86%E4%B8%B0%E7%BB%8F%E6%B5%8E%E5%AD%A6%E8%AE%B2%E4%B9%89 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E6%9E%AA%E7%82%AE%E3%80%81%E7%97%85%E8%8F%8C%E4%B8%8E%E9%92%A2%E9%93%81%3A%E4%BA%BA%E7%B1%BB%E7%A4%BE%E4%BC%9A%E7%9A%84%E5%91%BD%E8%BF%90%28%E4%B8%96%E7%BA%AA%E4%BA%BA%E6%96%87%E7%B3%BB%E5%88%97%E4%B8%9B%E4%B9%A6%C2%B7%E5%BC%80%E6%94%BE%E4%BA%BA%E6%96%87%29&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E6%9E%AA%E7%82%AE%E3%80%81%E7%97%85%E8%8F%8C%E4%B8%8E%E9%92%A2%E9%93%81%3A%E4%BA%BA%E7%B1%BB%E7%A4%BE%E4%BC%9A%E7%9A%84%E5%91%BD%E8%BF%90%28%E4%B8%96%E7%BA%AA%E4%BA%BA%E6%96%87%E7%B3%BB%E5%88%97%E4%B8%9B%E4%B9%A6%C2%B7%E5%BC%80%E6%94%BE%E4%BA%BA%E6%96%87%29&cat=1001&k=bookname_xxx HTTP/1.1" 301 333
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E6%9E%AA%E7%82%AE%E3%80%81%E7%97%85%E8%8F%8C%E4%B8%8E%E9%92%A2%E9%93%81%3A%E4%BA%BA%E7%B1%BB%E7%A4%BE%E4%BC%9A%E7%9A%84%E5%91%BD%E8%BF%90%28%E4%B8%96%E7%BA%AA%E4%BA%BA%E6%96%87%E7%B3%BB%E5%88%97%E4%B8%9B%E4%B9%A6%C2%B7%E5%BC%80%E6%94%BE%E4%BA%BA%E6%96%87%29 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E4%B8%AD%E5%A4%AE%E5%B8%9D%E5%9B%BD%E7%9A%84%E5%93%B2%E5%AD%A6%E5%AF%86%E7%A0%81&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E4%B8%AD%E5%A4%AE%E5%B8%9D%E5%9B%BD%E7%9A%84%E5%93%B2%E5%AD%A6%E5%AF%86%E7%A0%81&cat=1001&k=bookname_xxx HTTP/1.1" 301 156
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E4%B8%AD%E5%A4%AE%E5%B8%9D%E5%9B%BD%E7%9A%84%E5%93%B2%E5%AD%A6%E5%AF%86%E7%A0%81 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E6%96%B0%E7%BC%96%E8%AF%B4%E6%96%87%E8%A7%A3%E5%AD%97%E5%A4%A7%E5%85%A8%E9%9B%86%28%E8%B6%85%E5%80%BC%E7%99%BD%E9%87%91%E7%89%88%29&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E6%96%B0%E7%BC%96%E8%AF%B4%E6%96%87%E8%A7%A3%E5%AD%97%E5%A4%A7%E5%85%A8%E9%9B%86%28%E8%B6%85%E5%80%BC%E7%99%BD%E9%87%91%E7%89%88%29&cat=1001&k=bookname_xxx HTTP/1.1" 301 207
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E6%96%B0%E7%BC%96%E8%AF%B4%E6%96%87%E8%A7%A3%E5%AD%97%E5%A4%A7%E5%85%A8%E9%9B%86%28%E8%B6%85%E5%80%BC%E7%99%BD%E9%87%91%E7%89%88%29 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E5%B8%82%E5%9C%BA%E7%9A%84%E9%80%BB%E8%BE%91%EF%BC%88%E5%A2%9E%E8%AE%A2%E6%9C%AC%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E5%B8%82%E5%9C%BA%E7%9A%84%E9%80%BB%E8%BE%91%EF%BC%88%E5%A2%9E%E8%AE%A2%E6%9C%AC%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 165
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E5%B8%82%E5%9C%BA%E7%9A%84%E9%80%BB%E8%BE%91%EF%BC%88%E5%A2%9E%E8%AE%A2%E6%9C%AC%EF%BC%89 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E9%87%91%E8%9E%8D%E7%9A%84%E6%9C%AC%E8%B4%A8%EF%BC%9A%E4%BC%AF%E5%8D%97%E5%85%8B%E5%9B%9B%E8%AE%B2%E7%BE%8E%E8%81%94%E5%82%A8%28%E7%9C%8B%E4%B8%80%E4%B8%AA%E9%A3%8E%E4%BA%91%E4%BA%BA%E7%89%A9%E7%9A%84%E9%87%91%E8%9E%8D%E6%80%9D%E8%80%83%29&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E9%87%91%E8%9E%8D%E7%9A%84%E6%9C%AC%E8%B4%A8%EF%BC%9A%E4%BC%AF%E5%8D%97%E5%85%8B%E5%9B%9B%E8%AE%B2%E7%BE%8E%E8%81%94%E5%82%A8%28%E7%9C%8B%E4%B8%80%E4%B8%AA%E9%A3%8E%E4%BA%91%E4%BA%BA%E7%89%A9%E7%9A%84%E9%87%91%E8%9E%8D%E6%80%9D%E8%80%83%29&cat=1001&k=bookname_xxx HTTP/1.1" 301 315
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E9%87%91%E8%9E%8D%E7%9A%84%E6%9C%AC%E8%B4%A8%EF%BC%9A%E4%BC%AF%E5%8D%97%E5%85%8B%E5%9B%9B%E8%AE%B2%E7%BE%8E%E8%81%94%E5%82%A8%28%E7%9C%8B%E4%B8%80%E4%B8%AA%E9%A3%8E%E4%BA%91%E4%BA%BA%E7%89%A9%E7%9A%84%E9%87%91%E8%9E%8D%E6%80%9D%E8%80%83%29 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%AD%A6%E5%86%99%E4%BD%9C%EF%BC%9A%E4%B8%AA%E4%BA%BA%E5%A2%9E%E5%80%BC%E7%9A%84%E6%9C%89%E6%95%88%E6%96%B9%E6%B3%95&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%AD%A6%E5%86%99%E4%BD%9C%EF%BC%9A%E4%B8%AA%E4%BA%BA%E5%A2%9E%E5%80%BC%E7%9A%84%E6%9C%89%E6%95%88%E6%96%B9%E6%B3%95&cat=1001&k=bookname_xxx HTTP/1.1" 301 228
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%AD%A6%E5%86%99%E4%BD%9C%EF%BC%9A%E4%B8%AA%E4%BA%BA%E5%A2%9E%E5%80%BC%E7%9A%84%E6%9C%89%E6%95%88%E6%96%B9%E6%B3%95 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E4%B8%AD%E5%9B%BD%E5%9B%BD%E5%AE%B6%E6%B2%BB%E7%90%86%E7%9A%84%E5%88%B6%E5%BA%A6%E9%80%BB%E8%BE%91%EF%BC%9A%E4%B8%80%E4%B8%AA%E7%BB%84%E7%BB%87%E5%AD%A6%E7%A0%94%E7%A9%B6&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E4%B8%AD%E5%9B%BD%E5%9B%BD%E5%AE%B6%E6%B2%BB%E7%90%86%E7%9A%84%E5%88%B6%E5%BA%A6%E9%80%BB%E8%BE%91%EF%BC%9A%E4%B8%80%E4%B8%AA%E7%BB%84%E7%BB%87%E5%AD%A6%E7%A0%94%E7%A9%B6&cat=1001&k=bookname_xxx HTTP/1.1" 301 246
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E4%B8%AD%E5%9B%BD%E5%9B%BD%E5%AE%B6%E6%B2%BB%E7%90%86%E7%9A%84%E5%88%B6%E5%BA%A6%E9%80%BB%E8%BE%91%EF%BC%9A%E4%B8%80%E4%B8%AA%E7%BB%84%E7%BB%87%E5%AD%A6%E7%A0%94%E7%A9%B6 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E4%B8%AD%E5%9B%BD%E4%B8%BA%E4%BB%80%E4%B9%88%E6%9C%89%E5%89%8D%E9%80%94%EF%BC%9A%E5%AF%B9%E5%A4%96%E7%BB%8F%E6%B5%8E%E5%85%B3%E7%B3%BB%E7%9A%84%E6%88%98%E7%95%A5%E6%BD%9C%E8%83%BD%EF%BC%88%E7%AC%AC3%E7%89%88%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E4%B8%AD%E5%9B%BD%E4%B8%BA%E4%BB%80%E4%B9%88%E6%9C%89%E5%89%8D%E9%80%94%EF%BC%9A%E5%AF%B9%E5%A4%96%E7%BB%8F%E6%B5%8E%E5%85%B3%E7%B3%BB%E7%9A%84%E6%88%98%E7%95%A5%E6%BD%9C%E8%83%BD%EF%BC%88%E7%AC%AC3%E7%89%88%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 292
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E4%B8%AD%E5%9B%BD%E4%B8%BA%E4%BB%80%E4%B9%88%E6%9C%89%E5%89%8D%E9%80%94%EF%BC%9A%E5%AF%B9%E5%A4%96%E7%BB%8F%E6%B5%8E%E5%85%B3%E7%B3%BB%E7%9A%84%E6%88%98%E7%95%A5%E6%BD%9C%E8%83%BD%EF%BC%88%E7%AC%AC3%E7%89%88%EF%BC%89 HTTP/1.1" 200 None
|
||||
Starting new HTTPS connection (1): search.douban.com:443
|
||||
https://search.douban.com:443 "GET /book/subject_search?Host=www.douban.com&search_text=%E6%97%A5%E6%9C%AC%E7%9A%84%E4%B8%96%E7%95%8C%E8%A7%82%EF%BC%88%E3%80%8A%E5%89%91%E6%A1%A5%E6%97%A5%E6%9C%AC%E5%8F%B2%E3%80%8B%E4%B8%BB%E7%BC%96%E5%87%9D%E7%BB%83%E4%B9%8B%E4%BD%9C%E4%B8%89%E4%B8%AA%E4%BA%BA%E7%89%A9%E6%95%85%E4%BA%8B%E4%B8%B2%E8%B5%B7%E6%97%A5%E6%9C%AC%E4%B8%A4%E7%99%BE%E5%B9%B4%E5%8F%98%E5%B1%80%E4%BA%86%E8%A7%A3%E8%BF%91%E4%BB%A3%E6%97%A5%E6%9C%AC%E8%BD%AC%E5%90%91%E7%9A%84%E5%BF%85%E8%AF%BB%E4%B9%8B%E4%B9%A6%E7%90%86%E6%83%B3%E5%9B%BD%E5%87%BA%E5%93%81%EF%BC%89%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 None
|
||||
Starting new HTTPS connection (1): book.douban.com:443
|
||||
https://book.douban.com:443 "GET /subject_search?Host=www.douban.com&search_text=%E6%97%A5%E6%9C%AC%E7%9A%84%E4%B8%96%E7%95%8C%E8%A7%82%EF%BC%88%E3%80%8A%E5%89%91%E6%A1%A5%E6%97%A5%E6%9C%AC%E5%8F%B2%E3%80%8B%E4%B8%BB%E7%BC%96%E5%87%9D%E7%BB%83%E4%B9%8B%E4%BD%9C%E4%B8%89%E4%B8%AA%E4%BA%BA%E7%89%A9%E6%95%85%E4%BA%8B%E4%B8%B2%E8%B5%B7%E6%97%A5%E6%9C%AC%E4%B8%A4%E7%99%BE%E5%B9%B4%E5%8F%98%E5%B1%80%E4%BA%86%E8%A7%A3%E8%BF%91%E4%BB%A3%E6%97%A5%E6%9C%AC%E8%BD%AC%E5%90%91%E7%9A%84%E5%BF%85%E8%AF%BB%E4%B9%8B%E4%B9%A6%E7%90%86%E6%83%B3%E5%9B%BD%E5%87%BA%E5%93%81%EF%BC%89%EF%BC%89&cat=1001&k=bookname_xxx HTTP/1.1" 301 570
|
||||
Starting new HTTPS connection (1): www.douban.com:443
|
||||
https://www.douban.com:443 "GET /search?q=%E6%97%A5%E6%9C%AC%E7%9A%84%E4%B8%96%E7%95%8C%E8%A7%82%EF%BC%88%E3%80%8A%E5%89%91%E6%A1%A5%E6%97%A5%E6%9C%AC%E5%8F%B2%E3%80%8B%E4%B8%BB%E7%BC%96%E5%87%9D%E7%BB%83%E4%B9%8B%E4%BD%9C%E4%B8%89%E4%B8%AA%E4%BA%BA%E7%89%A9%E6%95%85%E4%BA%8B%E4%B8%B2%E8%B5%B7%E6%97%A5%E6%9C%AC%E4%B8%A4%E7%99%BE%E5%B9%B4%E5%8F%98%E5%B1%80%E4%BA%86%E8%A7%A3%E8%BF%91%E4%BB%A3%E6%97%A5%E6%9C%AC%E8%BD%AC%E5%90%91%E7%9A%84%E5%BF%85%E8%AF%BB%E4%B9%8B%E4%B9%A6%E7%90%86%E6%83%B3%E5%9B%BD%E5%87%BA%E5%93%81%EF%BC%89%EF%BC%89 HTTP/1.1" 200 None
|
|
@ -0,0 +1,658 @@
|
|||
# 笔记导出 2025-09-06 17:15
|
||||
|
||||
|
||||
## 众生无束:劳动社会的未来
|
||||
|
||||
### 译者序
|
||||
大多数人对自身和未来的评价和感受:既渴望“躺平”,又止不住“内卷”,对未来的就业环境充满焦虑和迷茫
|
||||
|
||||
引入“无条件基本收入制度”,以保障人们在意义社会中生存的物质基础,同时改革教育体系,以应对人们在面对意义社会时可能会产生的无措与迷茫。
|
||||
|
||||
意义社会”的核心在于切断那条紧紧捆绑收入与工作的坚固纽带,使得人们无论选择工作与否,无论选择从事何种工作,都不必再受到外部物质条件的制约,而只需要考虑这个选择是否符合自己内心对于意义的追求。
|
||||
|
||||
### (未找到章节)
|
||||
如果机器所有者反对财富再分配,那么大多数人将陷入赤贫。到目前为止,趋势似乎是朝着后一种情况发展,科技进步加剧了社会的不平等。
|
||||
|
||||
假使每件工具都能按照他人的意志自动完成工作,如同代达罗斯(Daedalus)的雕像或者赫斐斯托斯(Hephaestus)的三足宝座,它们自动参与众神的集会,如诗人所说;若不假人手,机杼能织布,锦瑟可自鸣,工匠就不再需要帮手,主人也不再需要奴隶了。
|
||||
——亚里士多德,《政治学》
|
||||
|
||||
### 引言
|
||||
简而言之,枯燥乏味的工作取代了艰辛劳累的工作。
|
||||
|
||||
对年轻一代而言,工作已不再是人生使命或人生意义所在。如今,最重要的财富不再是工作,而是人们可以自由支配时间,并根据自己的意愿来规划生活。
|
||||
|
||||
第二个机器时代的全自动化生产和人工智能向人们揭示了一个事实,这是第一个机器时代从未承认过的事实:资源已经充足到足以满足所有人的需求!丰裕社会睁开了惺忪双眼,意识到物质丰裕的时代已经来临。
|
||||
|
||||
### 经济陷入困境:转变思维的必要性
|
||||
在经济全球化的背景之下,资本几乎可以不受国界的限制而自由流动。而且,自动化程度越高,资本对地域的依赖度就越低。因为数据处理和人工智能程序的运作方式在任何一个国家都完全相同,无关乎这个国家的经济状况。这一趋势严重威胁到那些在全球化第一阶段凭借低廉劳动力而获利的贫穷国家和新兴工业化国家。全球化的第二阶段正在迅速吞噬这些国家的原有优势,并将一些工作流程重新带回到工业化国家,涉及布匹生产到会计核算等各个环节,值得注意的是,该进程并没有创造大量的就业机会。而真正的受益者是那些在全球范围内大额投资金融市场或科技公司的人。这些投资者通常在短短几毫秒内频繁转移资金进行投资,他们不断寻求着最大限度的收益。财富差距因此加速扩大,以至于全球前10%的富人拥有全球约85%的资产。最富有的1%拥有超过45%的财富,几乎占据了世界财富的一半。
|
||||
无论是在曼谷、班加罗尔、布加勒斯特还是柏林,输家都是那些不得不继续出卖自己的劳动力来维持生计的人。
|
||||
|
||||
可是实际上,反对和仇视技术发展可能是最不可能发生的情况。更有可能出现的场景是:大部分中产阶级沦为社会边缘人群,因此在政治上变得更加激进,最终引起右翼民粹主义的泛滥。
|
||||
|
||||
第四次工业革命带来的巨大危机并不是反对技术进步的大规模起义,而是社会两极分化、社会激进化和文化斗争,这种斗争并不是围绕“技术进步”展开的,而是围绕其他议题展开的:预防保健措施、移民政策、性别议题、社会多样性和正字法。
|
||||
|
||||
根据莱姆的原意,“技术陷阱”指的是技术进步的矛盾性,技术进步不仅创造美好,而且总是摧毁值得保存的旧物
|
||||
|
||||
人们要么通过征税将这个巨大的社会隐患简化成一个纯粹的财务问题,要么用魔术——向大众承诺经济会持续增长、岗位会无限增加——消除这个危机。
|
||||
|
||||
在第二个机器时代,工作自动化加剧了财富分配的不平等
|
||||
|
||||
事实上,受到抨击的并不是机器人和人工智能系统,而是移民群体和少数族裔、一切被视为“左派”的事物、执政党“建制派”和媒体“建制派”,以及气候政策。那些在社会上失利的人通常会寻找外表或社会背景与他们不同的人作为出气筒。外来人口和有远见的知识分子都会成为他们的攻击目标
|
||||
|
||||
由此可见,第一个问题(财富差异加剧)和第二个问题(低技能劳动力被淘汰或被降职降薪)是相互关联的。为了解决这两个问题,众多国家面临着相同的处境:国家的社会保障制度必须在经济层面上应对工作世界的变革所带来的挑战。但是应该如何应对呢?除了零利率政策引起的一些小幅度的波动,近几十年来,在大多数工业化国家,特别是在美国,政府机构越来越穷,而富人的财富却大幅增长。如果这种趋势继续发展下去——按照我们目前的工作和经济组织方式,这种趋势大概率会继续保持下去——那么对于大多数工业化国家,包括德国在内,其社会保障制度的崩溃将是不可避免的。
|
||||
|
||||
### 四个赢家和一个输家:未来的劳动力市场
|
||||
用人需求不仅稳定,未来的需求还将大幅增加的工作主要集中这四个领域:尖端信息技术领域、以高附加值服务为主导的第四产业、手工业,以及用人需求最大的领域——所谓的“同理心职业
|
||||
|
||||
如果自己的子女成为面包师、瓷砖工或水管工,那么大多数的德国家长可能不会将其视为职业生涯的成功,尽管许多手工业者的经济状况胜过多数行政人员、保险员、城市管理员、人文学者、演员、诗人或艺术家。然而,这种偏见可能会在第二个机器时代发生变化
|
||||
|
||||
机器人接手教育类工作,很可能是一个毫无未来的荒唐想法,至少在西方国家是这样
|
||||
> 过于武断。真正应该思考的是,替代后如何自处
|
||||
|
||||
在所有的人类创造力中,社交创造力被认为是最为复杂、最具挑战的一种能力。与计算机科学中的技术创造力不同,社交创造力不是在基于规则的环境中展现的。它所面临的挑战极其复杂,涉及人类的不可预测性,其寻找的并不是简单的“答案”,而是适当的决策。因为在现实的人类世界中,解决复杂问题在大多数情况下不像做数学题那样简单,问题并不会因为答案的出现而彻底消失。政治——与其他的社会议题并无不同——旨在减少问题、转移问题、推迟问题或缓和问题。而这个过程与计算机科学家解决问题的模式相矛盾(这也是数学家、技术人员和工程师经常对这个世界和他们的同伴感到绝望的原因)
|
||||
|
||||
### 工作:一个矛盾重重的概念
|
||||
原因很简单:只有当我们的努力和辛劳换来了报酬,我们才能把它称为工作。比如,梳理自己的头发不是工作,但为他人梳理头发并获得报酬就是工作
|
||||
|
||||
显然,一个活动是否被视为工作,主要取决于它能不能带来高额的收益。即使是最隐蔽和不光彩的活动,只要能带来高额利润,就会被视为工作
|
||||
|
||||
事实上,资本主义社会将工作视为一项重要的社会使命。那些不工作的人在某种程度上会失去身份认同感。他们甚至算不上一个独立的社会阶级,而只能被归类为“无阶级”人群
|
||||
|
||||
资产阶级劳动社会强调:工作即为人生的意义。除非一个人出生在富裕家庭或者与富人结婚,否则工作之外的人生意义便是一种奢侈品,而不是必需品。无论是陪伴孩子,还是照顾父母,都被视为是次要的事情,最重要的是参与劳动,尤其是对于男性而言。
|
||||
|
||||
毕竟,没有闲暇时间就没有反思时间,而反思对于个人成长和社会发展都是十分必要的。而所谓的“智慧的、受到良好教育的、懂得如何生活”的工厂工人只存在于现实社会主义的幻想之中。
|
||||
|
||||
虽然资产阶级的劳动伦理谴责游手好闲的行为,但它却暗自憧憬那种无须劳动就能享受生活的特权生活方式。“享乐主义者”“花花公子”(Playboys)、“浪荡子”(Dandys)等概念的产生正是源自这份憧憬——他们(主要是男性)不受工作的消耗,专注于享受生活。虽然资产阶级总是强调工作的价值,但是它也深知享受生活和辛苦工作是相互对立的。正因此,多才多艺、风流倜傥的花花公子冈特·萨克斯(Gunter Sachs)才会在资产阶级社会成为人人艳羡的楷模,而不是被贴上“懒惰”、“寄生虫”或“无所事事”的标签
|
||||
|
||||
在资产阶级劳动社会中,人生的目的和意义通常是被预先设定好的:人们为了生活而工作,为了工作而生活,直到退休的那天。
|
||||
|
||||
尽管在当今被资本主义所渗透的艺术市场中,先锋派的无畏思想已经成为一种迷思,但这些艺术家的追求却预示了在21世纪越发重要的东西:对自我实现的渴望、对高度个性化生活的追求、不再通过“职业”定义自己的社会身份
|
||||
|
||||
交通高速发展的城市成了漫游者的末日。
|
||||
|
||||
漫游者是现代主义先锋派最钟爱的形象,这并非偶然。艺术先锋派有目的地、有意识地将漫游者与劳动者对立起来。法国作家夏尔·波德莱尔(Charles Baudelaire)认为,只有那些不从事世俗工作的人才能成为“现代生活的画家”
|
||||
|
||||
如果今天的人们在追求自我实现的同时,也渴望放慢生活节奏,那么这在某种程度上也属于先锋派思想的精神遗产
|
||||
> 冥想和瑜伽
|
||||
|
||||
### 大变革来临,我们将会面临什么
|
||||
硅谷积极探索的技术进步不仅闪耀着光芒,同时也给这个国家投下了巨大阴影
|
||||
|
||||
如今,我们不再需要工作人员和中间商的帮助就能轻松地投递包裹、提交休假申请、预订航班、住宿或旅行。今天的消费者已经成为“产销合一者”,即生产性消费者
|
||||
|
||||
人们不需要专业人士的指导也能够自行完成诸多事宜,例如,网络购物、预定住宿、通讯交流、交通出行、处理能源问题、金融交易、饮食管理、预订电影票、摄影拍照、咨询生活问题,以及寻找伴侣等。
|
||||
|
||||
新自由主义的时代已经结束,重返世界舞台的政府,将再次获得主导地位。
|
||||
|
||||
### 自然定律和人类世界:补偿还是替代
|
||||
穆勒如此描述道:“如果一个国家生产的商品数量增加,那么就会产生相应的额外购买力;因此在自然情况下,一个国家永远不可能出现资本或商品过剩。”
|
||||
简而言之:供给越多,需求就越大。
|
||||
|
||||
在他的著作《政治经济学及赋税原理》
|
||||
(On the Principles of Political Economy and Taxation)中,李嘉图提出了后来被载入史册的“补偿理论”:生产效率越高,生产的产品数量就越多,由于大规模生产降低了生产成本,产品会变得更加便宜,能够负担得起这些产品的消费者也就越多,同时他们的口袋里也有了更多的余钱可以用于购买其他产品
|
||||
|
||||
以自然科学的视角看待市场定律的李嘉图对经济学产生了深远的影响,他在一定程度上使经济学成为一门精确的科学。相较之下,西斯蒙第很快就被遗忘了
|
||||
|
||||
李嘉图的辩论中获得了胜利,而“补偿理论”则相应地受到了质疑。技术进步不总是创造出更多新的就业机会以弥补被淘汰的旧有就业机会,这取决于特定的先决条件
|
||||
|
||||
### 解除警报:经济学家以过去预测未来
|
||||
让德国引以为豪的是,它总能自如地应对技术创新所带来的挑战,而它所凭借的两种方式分别是:劳动力市场的结构性变革和全球示范性的职业教育培训体系。回想一下印刷业在20世纪70至90年代所经历过的结构性变革,当时数字化技术彻底改变了生产方式,印刷厂变成了“媒体公司”,印刷工人和排字工人被“印刷媒体设计师”所取代。此外,人们也曾担心自动取款机的出现会导致银行大规模裁员,但实际上,自动取款机只是改变了出纳员的工作内容。历史上并不存在因自动化而引发大规模裁员的先例。
|
||||
|
||||
虽然在20世纪70年代末和80年代初德国的失业率的确异常的高,但到了80年代中期,失业率开始大幅下降。汽车工业和其他行业的自动化无疑导致了部分工人失业,但失业规模很小,而且并不是持续性的。这难道不是一个不断重复的故事吗?无论是蒸汽机和纺纱机的问世,还是电气化或者电子技术的发展,从长远来看,岗位数量从未因为这些技术进步而减少,反而每次都增多了。历史表明,在面对技术革新时,灾难主义者的悲观预测往往是错误的,而那些冷静理智者总能更加从容地应对变化。
|
||||
|
||||
### 对实证理性的批判:这场变革是否可被计算
|
||||
第二经济体
|
||||
|
||||
### 拉响警报:经济学家解读未来
|
||||
虽然康复治疗师、高级工程师、应急管理人员、精神病学家、社会工作者、听力学家、职能治疗师、整形外科医生、口腔颌面外科医生、消防员、营养师和编舞师等在未来仍然可以放心地专注于他们的工作,但是这项研究所列出的其他170项工作岗位就没有那么幸运了,因为它们的自动化概率超过90%。尤其
|
||||
|
||||
### 劳动与工作:劳动社会的诞生
|
||||
毋庸置疑的是,经济学家汉斯-维尔纳·沃尔特曼(Hans-Werner Wohltmann)在《加布勒经济词典》(Gabler Wirtschaftslexikon)中发表的这个观点——“工作最初是人类为了保障生存而与自然界进行互动和对抗的过程”
|
||||
——绝对是错误的。因为早期的狩猎采集社会并没有将人类的生存活动理解为后来工业社会所称的“工作”。事实上,在旧石器时代,人类的大部分时间都是用于休闲、玩耍和交流,这点与其他群居哺乳动物并无不同。恰恰可能是这段漫无目的的时光——而不是那段研制石斧和石棍的漫长岁月——成了人类文明发展的起点。
|
||||
|
||||
|
||||
工作者并不能因为工作而融入社会,相反,一个人越依赖工作,他的社会地位就越低。这一规律至今仍然适用于全世界范围的许多劳动者。例如,在阿拉伯世界受剥削的奴隶劳工,在美国从事服务行业的拉美裔,以及在东亚从事农业、建筑业和养老护理的民工。
|
||||
|
||||
在古希腊和古罗马社会,自由的男性几乎不参与劳动,无论是体力劳动,还是制作可出售的商品。正因此,他们感到自由、骄傲、光荣
|
||||
|
||||
人们普遍认为艰苦的体力劳动会使人思想贫瘠,这个观念在拥有高度文明的古典时代仍然普遍存在
|
||||
|
||||
随着基督教在整个希腊罗马世界的传播,它也带来了一种与上层阶级完全不同的工作观念,因为基督教在公元一世纪还只是小人物的宗教。它不仅尊崇那些受人尊敬的渔民、牧羊人、木匠和农民,也接纳那些被社会轻视的职业,例如税吏。福音书作者认为门徒的传教活动与他们先前所从事的手工业劳动并无不同,这些门徒就像“葡萄园里的工人”或“丰收季节的工人”一样,他们的工作是有意义和价值的,应该获得认可和报酬。
|
||||
|
||||
|
||||
不劳动者不得食
|
||||
|
||||
如果没有对工作的高度重视以及由此吸引而来的大量信徒,基督教无法以如此之快的速度发展壮大
|
||||
|
||||
柏拉图和亚里士多德一致主张金钱的权力应当受到限制,亚里士多德认为金融经济、金融投机和信贷经济并不符合自然规律,并试图抵制这些金融活动。他们将节制视为一种美德,以此对抗金钱所带来的贪婪、不道德和不正当的行为。这种价值观几乎受到所有古希腊哲学家的推崇
|
||||
|
||||
古代的自由公民通过不工作的方式模仿并接近逍遥的众神。而基督徒则相信,只有遵循上帝的旨意去工作和生活才能接近上帝,并获得上帝的赞赏和救赎。上帝这位伟大的造物主并不喜欢闲适无为,他花了六天时间创造世界,在第七天歇工休息,如同一个疲惫不堪的工匠一般。古代的上层阶级通过无须工作来展现他们的社会地位,而基督徒则为自己创造了一种内在自由和个人地位,这个地位完全取决于他们与上帝的亲疏远近。基督徒相信他们是为了上帝而工作,而真正的报酬不仅是世俗的物质奖励,还有上帝的认可和救赎。他们愿意为了上帝的欢心和来世的回报承受任何的艰辛和困难。
|
||||
|
||||
基督教徒几乎从未拥有过奴隶,这个在古代社会属于下层阶级的群体将自己的困境转化成了高尚的美德:他们对工作的认可度和重视度胜过所有更大的宗教。
|
||||
基督教在古代世界的迅速传播归功于基督徒从工作中获得收入,进而提供了巨额捐款,教会因此得以繁荣发展,并能够支付教会工作者的薪酬,派遣传教士到世界各地,以及救济贫困者
|
||||
|
||||
中世纪晚期教会花了大量的时间和精力来甄别那些不能工作的人,因为只有他们才能获得救济。
|
||||
政府甚至为乞讨人员颁发了乞讨许可证,这是一种特殊的乞丐标志
|
||||
|
||||
但是在中世纪晚期,教会宣称工作是摆脱贫困的最佳途径。尽管基督教在福音书中表示“富人若想进入天国,比骆驼穿过针眼还难”,但它却要求中世纪人民努力追求财富
|
||||
|
||||
基督教重视所有的工作和所有的人,包括收入微薄的短工,处于贫困边缘或生活拮据、不知如何生存的人。在上帝的眼中,任何工作都是一种美德
|
||||
|
||||
洛克认为,工作就是判断一个人是否拥有经济价值的最理想的方式。在他的人类学理论中,他强调财产和工作是构建一个良好和公正社会的基础。工作和财产是相互关联、相互依存的,因为财产是通过辛勤劳动应得的报酬。和斯多葛派哲学家(尤其是洛克热衷研究的西塞罗)一样,洛克认为人类被赋予的使命就是要提升自我。他巧妙地使用了“劳动”(labour)这个词。而斯多葛学派的理论则表达了截然相反的观点:只有那些不需要为了生计而工作的人才有足够的闲暇时间自我完善和提升。然而,洛克将劳动和工作这两个概念混为一谈。他认为人类的使命就是自我保全,并通过辛苦劳动获得财产,从而扩展自己的生存空间并保障自己的生存。
|
||||
|
||||
由此可见,洛克的哲学思想是矛盾的。一方面,他捍卫所有人的自然平等和自由;另一方面,他又为那个由金钱主导的不平等(和不自由)社会辩护。对这位哲学家而言,不平等的金钱社会比平等的自然状态更为优越。他
|
||||
|
||||
洛克认为,不劳动者无法拥有任何权利。不利用自然资源的人,也就没有履行上帝赋予他的任务。洛克的劳动理论对资产阶级社会产生了深远的影响
|
||||
|
||||
### 用“劳动”替代“人”:经济学中的“工作”概念
|
||||
洛克在《政府论》上篇中极力并有效地驳斥了君权神授的观点,他认为世间万物并不因为上帝的赐予就自动归属于任何人。虽然上帝将美洲赐予了印第安人,但是在洛克看来,其他人有权合法地夺取这片土地并加以开发,如果一个人没有占有和开发某个资源,那么他就没有权利拥有这个资源。
|
||||
|
||||
专家没有灵魂,享乐者没有良心
|
||||
|
||||
异化的生活”这个概念可以追溯到亚里士多德,马克思使得这个概念闻名于世。它指的是一种缺乏平衡和自我实现的状态。在资本主义制度下,无论是资本家,还是工厂工人,都生活在这种状态之下,他们难以获得真正的满足和内心的平静
|
||||
|
||||
### 工作厌倦和工作认同:社会民主主义中矛盾的工作概念
|
||||
傅立叶出生于一个富裕家庭,但在19世纪初不得不以旅行推销员和出纳员的身份勉强维持生计。他的伟大目标是全面重建法国经济和社会,因为工业革命并没有带来普遍的繁荣,而只是让少数人以牺牲许多人的利益为代价变得富裕
|
||||
|
||||
傅立叶开始思考如何重组19世纪的工作和工业,即整个经济综合体,以确保所有人能真正从中受益。于是他勾勒了一个由合作社组成的和谐社会,在这里,全体社员共同劳动、共享成果,每个人都能够在社会道德规范下自由发展自己的爱好、才能和欲望,并从事自己有意愿做且有能力做的工作
|
||||
> 制度设想是对某类现实的回应
|
||||
|
||||
在他那个时代,工业化国家的大多数人都是靠着辛苦、无意义的工作艰难度日。许多更不幸的人甚至找不到工作,只能上街乞讨,或者组建马克思后来所称的“工业后备军
|
||||
|
||||
社会民主党的核心理念是在工作中尊重和满足工人的需求和权益,而不仅仅是把他们视为生产要素。随着时间的推移,社会民主党的社会理论逐渐淡化,直到与其他所谓的资产阶级政党难以区分
|
||||
|
||||
重要性,同时也明确了工作和生活的界限。实际上,他们的目标并不是划分两者的界限,而是追求两者之间的平衡,即所谓的Labour-Life-Balance(劳动与生活的平衡)。
|
||||
|
||||
20世纪80年代,格尔茨曾如此描述这一转变:“从劳动义务制度过渡到物质激励制度绝非易事,人们必须说服劳动者相信消费能带来一定程度的幸福愉悦,可以弥补自己为工作所做出的辛勤付出,能够使人们摆脱普通、平庸的命运。
|
||||
|
||||
实际上,工业化国家确实成功转型成为以物质激励为基础的需求创造型社会,它通过投入数十亿元的广告营销费用,成功唤醒了数百万个新需求。这些需求对于雇员来说值得投入精力去努力工作。社会民主主义的补偿措施——不断提高工资以满足不断增长的消费需求——持续了一个世纪之久,被证明是有效可行的。雇员受到激励乐于“出售”自己的劳动力。而蓬勃发展的消费社会则演变成了当下的超级消费社会。无论是艾哈德时代的冰箱、瓷砖浴室和大众甲壳虫,还是20世纪七八十年代的联排别墅、大众高尔夫、长途旅行和网球俱乐部会员,抑或是今天的城市别墅、SUV(运动型多功能汽车)、鲁滨孙度假俱乐部会员,都让人趋之若鹜。
|
||||
|
||||
在这个世界中,几乎所有人都追求着相同的目标:通过购买一些量产的产品来显示自己与他人的不同。消费信仰成了当今世界的普遍信仰,成功地将所有需求都转化为货币价值。对于这种持续发展的趋势,人们有着不同的观点。有人将其看作一种全新的文化的现象:通过消费表达个性;也有人将其视为一种“文化突变”(格尔茨):一切都与金钱有关。人们最好是同时保留这两个视角,因为它们都有一定的合理性。
|
||||
|
||||
下降。可是,在社会福利国家出现之前是否真的存在过一个更为团结和互助的社会?何时存在过呢?对于这个问题,每位新马克思主义者都必须做出冷静且不浪漫的回答。
|
||||
|
||||
不把工作视为生活的中心势必会带来双重打击:一方面,它会对经济理性造成冲击,毕竟经济理性将劳动者的价值仅仅限定为他们所能提供的劳动产出;另一方面,它使那些代表工人利益的组织深感不安。如果工作的重要性降低,那么这些组织也就变得越来越不重要
|
||||
|
||||
### 劳动世界的解放:自由主义的劳动概念
|
||||
如果生产效率极高的机器被用于为所有人而不仅仅是少数人谋取福利,人类将迎来一个光明的未来。在他的脑海之中,戈德温看到了未来的几个世纪,机器将逐渐取代所有的初级人类劳动。机器会将人们从许多单调的工作和重复的日常中解救出来
|
||||
> 古罗马时代的奴隶。奴隶为公民服务,公民得以从体力劳动中解放出来。问题机器为谁服务?为企业主服务,企业主就获得更大的利润。
|
||||
|
||||
抛开这些想法,参考一下下面这个提议如何:自由才是工作的目的!首先是怀着对成果的骄傲完成工作之后的自由。再者就是工作之中的自由。这种自由意味着人们是自愿自发地从事一份喜爱的事业,理想情况下甚至还能在此过程当中进入一种忘我的境界,也就是所谓“心流”。
|
||||
|
||||
技术上的进步只让极小部分的人获取了好处而并没有满足大多数人的需求,更遑论让他们过上更加人性化的生活
|
||||
|
||||
要如何结束这种残酷的“剥削”,这种消除无产者的被异化了的工作呢?在19世纪40年代,马克思和恩格斯这两位年轻革命家的思想正聚焦于此
|
||||
|
||||
脱离了原来的环境,被迫从事单调的体力劳动,这种情况下的劳动者已不再是人,而只是一个被异化了的、能够实现某些功能的机械装置罢了。
|
||||
|
||||
劳动者“与生产力以及他们自身的存在之间的唯一联系,也就是工作,对他们来说已经失去了一切用于确认自我价值的假象,并且只能通过一种使生活失去活力的方式来维持生活。”工作变得毫无体面可言,劳动者自身也随之失去了尊严
|
||||
|
||||
他们的理想是以“联合”取代资本主义的生产方式,也就是指过去戈德温和傅立叶所提及的商品合作社:以自愿合作替代强迫,以合作社的形式替代剥削。异化应当被消除,以便今后每个人都有机会从事任何活动,而不仅仅是某一门专门的手艺或一种被简化了的工作
|
||||
> 问题是在生产力没有提升甚至急剧滑坡的情况下谈消除异化是不现实的。首先是有足够的资源,谈分配才有意义。
|
||||
|
||||
而在共产主义社会之中,人们并没有这样的专属活动范围,而是可以在任何行业中发展自身。一般性的生产由社会来调节,这使人们获得了这样一种可能性:今天做这件事,明天做那件事;早晨打猎,下午捕鱼,晚上放牧,晚饭后再随心所欲地针砭时弊,而并不需要以猎人、渔夫、牧民或者评论家作为职业
|
||||
> 然后大家都挨饿
|
||||
|
||||
马克思设想了这样一副图景:未来的工厂将通过全自动化的机器改变整个劳动世界。“生产的过程将不再是劳动的过程。”
|
||||
因为“活的工人”越来越不重要了。他们会被一种“活(跃)的机器”取代。这种机器相对于工人“个体的、微不足道的劳作”
|
||||
而言,显得像是一个巨大而有力的有机体。但这意味着什么呢?越来越多的工人被日臻完善的机器从异化的工作中解放出来,难道我们需要对此有所抱怨吗?绝非如此!事情恰恰相反。马克思欢呼道:“通过这个过程,生产某种商品所需的劳动量的确被最小化了,但这样做只是为了利用最大数量的劳动力来生产尽可能多的产品。前一个方面十分重要,因为资本在此处无意中将人类的劳动力输出减少到最低限度。这将有益于被解放了的劳动,也是其得到解放的条件。”
|
||||
换言之,由于在完全自动化的机器中集成了无穷无尽的劳动力,工人需要进行的工作大大减少,并有更多时间进行“被解放了的劳动”,即他们真正想做的事情。
|
||||
|
||||
马克思或许在某段时间里沉醉于他的想法之中。他终于找到了一种通过资本主义实现共产主义的机制,而不是像许多革命者所想的那样去对抗资本主义!无阶级社会会自然而然地从受资本主义推动的自动化中产生。人们要做的就是坚定地将它构想到底而已。因为如果没有雇佣劳动的话,资本主义还是资本主义吗?如果没有受剥削的人,剥削还算得上是剥削吗
|
||||
|
||||
依照马克思的观点,一个社会越能够承受工作量减少所带来的结果,它就越是处于一种良好的状态。机器中集中的劳动力使得人们能够进行自我实现,无论是何种意义上的自我实现。剥削与被异化的劳动并不通过革命结束,而是通过技术和经济的演进来实现
|
||||
|
||||
这篇今天被称为“机器论片断”(Maschinenfragment)的文章从未有过哪怕一天的影响力。在现实社会主义中,它未曾发挥过任何作用——无论是在正统的学说之中还是在对实践的考虑之中
|
||||
|
||||
于是可能发生的情况是,东欧和东南亚的那些自视为归属于社会主义和共产主义的国家以完全不同于马克思的方式来定义劳动者的解放。应当将工人和农民的工作从其异化中解脱出来的并非自动化,而是这样一个事实,即企业实质上属于国家——按照这一理念也就意味着属于全体人民。如果没有资本家对劳动者进行剥削,他们就应该会认为自己的工作是有价值且充满意义的。他们应该通过自己从事的工作来完成自我认同,就像通过他们所处的企业来做到这一点一样;并且应该满心欢喜地参与到人人各得其所的社会主义的建设中去。通过这一方式,即使是在现实社会主义中,“工作”也会变成存在的情念程式。但并没有人以上述方式满足了实现人生意义的崇高要求。即使是在国有企业,锯木厂或露天矿中的工作也不会有任何实质的变化。而在锯木厂或铀矿开采中做到完全的自我实现,充其量是一种俗套的说法,往坏了说就是一种不得已的挖苦讽刺。即使企业和公交车都掌握在人民——也就是国家——的手中,也并不意味着成为铸造工或公交车司机这件事情就能和人生的意义画上等号。过充实的生活并不等价于终身从事一份特定的职业。相反,这二者是相互矛盾的,这种矛盾即使通过“党的工作”也无法调和。
|
||||
|
||||
在传统的体力劳动职业中,要获得有意义的整体经验是十分困难的,即使在所有权关系发生变化的情况下也是如此
|
||||
|
||||
钢铁工人和矿工应该如何成为猎人、牧民、渔夫或者评论家,这一问题也得到了新的答案:其中的关键恰恰不在于他们的职业工作,而是在于工作之外大量的可支配时间。
|
||||
|
||||
它不需要革命者,而只需要这样的人——他们思考如何适当地分配因生产力提高而产生的社会财富,以便使每个人的物质需求都能够得到满足
|
||||
|
||||
在埃里希·弗罗姆(Erich Fromm)写出《占有还是生存》(Haben oder Sein)的几十年前,王尔德就写下了这样一句话:“真正关键之事并非‘占有’,而是‘存在’;人类真正的完美不在于他的占有之物,而在于他的本质
|
||||
|
||||
时代。而正如同树木会在农夫沉睡之时生长,当人类沉湎于享乐和闲情之中时——闲情才是人类的目标,而非工作——当他们在创造美好事物或阅读美好事物,或者仅仅用赞赏和享受的目光欣赏世界时,机器将会完成一切必要且令人不快的工作。”
|
||||
|
||||
|
||||
俗套。因此,只要有工作是“机械而单调乏味的,或是令人反感并且会将人置于糟糕的境地,那么它就必须由机器来执行”。
|
||||
|
||||
|
||||
身处东方阵营的国家或中国的劳动者感受到的受异化程度并不比资本主义社会中来得低,区别仅仅在于“上位者”的不同:政党的高层取代了企业高管和公司老板
|
||||
|
||||
而传统体力劳动则无非是被扰乱的、扭曲的、被异化的劳动。如此一来,社会主义的目标就确定了:铲除被异化的劳动(即传统体力劳动),并让人们遵循其天性,重新从事注重“质”的工作。
|
||||
|
||||
技术并不像后来的李嘉图所认为的那样造成了失业,而是导致了根本无工可做的局面!在未来的某个时候,劳动者很可能每天只需要工作三个小时就能够过上体面的生活。因此需要实行的是每周21小时的工作制度,因为现在已经不再需要将周末特别指定为“休闲时间”
|
||||
|
||||
然而,认为技术进步会越发地将人们从消耗性的、被异化的劳动中解放出来的观点在左翼的思想史中一直位于边缘,从未处在中心位置
|
||||
|
||||
与之相应的问题是就:我们如今究竟为何而工作?繁荣富足在21世纪意味着什么?如果一个社会的核心不再是要求工作的权利,而是对意义的追求,这又意味着什么
|
||||
|
||||
:“如果社会主义是专制的,如果其中存在以经济力量武装自己的政府,那它就同现在以政治力量武装自己的政府别无二致。简而言之,如果我们得到的是工业暴政的状态,那么人类的最终阶段将比最初阶段更加糟糕。”
|
||||
|
||||
|
||||
无论在青年时期还是晚年,马克思都将自决权等同于社会主义和共产主义,而这种自决权几乎不存在于任何地方。至少在体力劳动的领域,这种自欺欺人的制度始终是官僚主义的,具有党派性、控制性和强迫性的制度,而永远无法成为自由的制度。它如同一个鸟笼,不同程度地困住了许多与注重“质”的工作相关的职业。
|
||||
|
||||
对拉法格来说,“机器是人类的救世主,是让人们摆脱雇佣劳动的上帝,是给他们带来闲暇和自由的上帝”。
|
||||
|
||||
|
||||
诸如格尔茨这样聪明绝顶的思想家一生都致力于回答这样一些问题:为应对20世纪的情况,要如何重新审视马克思的观点?要如何确保工人拥有尽可能多的可支配时间?要如何让一个受经济主义支配的社会意识到这世上存在比经济理性更加高等的生活智慧?
|
||||
|
||||
### 在场即是一切:如今我们为何而工作
|
||||
如果没有一场能在未来学会并教导人们革除“对优越感的渴求”或“对金钱的热爱”的文化演进,那么光明和平的未来可能就会化为泡影
|
||||
|
||||
凯恩斯以一种将来完成时的手法巧妙地表述了他对于文化变革的希冀:在未来的某个时候,“人们将会认清对热爱金钱的实质:它是一种令人反感的病症,一种半是犯罪半是病态的倾向,使人只能将它交给精神疾病的专家来处置”。
|
||||
|
||||
|
||||
草率地批评嘲笑凯恩斯的那些人如若听到他下面的论断可能会竖起耳朵:“从充满贪欲的社会过渡到人人幸福满足的社会的巨大变革并不会一步一步地降临,也许在一百年后它才会到来,也就是2030年前后。直到那时人们才会‘以完全不同的方式利用大自然的馈赠’,并且重拾最值得信赖的宗教信条和传统智慧——贪欲乃是恶习,借由高利贷牟取暴利乃是罪行,对金钱的热爱乃是可憎的。”
|
||||
|
||||
他们得认识到,美好未来的实现并不仅仅在于“更多的物质”,而是有赖于新的经济组织形式
|
||||
|
||||
那么就如汉斯约格·西根塔勒(Hansjörg Siegenthaler)所问,为何我们的生活中虽没有真正意义上的稀缺性,但却还是存在一种“基于文化的稀缺性”?
|
||||
|
||||
这种稀缺性显然也正在消耗生产进步赠予我们的东西:马克思所说的可自由支配的时间。
|
||||
|
||||
在19世纪上半叶的曼彻斯特资本主义之下,纺织厂和矿井中的童工像牲口一样拼命地工作。它和如今德国的社会福利市场经济一样都是资本主义,但它们之间差异还是远远大过共性
|
||||
|
||||
最简单而常见的回答是:因为人类就是如此!他们很少能够持续地对某样事物感到满意,自然也不会满足于自己的生活水平,他们永远欲壑难填。如凯恩斯那样的设想过分高估了人的理性,同时严重低估了他们的贪欲
|
||||
|
||||
物质上的贪欲是本就深植于人性之中的,还是通过文化培育而生并为人所习得的?首先必须明确的是,“贪欲”是个相当难以捉摸的概念。人类可以对各种事物心存贪欲,包括爱情、性、食物、知识、享乐、认可、权力乃至毒品。然而根据“贪欲人类学”的观点,只有对于金钱与消费品的贪欲才与经济挂钩。而它应当作为塑造人类的主要因素。如果这种说法当真如此准确明了,那人们不禁要问,这种物质上的贪欲为何没有一一体现于人类历史之中?倘若它是人类的本质,为何上至古典时代下至中世纪之后,人们都对它嗤之以鼻?为何几乎所有东方宗教和智慧学说都如此不重视物质贪欲?显然存在一种基于节俭的经济学,在人类历史上的数万年间比基于贪欲的经济学更具影响力。而后者仅仅有两百年的历史,它如今的形态也是在过去几十年里才野蛮生长而成的
|
||||
|
||||
它只会不断地激起并刻意鼓动新的虚假的需求,除此之外什么也做不到
|
||||
|
||||
与之相对,越来越多的人则在对自己进行着关乎工作意义的灵魂拷问:我到底为何而做着我正在做的事情?我因为拥有一份工作而开心,这远远不能倒推出我的工作使我感到幸福这一结论
|
||||
|
||||
当今的信息社会中的一大批岗位其实都没有存在的必要,包括公司律师、说客、营销专员、战略顾问和广告从业者。格雷伯使用了一个非常惹眼的词汇:“随从”(Lakaien)。这些人的存在只是为了让他们的上级显得比较重要。那些向他人推销纯属多余的保险或投资产品,或者作为公司的律师和公关专员在竞争中牵制自己的同行的人,格雷伯称之为“打手”
|
||||
|
||||
根据其结果,在德国每三个雇员中就有一个认为自己的工作毫无意义。只有16%的雇员喜欢自己的工作,68%的雇员或多或少只是按部就班地跟着规章办事,另外16%的雇员则称自己内心已经处于一种离职的状态了
|
||||
|
||||
第二个假设认为财富会随着生产的进步而增加,而随着财富的增加,对有偿就业的需求也会增长。这同样经不起推敲
|
||||
|
||||
Schläger)。那些要么为老板犯的错误善后,要么通过自己的工作转移问题的人,被他称作“修补匠”(Flickschuster)。他还提到了“打钩者”(Kästchenankreuzer),也就是那些虽然装出一副全情投入的样子,但实际上只不过是在记录和总结他人工作的人;还有那些常见于中层管理岗位上,只是在简单地委派要求的“任务分配者”(Aufgabenverteiler),任何公司或机构都可以随手将其开除,而不会有任何损失。格雷伯认为,这些岗位之所以会存在,仅仅是因为它们处在一些极少由于企业经济效率的原因而需要削减开支的领域罢了。
|
||||
|
||||
其中相当一部分都是美国人类学家大卫·格雷伯(David Graeber)所说的“毫无意义的工作”
|
||||
(Bullshit Jobs)。在格雷伯看来,如今的很多职业中都充斥着多余的工作,时间被大量浪费。甚至有不计其数的职业是压根没有意义的
|
||||
|
||||
只需认识到,官僚化的方方面面都在通过一些过去并非必要且效用有限的工作来扩大就业市场,假如不曾这么操作的话,如今在德国就会有数以百万计的人处于失业状态。凯恩斯有句名言:建造金字塔好过支付失业金。这一说法广受认可。而这或许就是他的孙辈的工作量仍然如此之大的原因所在?我们的经济创造、增加了如此多非必要的工作,而我们的工作量相较于五十年前并没有明显减少,这一切也许完全是有意为之。难道不能提出这样一个观点来反对凯恩斯,说大多数人可能根本就不想减少工作量?
|
||||
|
||||
我们因何工作?这个问题是由市场逻辑、主流文化和社会心理学的相互作用来回答的
|
||||
|
||||
波兰尼已经敏锐地认识到,大多数人生活中最重要的价值并非金钱或物质财富,而是来自社会关系的认可
|
||||
|
||||
没有人会要求成绩斐然的德甲球员们在结束了职业生涯之后还继续工作。然而人们会要求社会救济金的领取者接受国家分配给他们的工作,否则救济金就会被削减。因此,受到谴责的并非不工作,而是无偿接受转移支付的行为。如果事情当真如此,那么后果将非常严重。我们只需要想象一下,所有那些生活在糟糕的经济条件下的无业者在某一时刻将不再能够通过那些从事有偿工作的人们的劳动来维持生计,就能够理解这一点。如果是这样,从逻辑上来讲就不应再谴责不工作的人。我们会在近期或中期内进入这样一个社会吗?是谁为此铺平了道路?是明智的洞察?抑或仅仅是旧制度的崩溃
|
||||
|
||||
在一个经济成就决定社会地位的文化中,许多人很容易想到要去追求象征着财富的商品,这一点可想而知。而且,财富越是来之不易,人们就越要显摆这些商品(譬如手提包、手表、车辆和房产)
|
||||
|
||||
### 苦力劳动并非真正的工作:旧有劳动社会崩溃的原因
|
||||
事实上,自20世纪60年代以来,工业社会并没有走上利用巨大的生产收益来为全体公民提供基本权利保障的道路。取而代之,它们在更大程度上扩大了福利制度——失业津贴、失业援助和社会援助——但仍然坚持每位成年男性(对于女性则不那么重要)都应该为了自己的生计而工作。工作与收入之间的纽带在经济意义上有所削弱,但在社会层面上,人们仍然对它深信不疑,就仿佛它还像早期一样强劲有力、不可替代。从越发自动化的新兴经济中获益的赢家变得越来越富有,工会坚持要求“工作权”,而政客们在每一个新的十年里都把“充分就业”这个词如星星一般描绘在代表未来的夜空之上。
|
||||
|
||||
鉴于生产力得到了极大发展,很多人已经不需要通过工作来过上好日子,但这能否真的实现还未可知。使之真正成为可能的并非逻辑过程,而是关乎权力关系的问题。
|
||||
|
||||
备忘录聚焦的重点是技术革命以及劳动社会的未来。高度自动化的机器那近乎无限的生产力,从长远来看,使人类的劳动变得越发可有可无
|
||||
|
||||
更加重要的问题是如何在21世纪分配权力和资源,即如何将技术进步所带来的收益尽量合理地分配给尽可能多的人的这一问题。当财富越来越多地由机器创造,而非依靠中等素质劳动力的大规模雇佣劳动来创造时,工作和收入之间的密切联系也越发失去意义
|
||||
|
||||
它必须确保今后每个个体、每个家庭都获得足够的收入——不是作为福利,而是作为一项基本人权。
|
||||
|
||||
成功社会正在逐步重塑旧有的高绩效社会。无论是通过创业、股票交易、诈骗还是通过社交媒体事业,快速的成功均变得越发具有吸引力,至少比缓慢而艰难的职业生涯更有吸引力。
|
||||
|
||||
双薪家庭越多,家政服务就越重要。无论是在安保服务行业从业还是去餐厅当服务员和帮厨,报酬低廉且缺乏社会保障的工作无处不在,等待着被人接手。社会学家安德烈亚斯·雷克维茨(Andreas Reckwitz)认为,现在已经形成了一个不断增长但却缺乏社会上升通道的服务阶级
|
||||
;然而这个结论并非新知。早在20世纪80年代,格尔茨就未卜先知,预测到了“新仆人阶级”
|
||||
的出现。
|
||||
|
||||
持续创新、适应新事物、追求日新月异、向星辰大海进发,诸如此类层出不穷的口号只适用于新产品的生产,而不适用于人类共同生活的全面改善。初创企业可以天天重塑世界,必要的时候甚至可以对着尚未提出的问题给出无数个答案,为未曾了解的困难提供解决方案——但它们不能让社会变得更加公平,不能减少紧张的局势与冲突或者让生活变得更加充实。
|
||||
|
||||
没有一个路人会根据这些人工作上的绩效来衡量他们的成功。也几乎没有人会认为,这种用于炫耀的经济方面的成功是源自勤奋与努力。在这样的场景之中,想显得自己有些身价的人就得尽量看上去放松一些,身上不能有打工人的气质。无论是足球运动员、扩大化的毒品经济领域的企业家还是某个成功的家族的成员,这样的人都是新时代封建贵族阶级的一员,已经完全脱离了苦力劳动。
|
||||
|
||||
20世纪50年代到80年代,德国比其他许多国家都更加接近绩效社会的理念。整整一代人通过教育与职业培训从中产阶级的中下层广泛跃升至中上层的历程历历在目,令人难忘。德国从一个由工人和小职员组成的国家转变为一个由工程师、教师、商业代表、经理人、律师主导的国家,它有着充分的理由重视绩效概念
|
||||
|
||||
根据德国经济研究所(DIW)的数据,1%的德国人占有了全部个人净资产的35%,而富有程度在前10%的德国人占有了全部个人净资产的59%。
|
||||
这样的数据乍看之下很难让人联想到德国是一个以中产阶级为主体的国家,而且财富分配越发不均的趋势还在持续发展。对于一个将劳动置于核心地位的社会来说,这种趋势极其令人担忧。因为没有任何人能够言之凿凿地表示,那1%的富人的财富增长完全是他们个人工作成就的体现。
|
||||
|
||||
硅谷很喜欢这样一套说辞:将人力用在那些重复的例行工作上实在太过可惜了,所以这些工作最好应该交由机器人来完成。然而整个硅谷的生活都要依靠外卖送餐员、保安、清洁工、保姆以及其他服务人员才能继续。但
|
||||
|
||||
### 紧握栏杆:工作对我们来说意味着什么
|
||||
很显然,许多人像抓住栏杆一样紧握着他们的职业生涯。这个栏杆能够确保他们迈出正确的步伐而不会陷入无依无靠的境地。这个栏杆也确实保证了他们社会地位,为他们指派了在社会中的席位,并将他们置于“劳动社会”宏大的秩序宇宙之中,使得他们能够成为巨大机器中的一颗有意义的齿轮
|
||||
|
||||
它取代了过去数千年间人类紧握的另一个栏杆:即大家庭的融合、家庭生活与职业的传统,以及在一种环境或信仰之中的定位
|
||||
|
||||
社会学家海因茨·布德(Heinz Bude)就提出过:如今,任何想通过工作来定义自身的人都会立即意识到,这件事情只能是暂时的、片面的,且有着决策主义和极权主义的倾向
|
||||
|
||||
对
|
||||
|
||||
经济与文化的隐隐不安引起了许多人的共鸣:每一次进步也是一次倒退,并非每一次优化都必然指向一个更加美好的世界,而颠覆并非仅仅因为它是颠覆就一定是好事
|
||||
|
||||
一方面它欣然接受通过机器带来的效率提升,经济界也因为未来需要的劳动力大大减少而欢欣鼓舞;另一方面,劳动社会又为随之而来的工作岗位流失而伤脑筋
|
||||
|
||||
然而在现实中却很少有人还将有偿工作的消失视为一种巨大的进步。失业——所指的仅仅是失去有偿工作的机会——在今天被视为一种缺陷和污点的程度并不比过去几十年间来得低。如今失业的人可能无法从这些理由中得到安慰:失业是人类的自然状态,古希腊人以不工作为荣,甚至晚年的马克思也只能在有偿工作的范围之外设想普遍的自我实现
|
||||
|
||||
正如社会学家卡尔·奥托·洪德里希(Karl Otto Hondrich)在20世纪90年代末明确指出的那样,有偿工作在社会中的价值越高,失业就越会被视为一种罪恶
|
||||
|
||||
但主要的错误在于,赫尔佐格轻率地将“工作”和“有偿工作”混为一谈。与她所说的有所不同,有偿工作并不能塑造人性,关于这一点已经有过详细的讨论
|
||||
|
||||
她在其后确定了一个结论:“工作极大程度上是一项存在于人性之中的事情,它是我们本质的一部分,即便社会关系的组织形式完全改变,它很可能也仍然会存在。人们想要创造某些东西,想要塑造世界,工作是满足这种冲动的核心形式。”
|
||||
|
||||
|
||||
如果他们舒适的生存和共处得到了保障,那么有去田间挥汗如雨地耕作或者在钢铁厂的高炉边干活这两种冲动的人肯定极少。去做些事情,以某种方式恣意生活,这些都是人类天性的一部分。但从事一份有偿工作却并不在此列
|
||||
|
||||
### 拯救或是替代:使工作变得人性化
|
||||
无论在何处,有偿工作都不应该是僵化单调的,而应该是灵活的,并以自我决定、自行组织的方式,在尽可能扁平的等级制度之中开展。这一切都是二十多年来人们所要求的
|
||||
|
||||
上述宣言的作者们认为人性化并非通过对有偿工作的改革来实现,而是要使劳动回归其最初的本质:即人类具有创造性的恣意生活
|
||||
|
||||
技术进步本身并没有意义,只有将其对劳动世界的影响用于造福大多数人才能使其变得有意义。反之,这对他来说意味着:“如果节省下来的工作时间不能变成自由时间,如果被解放出来的时间不能用于实现‘个体的自由发展’,那么节省工作时间这件事情则毫无意义。”
|
||||
在有偿工作过程中节省下来的时间越多,也就有越多的时间可供用于“自己的工作”——并非为了市场或国家,而是在第三部门进行的自己的工作。
|
||||
|
||||
自由的时间将会盖过充满强迫的时间,空闲时间将会盖过工作……空闲时间不会再仅仅被用于恢复元气或者作为一种补偿,而是实现充实生活所必需的重要时光,工作则会被降格为一种纯粹的工具
|
||||
|
||||
如果如赫尔佐格所说,如果无法利用社会民主主义的工具箱中那些久经考验的工具来拯救工作,那么也许能够通过以下方式做到这一点,即将工作的定义从有偿工作的狭窄范畴中解放出来,同时或多或少地将人们所做的一切事情都视为工作并且予以重视
|
||||
|
||||
若要在全球范围内消灭剥削,那么世界的财富就必须被公平分配。一言以蔽之,资本主义世界经济将土崩瓦解。剥削的终结将使许多产品变得更加昂贵。人们必须重新填补经济权力的真空地带,并将面临大规模的移民、内战、革命和屠杀等结果
|
||||
|
||||
免于被自动化取代的主要是第三部门的工作,即为照顾自己、照顾他人和保护环境所做的迄今没有报酬的工作。这样的体系所需的仅仅是一个新的税收模式,使自动化生产的利润被转移到第三部门,为其发展提供丰富的养料。
|
||||
|
||||
不好的工作是一种“轻微的疾病”,而好的工作则至少像性爱一样美好
|
||||
|
||||
人们应该做他们真正想做的事情,是一句动听的指导原则。但是如果缺乏现实的社会概念,缺乏政治上的议程,没有彻底思考如何为此提供资金支持、未来由谁来完成那些不那么美好的工作以及谁必将忍受“轻微的疾病”,那么这句话也只能是虚谈高论。
|
||||
|
||||
我们会不会不将工作量的减少视为一种失败,而是人类对于苦役的一场胜利?如果越来越多的工作被夺走并交由机器完成,德国乃至整个中欧的人们是否会失去他们的依靠?如果没有办公室,没有实践,没有工作台,没有议程和时限的压力,他们是否会无所适从?他们能否像非洲或拉丁美洲国家的许多人一样,学会享受这种不必日夜为金钱而奔忙的状态,并将之视为自我充实的方式?
|
||||
|
||||
首先是一种极端的宿命论。处于就业年龄的人如果不从事有报酬的工作,就会被指责为失去了自我价值感和生活的意义
|
||||
|
||||
第二个幻想是第三部门。第三部门越是执拗地在有偿劳动社会边上建立起一个准有偿劳动社会,那么它就越会固化传统的有偿劳动社会模式,直至没有任何一项人类活动不被视为工作
|
||||
|
||||
### 做正确的事:意义社会
|
||||
意义社会面临着堕落为上述两级社会的危险,即社会上只剩两类人,一类是收入越来越高的高素质劳动力,另一类是越来越多的再也跟不上时代的人。为了有效地应对这种危险,意义社会需要进行一种破坏,需要进行一种深入劳动社会DNA的干预才能取代它,也就是说意义社会需要切断收入和工作之间那条坚固的纽带
|
||||
|
||||
第二机器时代失去了工作的人,例如电车司机或保险公司员工,他们并不一定就会变得有创造力。相反,他们中的某些人可能还会变得具有攻击性、破坏性或变得抑郁
|
||||
|
||||
在荒谬的时代里没有正确的生活”,这是《最低限度的道德》中最著名的一句话
|
||||
|
||||
无止境地想要拥有更多东西”并不是一件好事,而是一个巨大的麻烦。有些人因为工作而错过了一些生活的乐趣,所以会不断地给自己物质奖励,并且已经完全离不开这些物质奖励。如果这样的人高达数十亿,那么他们就会变成一颗颗钉子,钉在人类的棺材板上。
|
||||
|
||||
这一点正好反映出阿伦特的思想中流淌着的古希腊、古罗马贵族精神,即怀念悠闲的生活,怀念对时间的自主支配权,让人们可以有空关注公共事务,怀念在社会群体中实现个体和社会共同进步的那些日子。
|
||||
|
||||
然而,什么才是有意义的事情呢?这个问题并不只关乎个人。它的答案在很大程度上还取决于一个人所处的文化。在西方基督教的历史中,一千多年里人们都曾认为:将自己的一生奉献给修道院并成为神职人员是一种有意义的、被社会高度认可的人生规划;而在伊图里丛林或巴布亚新几内亚情况则截然相反。在西欧,当萨满、制作木乃伊或者当武士,这些职业从来都不是能得到社会认可的选择。文化决定了需求。如果某种文化以人生的智慧和获取知识为最高目标,那么在这样的文化中总是会诞生一些哲学家,比如古代雅典就是如此;如果某种文化重视军事并欣赏勇气,那么它就会培育出许多不畏死亡的士兵,比如古代的斯巴达;如果某种文化欣赏冷漠、傲慢和种族主义,那么在这样的文化中就不难找到成千上万的“第三帝国”党卫军士兵。而那些把商业成功看得比什么都重要的国家,比如美国,也会培养出数百万同样追求商业成功的人。对雨林中的原住民来说,修道生活是不自然的;对古希腊、古罗马时期的哲学家来说,追求尽可能多的财产也是不自然的;同样,懦弱于斯巴达人而言,种族平等于种族主义者而言,平等分配于激进资本家而言,都是不自然的。生意迷看重精明胜过看重智慧,他们嘲笑那些精神充实的人(除非这些人很富有);相反,知识分子往往也认为,对金钱强烈的痴迷是一种性格上的缺陷。
|
||||
|
||||
这使得意义社会出现了两个极端:一方面是数字化进步带来的大规模失业,另一方面是数字化让每个人都有机会实现自我赋权
|
||||
|
||||
|
||||
哲学家汉娜·阿伦特(Hannah Arendt)看到自动化还在继续走向完全自动化,于是写下了以下话语来预示灾祸的来临:“技术进步似乎只是实现了人类世代梦寐以求却无法实现的东西。但这只是骗人的表象。17世纪,人们开始从思想理论上颂扬工作与劳动,这标志着新时代的开始。20世纪初,整个社会完成了向劳动社会的转变,这标志着新时代的结束。实现这个古老的梦想就像实现童话里的愿望一样,梦想得到祝福最后却受到了诅咒。因为,劳动社会一方面想要摆脱劳动的束缚,另一方面却对什么才是更高层次、更有意义的活动几乎一无所知,而只有为了这一类活动而去摆脱劳动的束缚才是值得的……劳动是劳动社会所拥有的唯一的优势,而劳动社会将不再会有劳动,这就是摆在我们面前的劳动社会的前景。还有什么能比这更致命呢?
|
||||
|
||||
### 后工业社会的生存保障:现收现付制度的终结
|
||||
让在职人员赡养退休人员,这是现收现付制一个非常宏伟的目标。而现收现付制其实不仅仅只针对养老金领取者,同时还适用于儿童和青少年。对施莱伯而言,强制保险只不过是以另外一种方式延续了传统的家庭运作模式。先是父母抚养子女,而等到父母老了以后就是子女赡养父母
|
||||
|
||||
即代际契约
|
||||
|
||||
很值得一提的是施莱伯对此给出的理由,非常具有洞察力,他认为:“那些无子或少子的退休人员,他们自以为是地强烈要求并且确实也拿到了与多子女者相同的养老金,但归根到底,他们其实是寄生虫似的蚕食掉了多子女者所做出的额外贡献,因为是多子女者承担了更多才弥补了少子女或无子女者少承担的那一部分贡献。虽然有很多人对这种说法不屑一顾,但关于孩子的数量确实有一个社会性的目标,那就是人均必须拥有1.2个孩子,社会才能保持活力,才能负担得起赡养老人的费用。”
|
||||
|
||||
|
||||
联邦银行和伊福经济研究所(ifo)也建议最早在69岁退休。而德国经济研究所则希望退休年龄推迟到73岁。
|
||||
当然,如果能等到80岁再领退休金那就更好了,或者不用领退休金那就最好了。
|
||||
|
||||
这意味着,要想维持代际契约,经济就必须在量上保持无条件的增长——在生态革命时代,这会带来灾难性的后果
|
||||
|
||||
即使是现在,德国的普通工薪阶层退休后也只能拿到他们之前工资的50%作为养老金,而相比之下,荷兰的退休金可以达到退休前收入的95%。
|
||||
而且,今后德国的退休金还要继续降低!德国数以百万计的养老金领取者将生活在贫困之中。而即便如此,我们的后代仍然会不堪重负,因为每2个人就需要负担1个退休人员的养老金,而在20世纪60年代初还是6个人负担1个人
|
||||
|
||||
就在我撰写本书时,联邦经济部科学顾问委员会确实也提出建议,要将退休年龄推迟到68岁,以抵消“从2025年起,法定养老保险制度所面临的以惊人速度日益加剧的融资问题
|
||||
|
||||
同时,自20世纪60年代末的“避孕药生育拐点”
|
||||
以来,出生率就一直呈下降趋势;而平均寿命则在大幅增加。因为允许提前退休,所以平均退休年龄降低到了今天的62岁左右。就平均生活质量而言,这些都是很好的变化——但对于现收现付制度来说却不是。虽然养老保险费用不断上调,但早在1965年,养老基金就不得不接受了30亿马克的国家补贴。30年后的1995年,这个补贴额已经达到了300亿马克,而到2020年,这个补贴额则达到了720亿欧元。“不允许用额外税款来填补养老基金”,这个由施莱伯提出的铁律,现在已经变成了一个笑话。
|
||||
|
||||
将国家用于支持养老保险的资金中的一小部分投资于资本市场,几乎也不会有任何作用。这方面的模范是瑞典:瑞典仅仅将2.5%的法定养老金缴款投到了股票和债券市场。虽然把养老保险的资金投资到资本市场上去的想法是由自由派人士提出并写到了德国联合政府文件上面的,但连同他们自己其实也并不相信,这种想法会对德国的法定养老保险制度有所帮助。毕竟,到2025年,德国用于补贴法定养老保险的税额就将超过1000亿。
|
||||
|
||||
### 天堂里的饥饿:进步的悖论
|
||||
完全自动化会继续发展,而它的进展越快,传统劳动就业社会需要的人力就越少。然而,拿工资的人越少,自动化经济下的产品消费者就越少
|
||||
|
||||
无条件基本收入的想法曾经在匮乏经济中以自由主义的形式萌芽,而现在所有这些自由主义的梦想,这些对有自由且有尊严的生活的渴望,都将在丰裕经济中得以实现。只要实施无条件基本收入,我们自然而然就能实现这一最终目标,不是吗?而我们唯一需要跨越的思维障碍就是传统劳动就业社会的历史残留。正如前文所言,传统劳动就业社会的出现与自由主义有着不可分割的联系。如果自由主义不想在21世纪和传统劳动就业社会一起灭亡,就需要向着自由迈出下一个大步,即转变为意义社会的自由主义。届时,它将承担起它在人类历史上的使命——松开连接就业与生存的最后那条绳索,让那些身处丰裕经济中的人民不再依靠就业而生存,从而化解列昂季耶夫的进步悖论。
|
||||
|
||||
将工作和收入分离之后,人们不需要任何形式的工作表现和工作贡献,其生存就可以得到物质上的保障。这种想法有很多名称——如“公民津贴”“最低生活保障”“土地分红”或“社会分红”。今天,它在德语国家被概括为无条件基本收入(BGE);在英语国家被称为基本收入保障(BIG)、无条件基本收入(UBI)或全民基本收入(UBI)
|
||||
|
||||
宣告天堂里存在饥饿的人,正是经济学家列昂季耶夫
|
||||
|
||||
在工业化国家中,任何不参与工作的人都需要有一个好的理由来解释自己为什么不去工作,而这个理由需要家长式的政府进行严格的审核与评估,除非这个人非常富有,不需要国家的资金来维持生计。这样一个国家,这样一个质疑每个人是否有充足理由不工作的国家,实际上应该让21世纪的每个自由主义者都感到愤怒。国家到底按照什么标准来判断不工作的理由是否合法?在一个富足的社会里,不论自己的年龄大小,有些人可能会选择旅行一段时间以获得经验和进一步的成长,这难道不是一个合法的理由吗?有些人可能会抽出更多的时间来反思自己的生活,可能决定离开职场去写书,去修复房子或照顾父母,这难道不都是合法的理由吗
|
||||
|
||||
这种不再将工作和生计、权利和成就必然联系在一起的想法,毕竟是对资产阶级雇佣劳动制度和绩效制度的一种根本性的干预。而大多数西欧人都有充分的理由认为,资产阶级雇佣劳动制度和绩效制度是一个史无前例的成功范例。难怪,人们并不愿意相信这两种制度将逐渐走向衰落,不愿意接受雷克维茨所提出的“幻想的终结”。但如果社会的繁荣和自由本质上是基于工作表现和职业道德的,那我们不是正在用无条件基本收入来锯掉我们“赖以栖息的枝干”吗?
|
||||
|
||||
与传统劳动就业社会相比,意义社会有其不同的价值标准。做一份工作并不等于做一份有意义的工作——这种想法创造了一种新的价值判断体系,即:做一份有意义的工作和不工作相比,哪种选择会更好?做一份无意义的工作和不工作相比,哪种选择又会更好?但不可否认的是,许多在传统劳动就业社会中长大的人,其实始终还是难以接受基本收入的无条件性
|
||||
|
||||
正如比利时哲学家和经济学家菲利普·范·帕里斯(Philippe Van Parijs)所写的那样,一个不工作的冲浪者对公共利益造成的损害很可能比一个高收入者通过奢侈生活带来的损害要小得多。
|
||||
|
||||
|
||||
只有在机会高度均等的条件下,绩效才会是一个合适的衡量标准,不是吗?如果有些人得到了家庭充分的供养,而另一些人却为了维持生计而被迫从事他们绝不会自愿从事的工作,那自主掌握自己人生的机会怎么可能会是人人均等的呢
|
||||
|
||||
国家不再以社会救济的形式来提供失业补助,而是普遍为每个公民提供绝对的生存保障,这种模式仍然被看作是对现有社会保障制度的一种挑衅
|
||||
|
||||
### 没有土地的人:基本收入的起源
|
||||
19世纪前三十年,“社会主义”一词首次出现,它包含两大基本原则:“资本不得统治劳动”以及“劳动不得成为保障生存的唯一来源
|
||||
|
||||
对斯宾塞来说,土地私有制是极其不公正的。“因为,谁拥有剥夺别人生命的权利,谁才拥有剥夺别人生活资料的权利。”
|
||||
对他来说,土地与我们呼吸的空气、太阳的光和热一样是生存的必要条件。而正因如此,他提出了相反的建议:市政当局应保留土地。市政通过土地获得的收益可以用来支付所有的市政支出,可以用来保障基础设施,可以用来为“穷人、失业者等有需要的人提供社会救济,从而保障他们的生存。”
|
||||
人人都有权利生存:任何人都不应该陷入物质上的困境!通过以上这个信条,斯宾塞将50年后社会主义的两项要求融合在了一起,这两条要求分别是:按现代企业制度对土地所有权进行管理,以及由市政当局提供普遍的生活保障。
|
||||
|
||||
人们完全忘记了这第二条原则,甚至连倍倍尔以及“苏联宪法之父”在内的社会主义者都提倡过保罗(Paulu)在《帖撒罗尼迦后书》中所说的那句老话:“不劳动者不得食。
|
||||
> 不劳而获可耻
|
||||
|
||||
而英国的济贫院是代表了社会的进步,还是代表了另一种精心策划的剥削,对于这个问题还需要进行更深入的讨论。
|
||||
|
||||
只是保护穷人免受饥饿,并不能等同于真正的无条件基本收入。以社会福利形式进行的扶贫如果附带上了工作的义务,那么它就无法像斯宾塞所要求的那样,赋予人们自由生存的基本权利。所以,荷兰及英国的济贫法、济贫院完全不能等同于斯宾塞的无条件基本收入制度
|
||||
|
||||
自然资源的权利。人类有权享用大自然的馈赠,有权狩猎、采集和放牧。因此,每个人都有权在饥饿时获得生存所需的食物……如果资本主义制度夺走了人类在自然界中获取生存所必需的四大支柱——狩猎、捕鱼、采集和放牧,那么那些剥夺了别人土地资源的阶级就有责任向被剥夺土地资源的阶级提供足够的最低生活保障。”
|
||||
|
||||
|
||||
孔多塞将两个几乎从未被联想到一起的想法结合了起来:即为所有无法养活自己的公民提供社会保险的想法;和潘恩一样,在所有公民成年时无条件地发放一笔资金来帮助他们自由、独立地进入理想职业生活的想法。打造一个提供独立谋生保障的福利国家——这真是一种全新的、革命性的组合方式
|
||||
|
||||
傅立叶梦想着一种全新的合作生产模式,这种模式能在最短的时间内使法国的繁荣程度至少翻三番。如果每个人都能追随自己的情欲
|
||||
,而不是以异化的方式开展工作,那么在正确的工作组织形式下,“下层阶级将成为中层阶级”,不满的工人阶级将成为知足的“小资产阶级”。此外,“当人民享有稳定的收入来源和适当的最低收入时,社会纷争的根源将被消除或至少减少到最低程度。”
|
||||
|
||||
|
||||
但如何让所有人都过上富足的生活呢?傅立叶的答案是,让每个人找到适合自己的生产和生活方式。我们必须释放“情欲”的力量,让每个人都从中受益。因此,在法伦斯泰尔中,每个人都可以从事他想做的工作。在19世纪初,这的确是一个乌托邦式的大胆要求。工作是为了自我实现,是为了满足自己的情欲,它与性行为并无不同。每个人都有创造和塑造事物的冲动
|
||||
|
||||
资本不得统治劳动”的想法仍然以这样或那样的形式存在于人们的脑海中,但“不依赖工作获得生存保障”的权利却渐渐被人遗忘
|
||||
|
||||
沙利耶所言:“人们对于财产的欲望往往是从拥有财产本身中产生的。”
|
||||
|
||||
|
||||
### 重新定义基本收入:工业进步背景下的社会乌托邦主义
|
||||
所有关于国家经济或基本收入的作品中,没有一本像《回顾》那样受到了如此广泛的关注。要想说服美国人,就得把自己的思想包装成一种具有远见卓识的乌托邦理念,就得表现出对世界的运行方式有清晰的认知,并承诺自己可以准确地预测未来。今天,向人们做出无数美好承诺的硅谷的做法也与贝拉米并无不同。此外,贝拉米还尽可能地避免使用“社会主义”一词,而是巧妙地用“民族主义”来代替它。在美国,民族主义一词不论是在过去还是现在,总是能引起人们的注意
|
||||
|
||||
和半个多世纪前的比利时人一样,林扣斯想把“必要的经济”国有化,只把“额外的经济”留给市场。而相比沙利耶,林扣斯的划分则更为严格,他提出:在开始工作后的13年(男性)或8年(女性)里,所有青年男女都要在负责保障“必要经济”的“生产队”中提供义务服务。青年人所提供的“必要生活物资”将为所有公民提供终身的、以物资形式发放的“最低生活和生存保障”。因此,没有人会再陷入贫困。食物由公共食堂统一分配,衣服和生活用品也由国家提供,住房则按需分配。除了“生计保障”以外,国家还以津贴的形式提供第二层保障,即“文化”方面的保障。此外,任何人都有权通过“额外经济”满足他们的特定需求。
|
||||
|
||||
理想的国家是无政府状态的,就像俄国无政府主义者米哈伊尔·巴枯宁(Michail Bakunin)和彼得·阿历克塞维奇·克鲁泡特金(Pjotr Kropotkin)设想的那样。在这里,不需要金钱,不需要婚姻,不需要学校,也不需要法院和监狱。人们之所以能实现这一切,是因为人们选择了远离大城市,选择了与大自然和谐相处
|
||||
|
||||
这种既能满足基本需求又能创造后天需求的经济模式并没有在他所在的地区,而是到了20世纪70年代末的中国才得以试行
|
||||
|
||||
在多种多样的基本收入概念中,迄今为止只有它被贴上了“右翼民粹主义”的标签。自此,对无条件基本收入的需求除了红色以外又有了第二种颜色。而第三种颜色也很快随之而来,即自由主义的蓝色。
|
||||
|
||||
他认为,社会主义的可取之处在于,它可以比资本主义更自由、更客观地评判劳动的价值,而资本主义则是以剥削和利润最大化为导向。在资本主义社会,富有的闲人可以比贫穷的工人得到更多的尊重。但罗素也看到了社会主义的极权主义统治倾向,他对此深恶痛绝。无政府主义吸引他的地方在于自由的理念,它比所有其他社会理论都更加无条件地提倡自由
|
||||
|
||||
### 工具还是基本权利:自由主义的基本收入
|
||||
弗里德曼这位芝加哥男孩认为,这个倡议从“纯技术的角度而言是一项有意义的举措”。
|
||||
负收入税不会增加富人的纳税负担,还能明显地减轻“行政负担”
|
||||
|
||||
只有当人们即使不参与劳动社会,也不会成为社会耻辱的时候,个人自由才会得到充分的保障
|
||||
|
||||
你是否承认基本收入是一项基本权利?正如达伦多夫所说,如果认为基本收入不是一项基本权利,那就意味着公民权利被“缩减”了。市场创造了失业,那它也必须消除失业。如果无法消除失业,国家就必须建立一个像“负收入税”这样的杠杆。但如果实施“负税收”,就又“留了一道能够取消公民所有权利保障的口子。我们甚至可以推论,负收入税制度通过一个侧门,又会将劳动社会制度偷偷运送回社会系统。如此一来,就只有税收体系内的公民才能得到权利的保障。”而与此相反,如果认为基本收入是一项基本权利,那就意味着基本收入首先不是一种工具,而是现代国家基本权利目录的延伸。这就要求我们,“首先必须确定公民权利,然后再确定满足这些权利的方法”。
|
||||
|
||||
|
||||
是赋予所有公民一项基本权利还是通过税收来为他们提供支持和保障,这两者即使在经济上具有相同的效果,但在本质上却是无法等同的
|
||||
|
||||
达伦多夫毫不掩饰地表示,他不接受将无条件基本收入仅仅视为一种工具,而只接受将其视作一项基本权利:“必须承认无条件基本收入是公民权利的一个基本组成部分,因为基本收入的意义就在于为所有人确定一个共同的起点,从而保证每个人都不会掉队。”
|
||||
这就是“小”基本收入(通常是“负所得税”模式)与“大”基本收入(无条件基本收入作为一项基本权利)的区别所在
|
||||
|
||||
如果参与劳动社会不再是衡量公民权利的标准,那么国家就不能像现在这样限制受助者的基本权利。达伦多夫认为,劳动社会凌驾于自由生存的基本权利之上,这才导致受助者的基本权利受到了限制
|
||||
|
||||
因为,如果想要“明确所有人共同的生存基础,那么就确实有必要将收入与工作脱钩”。而要实现这一点,无论是依靠纯粹的社会救济,还是恢复“不劳动者不得食”的做法,都是远远不够的。这些措施都只不过是创造一个宜居社会的必要条件,除此之外要完成的事情其实还有很多,尤其是在工作(分配)方面。和其他公民权利——例如法律面前人人平等的权利或普遍、平等的选举权利——一样,最低收入保障也是一项不可或缺的公民权利。
|
||||
|
||||
|
||||
### 今天的基本收入:实现无条件基本收入的现实要素
|
||||
如果失业的人越来越多,那么针对劳动所得征收的个人所得税就无法再负担得起福利国家的支出。负所得税从来就不是为应对第二个机器时代的挑战而设想的方案,因此它也解决不了这个时代所面临的问题。
|
||||
|
||||
社民党主席萨斯基娅·艾斯肯(Saskia Esken)曾说过一句话:“有了基本养老金以后,许多达到退休年龄的人才终于得到了对其一生成就的认可。”
|
||||
她的这句话听起来很是悲怆,但悲怆中又带着不由自主的讽刺
|
||||
|
||||
既然软件公司的营业额如此之高,那我们也可以对软件公司征收更多的税款,不是吗?问题的关键只在于,生产税如何定义“机器”这个概念。为什么我们只对硬件,而不对生产所需的软件征税呢?此外,为什么我们不将这类税收命名为“技术税”,而选择了“机器税”这么具有误导性的名字呢?
|
||||
|
||||
因此,要想通过无条件基本收入来增加每个公民的自由,就必须只能以个人而不能以家庭为单位。而且,基本收入的金额不得低于目前德国哈茨四救济金领取者所能拿到的最高金额
|
||||
|
||||
像德国这样世界上最富有的国家之一,却只给数百万老年人提供不到1000欧元每月的养老金,这不是一个运作良好的福利国家该有的表现。相反,拿劳动人民的钱一次又一次地对现有的福利国家制度缝缝补补,这并不能使这个制度变得更完美,而只会让它更加漏洞百出。与此同时,福利国家制度背后的问题仍在急剧增长。如前所述,养老金已经无法再以传统的方式进行融资了。在这种情况下,无条件基本收入承诺了一种全新的模式。根据这种模式,每个德国公民可以领到的基本收入不应该仅仅是1000欧元及以下。相反,无条件基本收入的金额必须明显高于基本养老金,并远远高于以前的哈茨四救济金,即应该达到每月1400欧元或1500欧元。
|
||||
|
||||
我们现有的社会保障体系里有许多成就都曾被嘲笑是冒险的、天真的,比如不再使用童工,为所有儿童提供义务教育、法定养老保险和医疗保险、商业养老保险、社会福利、儿童金等。在19世纪中期,人们甚至都无法想象这些要求能成为现实,因为这些人道主义的想法总被认为是离经叛道的理想,是在煽动社会动乱,而且关键在于——人们总是认为,国家完全无法负担这样的要求与想法。因此,这些想法在当时肯定也受到了绝大多数经济学家的反对。但现在,这些经济学家却要一次又一次地重蹈覆辙。
|
||||
|
||||
在自动化红利的基础上创造社会红利可行吗?事实上,这种征收生产税的模式已经被反复多次讨论,并且已经有大量的研究对其进行了全面的估算。不对劳动者征税,而对所有的生产行为征税。唯有如此,才能判断出企业的经济实力和盈利能力,从而确定其应交的税额。在第二个机器时代,这个想法以及这个模式得到了前所未有的重视。就连比尔·盖茨这样通常不太可能会支持征税的人,也在2017年提出了征收机器税的建议
|
||||
。然而,机器税只是生产税的一种最基本的形式,或者说是简化版的净生产税。因为生产不仅会涉及工资支出、企业应缴税款和机器的使用,还会涉及机器、建筑物和设备的折旧、借贷利息、租金以及各种各样的补贴。
|
||||
|
||||
想要适应未来社会就必须脱离传统的思维方式,不再通过(或者至少不主要通过)征收个人所得税来为无条件基本收入提供资金。因为,如果我们继续走这条道路,我们迟早将会陷入和今天的福利国家制度一样的困境。为此,维尔纳提出了几个著名的替代性方案:比如,只对消费而不对收入进行征税;对自然资源,特别是土地资源征税;对二氧化碳排放或对环境污染征税(庇古税)。这些建议各有利弊。事实上,消费税对有钱人来说不构成多少影响,但却会给那些经济并不宽裕的人带来负担。像潘恩提议的那样对土地资源征税,似乎也不再合乎时宜。并不是所有拥有大量土地的人都能支付得起高额的税款,因为并非所有土地都能产生相应的收益。虽然现在已经引入了碳税,但可持续发展革命也急需这笔钱。
|
||||
|
||||
相较传统的劳动就业社会,21世纪意义社会的自由主义和左派思想极有可能会变得越来越接近。但自由主义提倡尽可能地提高市场和企业的自由度,而左派则强调尽可能地提高工作保障和实现公平分配,这两者之间仍然存在不可调和的矛盾。未来的口号是:我们要将生存保障视作公民的基本权利,从而实现公民的自由
|
||||
|
||||
首先,必须确定未来无条件基本收入的金额以及这个金额是否确实能改善转移支付接受者、失业者、救济金领取者和退休人员的收入状况。目前在德国,单身人士领取失业救济金的标准是446欧元。此外,根据地区不同,还有390~590欧元不等的房租补贴,以及约130欧元的医疗、护理和养老保险补贴。如果把热水费或搬家费等小额补贴也算在内,那么德国每个领取救济金的单身人士根据其所属地区每月可领取到950~1200欧元不等
|
||||
|
||||
因此,比较值得考虑的是征收生产税或机器税。这两个概念从第一次工业革命时代就已经存在了。20世纪40年代十分流行“经济红利”。因此,早在1942年,美国科幻作家罗伯特·海因莱茵(Robert A. Heinlein)就已经在他的小说《地平线之外(乌托邦2300)》[Beyond this Horizon(Utopia 2300)]中提到了普遍征收生产税或机器税
|
||||
。现在,这种想法不是已经有可能实现了吗?我们为什么只对工作的人征税,而不对工作的机器,如拖拉机、煤炭挖掘机、印刷机以及越来越多的计算机和机器人征税呢?他们不是也创造了巨额的价值和利润吗?难道就不能像海因莱茵所描述的那样,由它们来为我们的退休金和基本收入提供资金支持吗?
|
||||
|
||||
这种模式的优势是显而易见的。决定税收的不再是雇员的数量,而是企业的业绩。而且,税基的范围越是广泛,需要征收的个人所得税也就越少。这样一来,社会保障基金就不再仅仅依赖于就业,其收入的来源就变得更加广泛。就算人类在第二个机器时代被计算机和机器人所取代,法定养老金也几乎不会受到影响;这与现行制度相比是一个多么关键的优势啊
|
||||
|
||||
工会一直努力改善就业环境,想要为劳动者争取更多的空闲时间,但同时他们也担心就业者的空闲时间过多以后,就业可能就会变得更加松散,不再那么依赖社会体制。而像计算机和机器人这样的非人类劳动者则根本就不需要工会…
|
||||
|
||||
苏德库姆认为,不应该征收任何税费,而应该让公司员工直接从机器人身上获得收益。如前所述,这个想法在德国已经因理查德·弗里曼(Richard Freeman)的推广而广为人知
|
||||
|
||||
世界各国此时都面临着一个相同的问题:如何阻止中产阶级的阶层跌落?如何防止出现剧烈的社会动荡?鉴于这种威胁的存在,目前看来完全是乌托邦的目标很快就有可能成为现实。推动社会进步的从来都不是各种观点与论据,而是人们激动的情绪以及社会性的灾难
|
||||
|
||||
推行生产税的国家可想而知会面临巨大的竞争劣势。欧盟
|
||||
|
||||
为了能够向德国所有公民支付无条件基本收入,德国的国民经济就必须蓬勃发展,德国就必须保持很高的生产力水平。德国企业的自动化程度越高,其工资成本就越低,利润就越高。然而,如果向他们征收技术税,就会阻碍他们的自动化进程
|
||||
|
||||
如果我们将无条件基本收入的融资方式从征收个人所得税转变成征收生产税,并对每笔金融交易征收小额税,那么不论是雇员还是雇主都很可能从中获得巨大的利益。毕竟,对每笔金融交易征收0.05%的小额税,就已经足以为瑞士的无条件基本收入提供资金支持。如果德国想要建立一个理想的、类似的模式,我们可以进行如下计算:德国的金融交易额为275万亿欧元以上,再加上对日常资金往来征收税款带来的收益。算下来,德国如果征收0.4%的小额税就能为整个德国提供无条件基本收入所需的资金
|
||||
|
||||
苏德库姆的这个想法乍听之下确实很吸引人。如果机器人变成了劳动力的一部分,那靠机器人获得收益不是要比靠生产税获得收益直接得多吗?但如果我们仔细思考就会发现,这种提议包含两个重大隐患:首先,公司员工能从机器人身上获得的分红可能非常有限,因此他们也谈不上真正对这些机器人拥有所有权。其次,这个建议使员工的收入与公司的效益直接挂钩。如果公司倒闭了,员工也得承受损失,而且这个损失会远超失业带来的损失。为什么要把劳动者的命运直接绑定在一家公司上,而不用广泛而普遍的技术税来保障他们的未来呢?
|
||||
|
||||
### 自由、可持续性和制度变革:左派人文主义的基本收入制度
|
||||
德国政府现在迫切希望能够保障每个人的生存。而一个需要800多亿欧元来补贴的、现收现付制的养老金体系,到底还算不算得上是现收现付制呢,抑或它其实已经将代际契约和国家养老金杂糅在一起变成了一个奇怪的混合体?
|
||||
|
||||
20世纪90年代初,确实没有地方能容下改进、改变、改造甚至废除资本主义的想法了。这种想法甚至几乎都没有人可以理解。为什么要改变一个在世界历史上明明已经成为获胜者的制度?美国哲学家弗朗西斯·福山(Francis Fukuyama)所宣告的“历史的终结”(该宣告后来又被撤回)塑造了20世纪90年代的时代精神
|
||||
|
||||
还有谁会关注左派在20世纪80年代提出了哪些关于全球发展的展望呢?还有谁会关注如何解放人类并创造一个更公正的世界呢?这种低迷的情绪一直持续到21世纪
|
||||
|
||||
取消失业救济金、法定养老金、社会福利、儿童津贴、对赡养义务承担者的税收减免、助学金、促就业措施、对次级劳动力市场的国家补贴以及对经营困难企业提供的国家援助,转而每月支付给每个公民一笔足以满足个人基本生活需求的资金。无论他是否工作,是贫穷还是富有,是独自生活还是与家人生活,是否已经婚配,也无论他过去是否曾工作过……他都有权获得这笔资金。同时,废除现行劳动力市场上的所有规则,以及有关最低工资和最长工作时间的所有法律规定。
|
||||
|
||||
引入无条件基本收入以后,国家就不能再用传统劳动就业社会的条条框框来衡量人们的生活,所以家长式国家带来的压力也会随之烟消云散,人们会变得更加独立,也更加自由。
|
||||
> 这正是很多国家不愿意这么做的原因所在
|
||||
|
||||
### 懒惰的其他人:对无条件基本收入的人类学反对意见
|
||||
无条件基本收入可能会导致懒惰的泛滥,可能会让“罗马帝国晚期的衰败景象”
|
||||
再现之类的说法,其实并没有什么客观依据
|
||||
|
||||
沙利耶也在19世纪30年代认识到了这一点:人是懒惰还是勤劳,并不取决于人的天性,而是取决于他们做什么工作以及为什么工作。如果能找到一个可以给自己带来满足感的职业,人们就会乐意去工作
|
||||
|
||||
无条件基本收入修复了劳动社会中这一严重的运作缺陷。目前,几乎还没有任何其他措施能够做到这一点。而这恰恰可能是今天许多“社会杰出贡献者”并不欢迎这一举措的原因。毕竟,不是每个人都希望对他们在社会上的贡献进行一番现实的考量
|
||||
|
||||
现在,我们已经在很大程度上将教育从先前几十年甚至几个世纪的压力以及恐惧中解放出来,现在轮到消除对工作的恐惧了
|
||||
|
||||
有了无条件基本收入以后,就不会有人因为养老金不够花而不得不去开出租车了。如果他们还是想去开出租车,那是因为他们乐意这样做。而且,医院护士和老年人护工终于也能拿到合理的报酬了。现在,旅游业中约有60%的工作被认为报酬过低,而有了无条件基本收入以后,一些服务也会变得更加昂贵,例如理发和餐饮,因为他们现在必须给服务人员支付更高的工资
|
||||
|
||||
低工资行业相比今天必定会发生一些变化。一方面,他们的工资必须大幅提高;另一方面,工作条件也必须得到改善
|
||||
|
||||
### 要给百万富翁发钱吗:对无条件基本收入的社会学反对意见
|
||||
除了绩效社会的核心假设被颠覆,许多人甚至也无法再像以前一样用金钱来衡量自己的人生成就与价值
|
||||
|
||||
模式。他也没有考虑到,未来会有越来越多高度自动化的机器人来代替人类促进社会的发展与繁荣。可怜的被剥削的机器人啊——至少还有人怜悯他们,真好。
|
||||
|
||||
给每个人发放1000欧元对罗马尼亚来说是一个从天而降的、具有欺骗性的礼物,有马上引起通货膨胀的风险,但这个金额对德国来说却太少了。泛欧洲的无条件基本收入听起来确实很人性化——但如果基本收入这根枝丫承载了过多的理想主义,那这根枝丫就无法生长得很强壮,最后就只能迎来断裂的结局。这样做对基本收入没有任何好处。
|
||||
|
||||
给富人发补贴有害无益的说法只不过是只一捅就破的纸老虎,但许多工会会员、社会民主党人和一些左派人士似乎还是认为,这种说法非常重要,他们不能置之不理
|
||||
|
||||
现在终于有了无条件基本收入这样一个替代性的方案,它以小额税和生产税作为资金来源,要求全面重组福利国家
|
||||
|
||||
那么我们如何应对可能出现的大量移民呢?毕竟,资本朝哪儿流动,人群就会朝哪儿流动。事实上,确实有必要修订相关的欧盟法律,从而给移民设置更高的门槛
|
||||
|
||||
而如果有谁对提高移民门槛的想法不满,大可争取在其他欧盟国家也引入无条件基本收入,这样葡萄牙人和保加利亚人也就能够拥有属于他们的无条件基本收入了
|
||||
|
||||
简而言之,我们夸大了我们的恐惧,低估了我们的适应能力。正是由于这种心理,才总是会有人在每一次的重大政治变革中表示反对。
|
||||
|
||||
另外一个截然不同的反对意见则显得更加重要,即无条件基本收入是否破坏了来之不易的“以需求为基础”的福利国家制度,却没有提供一个同等或更好的制度来代替它?也就是说,无条件基本收入的出现只不过是新自由主义发起的一场攻击,是一种类似于特洛伊木马的手段
|
||||
|
||||
我们就有必要分析一下那些从左翼和中左翼的角度攻击无条件基本收入的人的心理。总结起来至少有三个方面的因素:第一,他们担心长达150年劳工斗争的成果遭到破坏。这也是可以理解的,我们可以称其为损失厌恶。第二,他们的思维模式较为保守。毕竟几十年来,他们一直都在传统劳动就业社会中为制定和实施职工政策中所遇到的困境而伤脑筋,所以他们无法想象还能有什么替代方案。我们可以称其为路径依赖。第三,他们担心基本收入社会不仅不需要工会,而且也不需要任何劳工斗争和职工代表。社会民主主义和传统左派的社会使命将不复存在。我们可以称其为意义的丧失。
|
||||
|
||||
我们的祖父和曾祖父那一代人在两次荒诞且愚蠢的世界大战中白白送死,这公平吗?他们必须比我们工作得更久更辛苦,这公平吗?无数母亲被战争夺走了她们的孩子,她们在社会上几乎无依无靠,这公平吗?这一切无疑都是不公平的。但谁会就此得出结论说,因为上一代人经受了苦难,所以下一代人就不应该享受更好的生活呢?谁又会得出结论说,既然上一代人在战争中殒命或者工作到半死,那现在这一代人不应该也得经受这些才公平吗?一个现年九十岁的老太太在当年的解放运动中并不能像现在的年轻人一样从中受益,难道她就应该为此而去反对解放运动吗?或者说,我们其实是不是应该为这么多人能过上比以前更好的生活而感到高兴呢?因此,利用自己过去受过的苦来证明后代不应享受无条件基本收入,其实并不是一个好的理由。
|
||||
|
||||
社会心理学告诉我们,当谈到变化时,人们往往有一种倾向(一种偏见),那就是总是关注负面的东西。对大多数人来说,对损失的恐惧远远会超过对收益的喜悦与想象。但另外,虽然人们在面对变化时总是激烈地表示反对,但他们往往又能很快地适应并接受这些变化
|
||||
|
||||
### 谁来付钱:对无条件基本收入的经济学反对意见
|
||||
所有这些都表明,在德国引入无条件基本收入可能并不会引发通货膨胀,相反它可能会大大提高德国国内市场的活力
|
||||
|
||||
在过去的几十年里,像德国这样强大的经济体主要面临的是通货紧缩的威胁,而非通货膨胀的威胁。换句话说,德国生产的商品数量已经超过了民众的购买力,出现了供大于求的现象。其结果可想而知。一个国家的生产力越高于其国内的购买力,那这个国家就会越依赖出口。另外,国家和个人的负债也可能人为地让这种现象火上浇油,美国就是其中一个非常明显的例子。但是,如果未来因裁员而导致购买力急剧下降,又会发生什么呢?那到时候,我们岂不是无论如何都得用无条件基本收入来提高购买力吗?所以只要你愿意,你甚至可以把基本收入看作是经济因素影响下的必然结果。这里的经济因素指的是,消费者越来越变成产销合一者,或者说变成生产性消费者,他们作为“既生产又消费的顾客”,完成许多以前由商家负责完成的服务。从这一点看来,无条件基本收入就可以算作企业把这些工作外包给顾客之后发给顾客的一次性报酬。
|
||||
|
||||
技术和经济的进步使这一切成为可能,也使福利国家制度成为可能。每一项新税收和每一次经济增长,最初都被认为是无法想象的、不公平的、危害经济的,或被认为是异想天开的。到最后,真正妨碍税收的从来不是那些实际存在的困难,而只是因为游说团体所做的游说或者是因为人们缺乏足够的政治意愿
|
||||
|
||||
引入基本收入可能对社会产生的一个非常糟糕的后果就是物价的急剧上涨,例如租金的迅速攀升。而物价一旦上涨,就会抵消掉基本收入带来的经济收益
|
||||
|
||||
还有一种论点认为,金融投机者总是有足够多的方法来规避小额税。但即便如此,经济学家们还是有必要努力寻找方法来尽可能地提高他们避税的难度。众所周知,警察也不会因为犯罪一直在发生就放弃打击犯罪。
|
||||
|
||||
虽然直到最近,欧元区的通货膨胀率还很低,但却出现了资产通胀,并确实带来了一些切实的后果。哪里有大笔资金投入,哪里的价格就会上涨。这一点,我们只用看看住房市场、艺术品市场或几乎一直在上涨的黄金价格就会明白
|
||||
|
||||
### 实验中的证据:为什么模拟实验没有多大用处
|
||||
总结下来就是:如果有谁认为,有了基本收入以后,公民可能就不会再继续对自我负责,那他其实就是割裂了公民的自主行为能力和自由、自我责任之间的联系,转而把自主行为能力与传统劳动就业社会联系起来,认为如果外部环境不要求人们必须工作,人们就不会想去工作,而不想去工作其实就是一种对自我和社会的不负责任
|
||||
|
||||
### 从乌托邦到现实:基本收入是如何实施的
|
||||
也正因此,面对这场伟大的变革,我们已经没有更多的时间可以浪费了。当裂缝最终无法再修补时,就为时已晚了
|
||||
|
||||
德国新任联合政府也只是打算给原来的哈茨四救济金贴上“公民津贴”这个标签,而从本质上来看它仍然和以前一样,并不是真正的公民津贴
|
||||
|
||||
我们所面临的危险是极其巨大的:右翼民粹主义、对国家的不满、仇恨和蔑视已经明确地向我们发出预警,美国和许多西欧国家都可能会发生经济震荡。华盛顿国会大厦的示威者其实就是当代的卢德主义者。那些被误认为是在争取身份认同的人,其实往往都只是在坚持旧的经济秩序,坚持这种经济秩序下的文化特性,从而避免被进步的风暴所席卷。人们如果无法从新的经济秩序中获利,如果感到被抛弃,那他们很快就会无法理解这个世界,就会建立一种简单粗暴的思维模式来逃避现实,并慌乱地指控一些他们早就认为是幕后操纵者和罪魁祸首的人——他们尤其喜欢指控那些比他们生活得更好的人,比如自诩聪颖的社会精英以及被收买的政客。他们也喜欢指控那些生活得不如他们的人,尤其是那些拥有其他文化意识形态的外来移民。
|
||||
|
||||
卢德主义者,即19世纪初持反机械化观点的人,他们和纺织工人起义,特别是1844年的西里西亚纺织工人起义一样,都深深地留在了人们的记忆中。如果工业革命的好处只惠及少数人,那社会冲突就不可避免
|
||||
|
||||
从2019年春季开始,意大利就引入了公民津贴,单身人士可以领到780欧元,夫妇可以领到1280欧元。只要能证明自己需要这笔钱就可以申请该津贴。但意大利引入该津贴并不是为了适应社会和经济结构的变化,而是和过去一样为了消除贫困和刺激国内经济——即便如此,该津贴的引入也确确实实可以被看作谨慎地迈入基本收入社会的第一步,即将工作与生活分离的第一步
|
||||
|
||||
悲观主义者的判断往往只是部分正确的,而乐观主义者通常却能获得整体的胜利
|
||||
|
||||
如果工业化国家确实迟早会变成一个由“有特权的有业者阶级”掌握的社会,社会中相当一部分人口被边缘化,并沉浸在越来越精妙的数字娱乐中,那么就有可能会爆发严重的社会危机
|
||||
|
||||
往往是那些捍卫当前现状并认为重大改变不可能发生的人,才会将自己标榜为现实主义者。而那些质疑传统和已知事物的人,都被认作怪人或空想家。但从历史的长远角度来看,往往正是那些未能认清时代发展走向的、所谓的现实主义者,最终变成了怪人,而一些空想家最终则变成了更伟大的现实主义者
|
||||
|
||||
它清楚地向我们表明,我们所熟知的现收现付制度已经不可行了。未来,我们需要寻找代际契约以外的方式来为养老金融资——事实上,我们现在就应该这么做
|
||||
|
||||
基本收入制度诞生于经济匮乏的年代,却在经济富余之时找到了适合自己的位置
|
||||
|
||||
### 自主学习:21世纪的教育
|
||||
在距离意义社会到来还有两个世纪的时候,威廉·洪堡就已经认识到,工作本身并不是目的,所以一直以来新教对工作的认知都是错误的。威廉·洪堡认为,想要工作和必须工作只是人类天性的一个重要组成部分。另外还有一些其他的部分也同等重要,例如社交、休闲和享受
|
||||
|
||||
教育的目标更主要是在于让人们拥有充实的生活,而充实的生活往往也包含劳动与就业
|
||||
|
||||
因为没有人能准确地知道未来会发生什么,所以在我们的学校里,重要的不再是教会孩子们一些具体的、细节性的知识,重要的也不再是给相同年龄段的孩子讲授完全相同的知识。更重要的是,我们要成功赋予孩子们自主学习的能力。威廉·洪堡的梦想是,让孩子们在学校里学会如何学习。现在,这个梦想比以往任何时候都更具现实意义。相比过去,我们越来越难以判断,我们的孩子到底需要什么才能成功应对未来的生活,才能找到生活的价值和意义。既然如此,那我们就更加有必要帮助他们学会自主学习并设定自己的目标。
|
||||
|
||||
### 保持好奇心:当代教育学的目标
|
||||
我梦想未来有一天能创办一所学校,让年轻人能够在趣味中学习;这所学校鼓励他们提出和讨论问题;在那里,他们可以带着问题学习,他们可以不必听那些已经写好的答案;在那里,学习不是为了通过考试,而是为了学到东西。”
|
||||
|
||||
真正的教育并不是靠竞争。虽然通过竞争也能实现教育的目标,但人们接受教育终究是为了自己,而不是为了与他人竞争。如果教育并不等于“讲授教学资料”,而是一种对自我的塑造,那么现在这种考试、分数、家庭作业无止无休的模式就不符合威廉·洪堡所提出的“尽可能让所有人都接受教育”的理想
|
||||
|
||||
理论上,数字时代丰富的学习机会也为那些教育欠发达地区的儿童提供了新的机会。然而,现实却给我们泼了一盆冷水,社会差距不仅没有缩小,反而在扩大。
|
||||
|
||||
学会容忍矛盾而不觉得被冒犯,学会承认错误,应对失败,学会承认自己缺乏安全感,学会独立承担,学会激励和支持他人
|
||||
|
||||
创造性、好奇心、主动性和集体意识——当没有办公室生活,也没有其他工作等着你的时候,这些不就是不可或缺的能力吗?稳定的自我意识、对自己的目标持有期待,以及对人类持有积极乐观的态度——无论你做什么,这些东西在生活永远都是有用的,不是吗?工作上有用的东西在换工作时也同样会有用
|
||||
|
||||
在知识社会,知识的半衰期不断缩短,因此,知识的有效期也在缩短。
|
||||
|
||||
纵观人类历史,人们可能并不能断言,有了教育以后,社会相对而言就会变得更加和平。毕竟,稍稍回顾一下20世纪德国发动的两次大规模战争就可以清楚地知道,情况确实并非如此
|
||||
|
||||
教育是一种基本装备,它帮助人们应对生活,实现自己设定的目标,而这其中也包括应对失败
|
||||
|
||||
根据过去几十年的经验,公司和组织机构在招聘新人时越来越不看重形式上的学历标准。这一现象的出现不仅仅是因为学历通胀,也是因为这些踌躇满志的学生们过于追求学业,反而导致他们在生活经验方面有所欠缺
|
||||
|
||||
### 十二条原则:未来的学校
|
||||
如果有些学校能够像全球的一些精英学校一样,发展成全方位的教育机构,下午和晚上提供体育、舞蹈和戏剧课程,并且还能提供工作室和节庆活动场所,那就再好不过了。这样一来,学校教育就能回归到它应该到达,但从未到达的地方,即回归到生活中去
|
||||
|
||||
第七条原则是建设有利于学习的学校建筑。大多数传统的学校建筑仍然让人联想起医院、税务局或军营
|
||||
|
||||
好的老师都是好的讲述者,他们能够吸引学生,能够激发学生的热情,让学生们都很乐意听他们讲话
|
||||
|
||||
无论我们把这种训练称作“幸福”训练,称作“生活的艺术”“自我反思”还是“哲学”训练,其实都没有太大关系
|
||||
|
||||
在人类历史上,儿童和青少年的大脑从未像今天这样被如此多的刺激所冲击。难怪许多儿童会对此感到不知所措,以至于失去了躲避这些刺激的能力
|
||||
|
||||
第五条原则就是我们要在我们的学校中建立一种关系和责任文化——这不能仅仅只是讲讲空话,而是必须以一定的组织形式来实践。在
|
|
@ -0,0 +1,308 @@
|
|||
|
||||
murl = ""
|
||||
#########################################################
|
||||
## @file : parseweb.py (refactored)
|
||||
## @desc : Douban & Amazon book spider (Douban primary)
|
||||
#########################################################
|
||||
|
||||
import requests
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from html import unescape
|
||||
from urllib.parse import quote
|
||||
|
||||
logger = logging.getLogger()
|
||||
if not logger.handlers:
|
||||
logger.addHandler(logging.FileHandler('log'))
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
ISDOUBAN = 1
|
||||
IMGPATH = './downimg'
|
||||
LINKPREF = 'https://book.douban.com/subject/' if ISDOUBAN else 'https://www.amazon.cn/s?k='
|
||||
|
||||
mheaders = {
|
||||
'Host': 'www.douban.com',
|
||||
'Referer': 'https://www.douban.com',
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'
|
||||
}
|
||||
|
||||
mparams = {}
|
||||
murl = ''
|
||||
if ISDOUBAN:
|
||||
mparams['Host'] = 'www.douban.com'
|
||||
mparams['search_text'] = 'bkname_xxx'
|
||||
mparams['cat'] = '1001'
|
||||
mparams['k'] = 'bookname_xxx'
|
||||
murl = 'https://search.douban.com/book/subject_search'
|
||||
else:
|
||||
mheaders['Host'] = 'www.amazon.cn'
|
||||
mheaders['Referer'] = 'https://www.amazon.cn'
|
||||
mparams['Host'] = 'www.amazon.cn'
|
||||
mparams['k'] = 'bkname_xxx'
|
||||
mparams['i'] = 'stripbooks'
|
||||
mparams['__mk_zh_CN='] = '亚马逊网站'
|
||||
mparams['reg'] = 'nb_sb_noss'
|
||||
murl = 'https://www.amazon.cn/s'
|
||||
|
||||
TEST_BOOKS = [
|
||||
'24堂财富课','甲骨文','庆余年(精校版)','商君书-中华经典名著全本全注全译丛书','苏世民:我的经验与教训',
|
||||
'杨伯峻_论语译注','小窗幽记','少年凯歌','投资要义','白鱼解字','历史的巨镜','货币的教训','钱从哪里来',
|
||||
'中国古代简史','罗马人的故事(套装共15册)','改变心理学的40项研究','如何假装懂音乐','管子(上下册)',
|
||||
'投资中最简单的事','薛兆丰经济学讲义','枪炮、病菌与钢铁:人类社会的命运','中央帝国的哲学密码','新编说文解字大全集',
|
||||
'市场的逻辑(增订本)','金融的本质:伯南克四讲美联储','从零开始学写作','中国国家治理的制度逻辑','中国为什么有前途','日本的世界观'
|
||||
]
|
||||
|
||||
class BookInfoSpider:
|
||||
if ISDOUBAN:
|
||||
re_bn = re.compile(r'class="nbg.+?sid: (\d+?),.+?title="(.+?)".+?img src="(.+?)"')
|
||||
re_star = re.compile(r'^<span class="allstar(\d+)"></span>')
|
||||
re_score = re.compile(r'class="rating_nums">(.+?)<')
|
||||
re_ratenum = re.compile(r'^<span>\((\d+)人评价\)</span>')
|
||||
re_author = re.compile(r'class="subject-cast">(.+?)<')
|
||||
re_description = re.compile(r'^<p>(.+?)(</p>){0,1}$')
|
||||
else:
|
||||
re_asin = re.compile(r'^<div data-asin="(.+?)" data-index')
|
||||
re_img = re.compile(r'^<img src="(.+?)"$')
|
||||
re_bn = re.compile(r'^alt="(.+?)"$')
|
||||
re_author = re.compile(r'^<div class=.+auto"><\/span>.+$')
|
||||
re_rate = re.compile(r'^<span aria-label="(.+?)">$')
|
||||
re_end = re.compile(r'^<span class="a-letter-space"><\/span><\/div><\/div>')
|
||||
|
||||
def _douban_suggest(self, query: str):
|
||||
"""调用豆瓣 suggest 接口返回列表[{id,title,url,pic}]"""
|
||||
session = requests.Session()
|
||||
session.headers.update(mheaders)
|
||||
# 先访问主页获取 cookies
|
||||
try:
|
||||
session.get('https://www.douban.com/', timeout=10)
|
||||
except:
|
||||
pass
|
||||
url = f'https://book.douban.com/j/subject_suggest?q={quote(query)}'
|
||||
try:
|
||||
r = session.get(url, timeout=8)
|
||||
if r.status_code == 200:
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.debug(f"suggest error {query}: {e}")
|
||||
return []
|
||||
|
||||
def _fetch_subject(self, sid: str):
|
||||
url = f'https://book.douban.com/subject/{sid}/'
|
||||
try:
|
||||
r = requests.get(url, headers=mheaders, timeout=10)
|
||||
if r.status_code == 200:
|
||||
return r.text
|
||||
except Exception as e:
|
||||
logger.debug(f'subject fetch {sid} error: {e}')
|
||||
return ''
|
||||
|
||||
def _extract_description(self, html: str) -> str:
|
||||
if not html:
|
||||
return ''
|
||||
m = re.search(r'<meta property="og:description" content="(.*?)"', html, re.S)
|
||||
if m:
|
||||
return unescape(m.group(1).strip())
|
||||
m = re.search(r'<div class="intro">(.*?)</div>', html, re.S)
|
||||
if m:
|
||||
raw = m.group(1)
|
||||
ps = re.findall(r'<p>(.*?)</p>', raw, re.S)
|
||||
txt = '\n'.join(unescape(re.sub(r'<.*?>','', p)).strip() for p in ps)
|
||||
if txt:
|
||||
return txt
|
||||
return ''
|
||||
|
||||
def grab_book_info_new(self, bookname: str):
|
||||
if ISDOUBAN != 1:
|
||||
return None
|
||||
suggestions = self._douban_suggest(bookname)
|
||||
if not suggestions:
|
||||
return None
|
||||
def score(item):
|
||||
title = item.get('title','')
|
||||
if bookname in title:
|
||||
return 3*len(bookname)/max(len(title),1)
|
||||
if title in bookname:
|
||||
return 2*len(title)/max(len(bookname),1)
|
||||
return len(set(bookname) & set(title)) / max(len(title),1)
|
||||
suggestions.sort(key=score, reverse=True)
|
||||
chosen = suggestions[0]
|
||||
sid = str(chosen.get('id'))
|
||||
html = self._fetch_subject(sid)
|
||||
desc = self._extract_description(html)
|
||||
author=''; publisher=''; publishing=''
|
||||
info_blk = re.search(r'<div id="info">(.*?)</div>', html, re.S)
|
||||
if info_blk:
|
||||
info_txt = re.sub(r'<br\s*/?>','\n', info_blk.group(1))
|
||||
info_txt = re.sub(r'<.*?>','', info_txt)
|
||||
lines = [l.strip() for l in info_txt.split('\n') if l.strip()]
|
||||
for line in lines:
|
||||
if line.startswith('作者') and not author:
|
||||
author = line.split(':',1)[-1].strip()
|
||||
elif line.startswith('出版社') and not publisher:
|
||||
publisher = line.split(':',1)[-1].strip()
|
||||
elif re.search(r'出版年', line) and not publishing:
|
||||
publishing = line.split(':',1)[-1].strip()
|
||||
bkinfo = defaultdict(dict)
|
||||
bkinfo[sid]['link'] = f'https://book.douban.com/subject/{sid}'
|
||||
bkinfo[sid]['bookname'] = chosen.get('title','')
|
||||
bkinfo[sid]['img'] = chosen.get('pic','')
|
||||
if desc: bkinfo[sid]['description'] = desc
|
||||
if author: bkinfo[sid]['author'] = author
|
||||
if publisher: bkinfo[sid]['publisher'] = publisher
|
||||
if publishing: bkinfo[sid]['publishing'] = publishing
|
||||
return [bookname, bkinfo]
|
||||
|
||||
def grab_book_info(self, mbkn: str):
|
||||
if ISDOUBAN==1:
|
||||
mparams['search_text'] = mbkn
|
||||
mparams['k'] = mbkn
|
||||
else:
|
||||
mparams['k'] = mbkn
|
||||
session = requests.Session()
|
||||
session.headers.update(mheaders)
|
||||
# 先访问主页获取 cookies
|
||||
try:
|
||||
session.get('https://www.douban.com/', timeout=10)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
r = session.get(murl, params=mparams, timeout=10)
|
||||
except Exception as e:
|
||||
logger.debug(f'request error {mbkn}: {e}')
|
||||
return [mbkn, defaultdict(dict)]
|
||||
if r.status_code != 200:
|
||||
logger.debug(f'status {r.status_code} for {mbkn}')
|
||||
bkinfo = defaultdict(dict)
|
||||
sid=None; stat=None
|
||||
resp = r.text
|
||||
if ISDOUBAN==1:
|
||||
stat='SID'
|
||||
for line in resp.split('\n'):
|
||||
line=line.strip()
|
||||
if not line: continue
|
||||
if stat=='SID':
|
||||
ret=re.search(self.re_bn, line)
|
||||
if ret:
|
||||
sid=ret.group(1)
|
||||
bkinfo[sid]['link']=os.path.join(LINKPREF,sid)
|
||||
bkinfo[sid]['bookname']=ret.group(2)
|
||||
bkinfo[sid]['img']=ret.group(3)
|
||||
stat='STAR'
|
||||
continue
|
||||
elif stat=='STAR':
|
||||
ret=re.search(self.re_star, line)
|
||||
if ret:
|
||||
star=ret.group(1)
|
||||
if star=='00':
|
||||
stat='AUTHOR'
|
||||
elif star.isdigit() and int(star)>0:
|
||||
stat='SCORE'
|
||||
elif stat=='SCORE':
|
||||
ret=re.search(self.re_score, line)
|
||||
if ret:
|
||||
bkinfo[sid]['score']=ret.group(1)
|
||||
stat='RATENUM'
|
||||
continue
|
||||
elif stat=='RATENUM':
|
||||
ret=re.search(self.re_ratenum, line)
|
||||
if ret:
|
||||
bkinfo[sid]['ratenum']=ret.group(1)
|
||||
stat='AUTHOR'
|
||||
continue
|
||||
elif stat=='AUTHOR':
|
||||
ret=re.search(self.re_author, line)
|
||||
if ret:
|
||||
tt=ret.group(1).split(' / ')
|
||||
if len(tt)>=3:
|
||||
*author, bkinfo[sid]['publisher'], bkinfo[sid]['publishing']=tt
|
||||
bkinfo[sid]['author']='/'.join(author)
|
||||
else:
|
||||
bkinfo[sid]['author']=ret.group(1)
|
||||
stat='DESCRIPTION'
|
||||
continue
|
||||
elif stat=='DESCRIPTION':
|
||||
ret=re.search(self.re_description, line)
|
||||
if ret:
|
||||
bkinfo[sid]['description']=ret.group(1).strip()
|
||||
stat='SID'
|
||||
continue
|
||||
else: # AMAZON
|
||||
stat='ASIN'
|
||||
for line in resp.split('\n'):
|
||||
line=line.strip()
|
||||
if not line: continue
|
||||
if stat=='ASIN':
|
||||
ret=re.search(self.re_asin, line)
|
||||
if ret:
|
||||
sid=ret.group(1)
|
||||
bkinfo[sid]['link']=os.path.join(LINKPREF,ret.group(1))
|
||||
stat='IMG'
|
||||
continue
|
||||
elif stat=='IMG':
|
||||
ret=re.search(self.re_img, line)
|
||||
if ret:
|
||||
bkinfo[sid]['img']=ret.group(1)
|
||||
stat='BOOKNAME'
|
||||
continue
|
||||
elif stat=='BOOKNAME':
|
||||
ret=re.search(self.re_bn, line)
|
||||
if ret:
|
||||
bkname=re.split(r'[((\s]',ret.group(1).strip())[0]
|
||||
bkinfo[sid]['bookname']=bkname
|
||||
stat='AUTHOR'
|
||||
continue
|
||||
elif stat=='AUTHOR':
|
||||
ret=re.search(self.re_author, line)
|
||||
if ret:
|
||||
author=','.join(re.split(r'<span.+?auto">|<\/span', ret.group(0))[3::4])
|
||||
bkinfo[sid]['author']=author
|
||||
stat='RATE'
|
||||
continue
|
||||
elif stat=='RATE':
|
||||
ret=re.search(self.re_rate, line)
|
||||
if ret:
|
||||
bkinfo[sid]['rate']=ret.group(1).split(' ')[0]
|
||||
stat='AUTHOR'
|
||||
continue
|
||||
if re.search(self.re_end, line):
|
||||
stat='ASIN'
|
||||
return [mbkn, bkinfo]
|
||||
|
||||
def filter_spide_book(self, mbkinfo):
|
||||
if not mbkinfo: return None
|
||||
mbkn=mbkinfo[0]
|
||||
best=None
|
||||
for sid,v in mbkinfo[1].items():
|
||||
bkn=v.get('bookname','')
|
||||
if not bkn: continue
|
||||
score=0
|
||||
if mbkn in bkn: score+=3
|
||||
if bkn in mbkn: score+=2
|
||||
score+=len(set(mbkn)&set(bkn))*0.01
|
||||
if (not best) or score>best[0]:
|
||||
best=(score,{mbkn:v})
|
||||
return best[1] if best else None
|
||||
|
||||
def down_book_img(self, mbkinfo):
|
||||
if not mbkinfo: return
|
||||
if not os.path.exists(IMGPATH): os.mkdir(IMGPATH)
|
||||
for _,v in mbkinfo.items():
|
||||
link=v.get('img')
|
||||
if not link: continue
|
||||
fname=link.split('/')[-1]
|
||||
p=os.path.join(IMGPATH,fname)
|
||||
if os.path.exists(p):
|
||||
continue
|
||||
try:
|
||||
img=requests.get(link, headers=mheaders, timeout=10)
|
||||
if img.status_code==200:
|
||||
with open(p,'wb') as fp:
|
||||
fp.write(img.content)
|
||||
except Exception as e:
|
||||
logger.debug(f'download img error {link}: {e}')
|
||||
|
||||
|
||||
|
||||
|
|
@ -11,6 +11,8 @@ beautifulsoup4>=4.9.0
|
|||
# 命令行交互界面(用于 exportbooknotes.py)
|
||||
InquirerPy>=0.3.0
|
||||
|
||||
# 可视化:已改为使用原生 Qt(QPainter) 自绘, 不再依赖 matplotlib
|
||||
|
||||
# 以下为 Python 标准库,无需安装:
|
||||
# sqlite3 - 数据库支持
|
||||
# plistlib - plist 文件解析
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
class BookReviewWorker(QThread):
|
||||
"""后台获取书籍 AI 简评的线程(迁移自 ibook_export_app.py)。"""
|
||||
finished = pyqtSignal(str, str) # bookname, review
|
||||
|
||||
def __init__(self, bookname, prompt, json_path, parent=None):
|
||||
super().__init__(parent)
|
||||
self.bookname = bookname
|
||||
self.prompt = prompt
|
||||
self.json_path = json_path
|
||||
|
||||
def run(self):
|
||||
import json
|
||||
try:
|
||||
from ai_interface import DashScopeChatClient
|
||||
chat = DashScopeChatClient()
|
||||
review = chat.ask(self.prompt)
|
||||
except Exception as e:
|
||||
review = f"[AI简评获取失败: {e}]"
|
||||
try:
|
||||
try:
|
||||
with open(self.json_path, 'r', encoding='utf-8') as f:
|
||||
intro_dict = json.load(f)
|
||||
except Exception:
|
||||
intro_dict = {}
|
||||
intro_dict[self.bookname] = review
|
||||
with open(self.json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(intro_dict, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"写入 bookintro.json 失败: {e}")
|
||||
self.finished.emit(self.bookname, review)
|
Loading…
Reference in New Issue