颠覆传统,清理软件,删除无用文件,节省空间,编写程序,保留长期搁置的旧文档,定期随机翻阅,唤醒过去未成型的创意构思。
实际应用场景描述
在心理健康与创新能力的研究中,有一个常见的观点:创新往往源于对过去经验的重新组合与联想。我们在日常工作和学习中会产生大量"未完成"的文档——灵感碎片、半途而废的方案、草图笔记。传统清理软件(如CCleaner、SpaceSniffer等)的核心逻辑是"删除低频文件以释放空间",这虽然节省了存储空间,却可能无意中清除了未来创新所需的"认知种子"。
本程序旨在颠覆这一逻辑:不删除旧文档,而是通过定期随机召回(Random Recall)机制,将长期搁置的文件重新呈现在用户面前,模拟"心理顿悟"的过程——当旧想法与新语境碰撞时,往往能激发出意想不到的创新构思。同时,通过智能归档(而非删除)来管理存储空间。
引入痛点
1. 传统清理软件的盲区:它们只关注文件大小与访问频率,完全忽略文件的语义价值与创新潜力。
2. "创意坟墓"问题:硬盘中堆积了大量被遗忘的半成品文档,它们不是"垃圾",而是未被激活的灵感。
3. 认知偏差:人们倾向于认为"新信息更有价值",而忽视了对旧有知识的重新编码(Re-encoding)所带来的创新收益。
4. 空间焦虑:用户既想保留旧文件,又担心存储空间不足,陷入两难。
核心逻辑讲解
程序的核心是一个三层过滤与召回引擎:
1. 语义价值评估层:通过文件元数据(修改时间、类型、内容摘要)给文件打上"创意价值分",而非简单地按时间排序。
2. 随机漫步召回层:使用加权随机算法,让高价值但长期未访问的文件有更高概率被"翻牌",模拟人类记忆的随机联想机制。
3. 智能归档压缩层:对超过一定阈值(如365天未修改)的文件进行透明压缩(ZIP),释放空间但不删除,保留随时解压访问的能力。
关键设计哲学:
- 不删除(No-Deletion):所有文件至少保留元数据索引
- 随机性(Serendipity):模仿心理学中的"偶遇学习"(Incidental Learning)
- 渐进式压缩(Progressive Archival):空间不够时先压缩再提示,而非直接删除
代码模块化实现
项目结构
creative_archive/
├── README.md
├── core/
│ ├── scanner.py # 文件系统扫描与元数据提取
│ ├── evaluator.py # 创意价值评估引擎
│ ├── recall.py # 随机召回机制
│ └── archiver.py # 智能压缩归档
├── utils/
│ ├── config.py # 配置管理
│ └── logger.py # 日志记录
├── main.py # 主入口
└── requirements.txt # 依赖声明
"requirements.txt"
# 核心依赖
PyMuPDF>=1.23.0 # PDF文本提取(备选文档解析)
python-docx>=0.8.11 # .docx文件解析
openpyxl>=3.1.0 # .xlsx文件解析
markdown>=3.5.0 # Markdown解析
# 工具库
schedule>=1.2.0 # 定时任务
rich>=13.0.0 # 终端美化输出
pyyaml>=6.0 # 配置文件解析
tqdm>=4.66.0 # 进度条
"utils/config.py"
"""
配置管理模块 —— 统一管理程序的可变参数
通过读取 config.yaml 实现外部化配置,避免硬编码
"""
from pathlib import Path
import yaml
DEFAULT_CONFIG = {
"scan_paths": ["~/Documents", "~/Desktop"], # 扫描目录
"ignore_patterns": ["*.tmp", "*.log", ".git/*"], # 忽略规则
"recall": {
"max_files": 10, # 每次召回展示的最大文件数
"min_idle_days": 90, # 最少搁置天数(低于此值不进入召回池)
"weights": {
"idle_time": 0.4, # 搁置时间权重
"file_type": 0.3, # 文件类型权重(文档 > 代码 > 其他)
"content_richness": 0.3 # 内容丰富度权重
}
},
"archive": {
"idle_threshold_days": 365, # 超过此天数自动归档压缩
"archive_dir": "~/.creative_archive/vault"
}
}
def load_config(config_path: str = "config.yaml") -> dict:
"""加载配置文件,缺失时返回默认配置"""
path = Path(config_path)
if path.exists():
with open(path, "r", encoding="utf-8") as f:
user_cfg = yaml.safe_load(f) or {}
# 深层合并(简单实现)
return {**DEFAULT_CONFIG, **user_cfg}
return DEFAULT_CONFIG
"utils/logger.py"
"""
日志模块 —— 统一输出格式,支持彩色终端与文件双写
"""
import logging
from rich.logging import RichHandler
def setup_logger(name: str = "CreativeArchive", level: str = "INFO") -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
# 终端彩色输出
console_handler = RichHandler(rich_tracebacks=True)
console_handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(console_handler)
# 文件日志(持久化运行记录)
file_handler = logging.FileHandler("creative_archive.log", encoding="utf-8")
file_handler.setFormatter(logging.Formatter(
"[%(asctime)s] %(levelname)-7s %(name)s - %(message)s"
))
logger.addHandler(file_handler)
return logger
"core/scanner.py"
"""
文件系统扫描器 —— 递归遍历目标目录,提取文件元数据
关键设计:使用生成器(generator)逐批产出,避免一次性加载大量文件导致内存膨胀
"""
import os
import hashlib
from pathlib import Path
from datetime import datetime
from typing import Iterator, Dict, Any
import fnmatch
from utils.logger import setup_logger
logger = setup_logger("Scanner")
class FileMetadata:
"""文件元数据结构体 —— 封装扫描阶段采集的所有信息"""
def __init__(self, path: str):
self.path = Path(path)
self.name = self.path.name
self.suffix = self.path.suffix.lower()
self.size = self.path.stat().st_size
self.mtime = datetime.fromtimestamp(self.path.stat().st_mtime)
self.atime = datetime.fromtimestamp(self.path.stat().st_atime)
self.ctime = datetime.fromtimestamp(self.path.stat().st_ctime)
self.content_hash = self._calc_hash()
def _calc_hash(self, chunk_size: int = 8192) -> str:
"""计算文件SHA-256哈希,用于内容去重(大文件分块读取)"""
h = hashlib.sha256()
try:
with open(self.path, "rb") as f:
while chunk := f.read(chunk_size):
h.update(chunk)
return h.hexdigest()
except (PermissionError, OSError):
return "" # 无法读取的文件返回空哈希
def days_idle(self) -> int:
"""计算文件搁置天数 —— 自最后一次修改至今的天数"""
delta = datetime.now() - self.mtime
return delta.days
def to_dict(self) -> Dict[str, Any]:
"""序列化为字典,便于后续存储与传输"""
return {
"path": str(self.path),
"name": self.name,
"suffix": self.suffix,
"size": self.size,
"mtime": self.mtime.isoformat(),
"atime": self.atime.isoformat(),
"days_idle": self.days_idle(),
"content_hash": self.content_hash
}
def scan_directory(
root: str,
ignore_patterns: list[str] = None
) -> Iterator[FileMetadata]:
"""
递归扫描目录,逐文件产出元数据
ignore_patterns: 类似 .gitignore 的匹配规则列表
"""
root_path = Path(root).expanduser().resolve()
if not root_path.exists():
logger.warning(f"路径不存在,跳过: {root}")
return
ignore_patterns = ignore_patterns or []
for entry in root_path.rglob("*"):
if not entry.is_file():
continue
# 忽略规则匹配
relative = str(entry.relative_to(root_path))
if any(fnmatch.fnmatch(relative, pat) for pat in ignore_patterns):
continue
try:
yield FileMetadata(str(entry))
except (PermissionError, OSError) as e:
logger.debug(f"无法读取文件 {entry}: {e}")
continue
"core/evaluator.py"
"""
创意价值评估引擎 —— 为每份文档计算"创意唤醒分"
设计思路:模仿心理学中的"记忆激活扩散"模型(Spreading Activation)
"""
from typing import Dict, Any, List
from datetime import datetime
from core.scanner import FileMetadata
from utils.logger import setup_logger
logger = setup_logger("Evaluator")
# 文件类型创意价值映射 —— 基于经验设定的基础分
# 文档类(文字密度高)> 代码类(结构化强)> 媒体类(已固化)
TYPE_SCORES = {
# 文档 —— 高创意唤醒潜力
".docx": 0.9, ".doc": 0.85, ".pdf": 0.7,
".txt": 0.6, ".md": 0.8, ".rtf": 0.5,
# 表格/演示 —— 中等
".xlsx": 0.65, ".pptx": 0.6, ".csv": 0.5,
# 代码 —— 结构化高,创意唤醒中等
".py": 0.55, ".js": 0.5, ".java": 0.45, ".cpp": 0.4,
# 媒体 —— 低(但非删除)
".png": 0.2, ".jpg": 0.15, ".mp4": 0.1,
}
# 小文件(<1KB)通常是占位符或碎片,降低权重
SIZE_PENALTY_THRESHOLD = 1024 # 1KB
def evaluate_file(meta: FileMetadata, config: Dict[str, Any]) -> Dict[str, Any]:
"""
对单个文件进行多维度评分,返回评估结果字典
评分维度:
1. 搁置时间分(idle_score) —— 越久未动,召回价值越高
2. 文件类型分(type_score) —— 文本类高于二进制类
3. 内容丰富度分(richness) —— 文件越大(合理范围内)信息密度越高
4. 最终加权得分(final_score)—— 各维度加权求和
"""
weights = config.get("recall", {}).get("weights", {})
# ---- 维度1:搁置时间分(0~1 归一化,180天为满分拐点) ----
days = meta.days_idle()
idle_score = min(days / 180.0, 1.0) # 180天以上均为满分
# ---- 维度2:文件类型分 ----
type_score = TYPE_SCORES.get(meta.suffix, 0.3) # 未知类型给基础分0.3
# ---- 维度3:内容丰富度分 ----
size_kb = meta.size / 1024
if size_kb < 1:
richness = 0.1 # 极小文件可能是碎片
elif size_kb < 100:
richness = min(size_kb / 100.0, 1.0) # 100KB 以下线性增长
else:
richness = 1.0 # 大于100KB认为内容充分
# ---- 加权汇总 ----
final_score = (
idle_score * weights.get("idle_time", 0.4) +
type_score * weights.get("file_type", 0.3) +
richness * weights.get("content_richness", 0.3)
)
return {
**meta.to_dict(),
"idle_score": round(idle_score, 3),
"type_score": round(type_score, 3),
"richness": round(richness, 3),
"final_score": round(final_score, 4)
}
def batch_evaluate(
files: List[FileMetadata],
config: Dict[str, Any]
) -> List[Dict[str, Any]]:
"""批量评估文件,返回按分数降序排列的评估结果列表"""
results = [evaluate_file(f, config) for f in files]
results.sort(key=lambda x: x["final_score"], reverse=True)
logger.info(f"评估完成: {len(results)} 个文件进入召回池")
return results
"core/recall.py"
"""
随机召回引擎 —— 加权随机挑选"被遗忘的创意"
核心算法:加权轮盘赌(Weighted Roulette Wheel Selection)
配合"探索-利用"平衡策略(Epsilon-Greedy 的变体)
"""
import random
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
from utils.logger import setup_logger
logger = setup_logger("Recall")
def build_recall_pool(
evaluated_files: List[Dict[str, Any]],
min_idle_days: int = 90,
max_idle_days: Optional[int] = None
) -> List[Dict[str, Any]]:
"""
构建召回候选池 —— 过滤不符合条件的文件
条件:
1. 搁置天数 >= min_idle_days(太新的文件不需要唤醒)
2. 如果指定了 max_idle_days,则不超过该值(用于分桶测试)
"""
pool = []
for f in evaluated_files:
if f["days_idle"] < min_idle_days:
continue
if max_idle_days and f["days_idle"] > max_idle_days:
continue
pool.append(f)
logger.info(f"召回池构建完成: {len(pool)} 个候选文件(最小搁置 {min_idle_days} 天)")
return pool
def weighted_random_pick(
pool: List[Dict[str, Any]],
n: int = 10,
temperature: float = 1.0
) -> List[Dict[str, Any]]:
"""
加权随机召回 —— 核心算法
temperature: 温度参数
- =1.0 完全按分数加权
- >1.0 增加随机性(更多探索)
- <1.0 偏向高分文件(更多利用)
实现方式:对分数做 softmax 变换后按概率采样,不放回
"""
if not pool:
return []
n = min(n, len(pool))
scores = [f["final_score"] for f in pool]
# Softmax 变换
import math
exp_scores = [math.exp(s / temperature) for s in scores]
total = sum(exp_scores)
probabilities = [es / total for es in exp_scores]
# 不放回加权采样
selected_indices = []
indices = list(range(len(pool)))
probs = probabilities.copy()
for _ in range(n):
if not indices:
break
chosen = random.choices(indices, weights=probs, k=1)[0]
selected_indices.append(chosen)
# 移除已选,重新归一化
idx_in_probs = indices.index(chosen)
indices.pop(idx_in_probs)
probs.pop(idx_in_probs)
if probs:
norm = sum(probs)
probs = [p / norm for p in probs]
return [pool[i] for i in selected_indices]
def display_recall(files: List[Dict[str, Any]]) -> None:
"""格式化展示召回结果 —— 使用 rich 库输出美观的终端表格"""
from rich.table import Table
from rich.console import Console
from rich.text import Text
console = Console()
# 标题
console.print("\n" + "="*60, style="dim")
console.print(" 🧠 创意唤醒 —— 被遗忘的文件召回", style="bold cyan")
console.print("="*60, style="dim")
table = Table(show_header=True, header_style="bold magenta", box=None)
table.add_column("#", width=4, justify="center")
table.add_column("文件名", style="green", no_wrap=False)
table.add_column("搁置天数", justify="center", style="yellow")
table.add_column("创意唤醒分", justify="center", style="cyan")
table.add_column("路径", style="dim", no_wrap=False)
for i, f in enumerate(files, 1):
# 分数可视化
score_bar = "█" * int(f["final_score"] * 20)
score_text = Text(f"{f['final_score']:.2f} {score_bar}", style="cyan")
idle_text = Text(f"{f['days_idle']}天", style="yellow")
table.add_row(
str(i),
f["name"][:40] + ("..." if len(f["name"]) > 40 else ""),
idle_text,
score_text,
f["path"][:60] + ("..." if len(f["path"]) > 60 else "")
)
console.print(table)
console.print("="*60, style="dim")
console.print(f"\n 💡 共召回 {len(files)} 个文件,试着打开看看?\n", style="bold green")
"core/archiver.py"
"""
智能归档模块 —— 对长期未访问的大文件进行透明压缩
策略:不删除,只压缩。用户可随时解压恢复
"""
import zipfile
import os
from pathlib import Path
from typing import List, Dict, Any
from datetime import datetime
from utils.logger import setup_logger
logger = setup_logger("Archiver")
def should_archive(meta_dict: Dict[str, Any], threshold_days: int = 365) -> bool:
"""判断文件是否需要归档:搁置超过阈值天数"""
return meta_dict["days_idle"] >= threshold_days
def compress_to_vault(
files: List[Dict[str, Any]],
vault_dir: str = "~/.creative_archive/vault"
) -> Dict[str, Any]:
"""
将符合条件的文件压缩到保险库(vault)中
返回归档报告:成功数、跳过数、释放空间
"""
vault_path = Path(vault_dir).expanduser()
vault_path.mkdir(parents=True, exist_ok=True)
# 按日期命名归档包,避免冲突
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive_path = vault_path / f"archive_{timestamp}.zip"
archived_count = 0
skipped_count = 0
freed_bytes = 0
archived_files = []
with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zf:
for f in files:
src = Path(f["path"])
if not src.exists():
skipped_count += 1
continue
# 使用相对路径存储,避免解压时路径冲突
arcname = f"archive_{timestamp}/{src.name}"
zf.write(src, arcname=arcname)
freed_bytes += f["size"]
archived_files.append({
"original_path": f["path"],
"archived_as": arcname,
"size": f["size"]
})
archived_count += 1
logger.debug(f"已归档: {src.name}")
# 归档成功后,删除原文件(保留索引记录)
for af in archived_files:
try:
Path(af["original_path"]).unlink()
except OSError as e:
logger.warning(f"删除原文件失败 {af['original_path']}: {e}")
report = {
"archive_path": str(archive_path),
"archived_count": archived_count,
"skipped_count": skipped_count,
"freed_mb": round(freed_bytes / (1024 * 1024), 2),
"files": archived_files
}
logger.info(
f"归档完成: {archived_count} 个文件 → {archive_path} "
f"(释放 {report['freed_mb']} MB)"
)
return report
def restore_from_vault(archive_path: str, restore_dir: str = ".") -> int:
"""从保险库恢复文件 —— 解压归档包到指定目录"""
count = 0
with zipfile.ZipFile(archive_path, "r") as zf:
zf.extractall(restore_dir)
count = len(zf.namelist())
logger.info(f"恢复完成: {count} 个文件 → {restore_dir}")
return count
"main.py"
"""
主程序入口 —— 编排扫描→评估→召回→归档的完整流水线
支持两种模式:
1. 单次运行(--run-once):立即执行一次完整流程
2. 定时运行(默认):按 schedule 配置周期性执行
"""
import argparse
import sys
from pathlib import Path
from datetime import datetime
from utils.config import load_config
from utils.logger import setup_logger
from core.scanner import scan_directory
from core.evaluator import batch_evaluate
from core.recall import build_recall_pool, weighted_random_pick, display_recall
from core.archiver import should_archive, compress_to_vault
logger = setup_logger("Main")
def collect_all_files(config: dict) -> list:
"""从所有配置路径收集文件元数据"""
all_files = []
scan_paths = config.get("scan_paths", [])
ignore_patterns = config.get("ignore_patterns", [])
for p in scan_paths:
logger.info(f"开始扫描: {p}")
count_before = len(all_files)
for meta in scan_directory(p, ignore_patterns):
all_files.append(meta)
new_count = len(all_files) - count_before
logger.info(f" → 发现 {new_count} 个文件")
return all_files
def run_pipeline(config: dict, max_recall: int = None) -> None:
"""执行完整流水线"""
# Step 1: 扫描
files = collect_all_files(config)
if not files:
logger.warning("未发现任何文件,请检查 scan_paths 配置")
return
logger.info(f"扫描完成,共发现 {len(files)} 个文件")
# Step 2: 评估
evaluated = batch_evaluate(files, config)
# Step 3: 构建召回池
recall_cfg = config.get("recall", {})
pool = build_recall_pool(
evaluated,
min_idle_days=recall_cfg.get("min_idle_days", 90)
)
if not pool:
logger.info("召回池为空(所有文件都太"年轻"),本次无需召回")
else:
# Step 4: 随机召回
n = max_recall or recall_cfg.get("max_files", 10)
picked = weighted_random_pick(pool, n=n, temperature=1.2)
display_recall(picked)
# Step 5: 智能归档(空间管理)
archive_cfg = config.get("archive", {})
threshold = archive_cfg.get("idle_threshold_days", 365)
to_archive = [f for f in evaluated if should_archive(f, threshold)]
if to_archive:
logger.info(f"发现 {len(to_archive)} 个文件超过 {threshold} 天,准备归档...")
report = compress_to_vault(
to_archive,
vault_dir=archive_cfg.get("archive_dir", "~/.creative_archive/vault")
)
logger.info(f"归档报告: 释放 {report['freed_mb']} MB 空间")
else:
logger.info("暂无需要归档的文件")
def main():
parser = argparse.ArgumentParser(
description="Creative Archive —— 不删文件,唤醒创意"
)
parser.add_argument(
"--run-once", action="store_true",
help="立即执行一次完整流水线(默认启动定时任务)"
)
parser.add_argument(
"--config", default="config.yaml",
help="配置文件路径(默认: config.yaml)"
)
parser.add_argument(
"--max-recall", type=int, default=None,
help="每次召回的最大文件数(覆盖配置文件)"
)
args = parser.parse_args()
config = load_config(args.config)
if args.run_once:
logger.info("单次运行模式")
run_pipeline(config, args.max_recall)
else:
# 定时模式:每天执行一次
import schedule
import time
logger.info("定时运行模式,每天 09:00 执行召回")
schedule.every().day.at("09:00").do(run_pipeline, config, args.max_recall)
while True:
schedule.run_pending()
time.sleep(60)
if __name__ == "__main__":
main()
README.md
# Creative Archive — 创意唤醒归档工具
> *"Innovation is the re-ordering of existing elements."* — attributed to various sources
## 是什么
Creative Archive 是一个 Python 命令行工具,它**不删除任何文件**,而是通过智能评估 + 加权随机召回 + 透明压缩归档,帮助你在清理存储空间的同时,**定期"翻看"被遗忘的旧文档**,唤醒沉睡的创意构思。
## 核心设计原则
| 原则 | 说明 |
|------|------|
| **No-Deletion(不删除)** | 所有文件至少保留压缩副本,索引永不丢失 |
| **Serendipity(偶遇性)** | 加权随机召回,模拟人类记忆的随机联想 |
| **Progressive Archival(渐进归档)** | 空间不足时压缩而非删除,随时可恢复 |
## 安装
bash
克隆或下载本仓库
cd creative_archive
安装依赖(推荐虚拟环境)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
## 配置
编辑 `config.yaml`(首次运行会自动生成默认配置):
yaml
scan_paths:
- "~/Documents"
- "~/Desktop"ignore_patterns:
- "*.tmp"
- "*.log"
- ".git/*"recall:max_files: 10min_idle_days: 90weights:idle_time: 0.4file_type: 0.3content_richness: 0.3archive:idle_threshold_days: 365archive_dir: "~/.creative_archive/vault"
## 使用
bash
立即执行一次完整流程
python main.py --run-once
启动定时任务(每天 09:00 自动召回)
python main.py
指定召回数量
python main.py --run-once --max-recall 5
## 模块说明
| 模块 | 职责 |
|------|------|
| `core/scanner.py` | 递归扫描文件系统,提取文件元数据 |
| `core/evaluator.py` | 多维度创意价值评分引擎 |
| `core/recall.py` | 加权随机召回 + 终端可视化展示 |
| `core/archiver.py` | 透明压缩归档与恢复 |
| `utils/config.py` | 配置加载与默认值管理 |
| `utils/logger.py` | 彩色日志与文件双写 |
## 输出示例
============================================================
🧠 创意唤醒 —— 被遗忘的文件召回
文件名 搁置天数 创意唤醒分 路径
1 老项目的脑洞笔记.docx 412天 0.82 ████████████████ /Users/...
2 未完成的API设计.md 301天 0.76 ██████████████ /Users/...
...
💡 共召回 10 个文件,试着打开看看?
## 恢复归档文件
python
from core.archiver import restore_from_vault
restore_from_vault("~/.creative_archive/vault/archive_20250615_143022.zip")
## 许可
MIT License —— 自由使用、修改、分发。
核心知识点卡片
┌─────────────────────────────────────────────────────────┐
│ 知识点卡片 #1:加权随机采样 │
├─────────────────────────────────────────────────────────┤
│ 概念:Softmax 加权随机不放回采样 │
│ 场景:需要按概率分布选人/选文件,且不允许重复 │
│ 核心公式:P(i) = exp(s_i / T) / Σ exp(s_j / T) │
│ 温度 T 的作用: │
│ T > 1 → 更均匀(探索) T < 1 → 更集中(利用) │
│ 代码位置:core/recall.py → weighted_random_pick() │
└─────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!