☰
Python智能笔记小程序实战:Flask后端+微信小程序+中文分词全解析
2026/10/1 18:45:26 网站建设 项目流程

1. 项目到底是什么:从一个小痛点说起

前阵子整理电脑里的笔记,发现光是 txt、md、doc 混在一起的文本就上千份。想找一条一年前记过的命令,翻了半天才翻到;想看看某个主题下自己都写过啥,更是无从下手。后来一咬牙,花了两三个周末,用 Python 写了一个叫“智能笔记”的小程序——一个命令行就能跑起来的轻量笔记系统,后端是一组纯 Python 实现的 API,前端顺手接了个微信小程序壳子。这个项目的核心就是解决三件事:让笔记能被秒搜、能自动打标签、能在手机上随时查。如果你也是 Python 入门不久的开发者,或者正在琢磨怎么把自己的小工具产品化,这篇文章的完整思路、完整代码和踩坑记录应该能让你少走不少弯路。

先解释一下标题里“Python 小程序”这个说法。它有两层意思:第一,它本身是一个用 Python 写的小型应用程序,不依赖任何重量级框架,一个文件或者一个小目录就能跑起来;第二,我做了一组 HTTP 接口,方便接到微信小程序这类前端上。也就是说,你既可以把它当成一个本地命令行工具用,也可以把它扩展成一套前后端分离的完整应用。这不是什么高深的东西,但胜在实用,而且每一步都可以自己掌控。

1.1 为什么用 Python 来写笔记工具

说实话,市面上的笔记软件已经多到数不过来了,为什么还要自己写?我的理由很简单:数据要掌握在自己手里。很多笔记软件的数据都存在人家的服务器上,导出格式又封闭,用久了就会被绑住。本地纯文本文件最自由,但纯文本没有检索能力。于是我想要一个“中间态”:文件还是自己的,但检索、标签、关联这些能力用代码补上。

Python 在这件事上有天然优势。第一,生态里现成的库够用,分词可以用 jieba,做 Web 接口可以用 Flask,数据库直接用内置的 SQLite,连编译部署都省了。第二,Python 的代码量少,维护起来没压力,一个周末写出来的工具,之后想改什么功能直接改脚本就行。第三,Python 的调试和实验成本低,我可以先跑命令行版本验证想法,再决定要不要做界面。如果是用 C++ 或者 Java 起步,可能还没写完存储层就放弃了。

1.2 这个项目能做什么:三层能力拆解

我把它分成了三层能力,每一层都是独立的,也可以单独使用:

  • 第一层是本地笔记管理。支持新增、修改、删除、搜索笔记,标题和正文分开存,时间戳自动记录。这一层解决的是“笔记散落各处”的问题。
  • 第二层是智能处理。对每篇笔记自动提取标签,TF-IDF 权重计算关键词,新增笔记时自动找出最相似的几篇旧笔记做关联推荐。这一层解决的是“记过的东西想不起来”的问题。
  • 第三层是移动端接入。通过 Flask 暴露一组 JSON API,微信小程序端可以分页加载笔记列表、搜索、查看详情、自动保存草稿。这一层解决的是“不在电脑前也想查笔记”的问题。

整体思路是渐进式的:先跑通本地闭环,再逐步加接口、加前端。不要一上来就想着做大而全的系统,一个能跑起来的小工具,远比一个写了一半的大架构有价值。

2. 核心设计思路与方案选型

2.1 整体架构:本地优先,API 可扩展

我在设计时定了一条原则:一切以本地文件和数据为主,网络接口只是附赠的扩展能力。所以数据存储选择了 SQLite,一个 .db 文件就包住了所有笔记数据,备份就是复制文件,迁移就是拷走文件。完全不需要装数据库服务,也不需要配账号密码。

目录结构非常简单:

smart-notes/ ├── app.py # Flask API 入口 ├── cli.py # 命令行入口 ├── storage.py # SQLite 存储层 ├── intelligence.py # 分词、标签、相似度计算 ├── requirements.txt # 依赖清单 └── notes.db # 数据库文件(自动生成)

整个项目没有一个 Python 包依赖外部服务,跑在什么机器上都一样。命令行工具和 API 共用同一套存储层和智能层代码,这样不会出现“命令行能搜到但 API 搜不到”的割裂问题。很多个人项目写着写着就失控,就是因为入口多了,逻辑没有收敛到同一层。

2.2 智能模块怎么选:jieba + TF-IDF + 余弦相似度

“智能”这个词听起来玄乎,落到笔记场景其实就是三个具体能力:提取关键词、算相似度、做关联推荐。我选了三件套:

  • jieba 负责中文分词。英文按空格拆就行,中文必须分词,否则“智能笔记”这四个字会被当成一个整体,搜“笔记”就匹配不到。
  • TF-IDF 负责提取关键词。TF 是词频,IDF 是逆文档频率。一个词在当前文档里出现次数越多、在整个语料库里出现越少,它就越能代表这篇笔记的主题。比如“笔记”这个词可能每篇都有,权重就低;而“布隆过滤器”只在某一篇出现,权重就高,很自然就提取出来了。
  • 余弦相似度负责找关联。把每篇笔记的词频向量拉出来,算两个向量之间的夹角余弦值,越接近 1 表示越相似。生活化地理解,就是看两篇笔记的“用词习惯”重合度有多高。

没有选择上大模型、向量数据库之类的方案,原因很简单:普通个人笔记的数据量也就几百上千篇,用传统 NLP 方法已经是杀鸡用牛刀了,而且离线可用、毫秒级响应、零成本。大模型适合更复杂的语义理解场景,但在这个项目里,先跑通经典方法再看要不要升级,性价比更高。

2.3 数据存储:为什么选 SQLite 而不是 JSON 文件

一开始我也想过直接用 JSON 文件存笔记,读取方便,人眼可读。但很快发现几个问题:搜索要遍历整个文件,笔记一多就慢;并发写入容易冲突;没有索引,查询优化无从谈起。换成 SQLite 之后,这些问题都不存在了。

SQLite 是嵌入式关系型数据库,一个文件一个库,支持 SQL 查询、事务、索引,而且 Python 标准库直接内置了 sqlite3 模块,连额外安装都省了。对于个人笔记这种规模的数据,SQLite 的性能绰绰有余。我用了一张表,字段设计如下:

字段类型说明
idINTEGER PRIMARY KEY笔记唯一 ID
titleTEXT标题
contentTEXT正文内容
tagsTEXT自动生成的标签,逗号分隔
created_atTEXT创建时间
updated_atTEXT最后修改时间

2.4 为什么做小程序端而不是原生 APP

很多朋友看到“小程序”第一反应是为什么要做微信小程序,不做安卓或者 iOS 原生应用。我当时的考虑是:第一,小程序免安装、跨平台,安卓和苹果手机都能用;第二,小程序的开发成本比原生低得多,一套代码两端跑,适合个人项目;第三,我只需要把关键的浏览和搜索功能搬到手机上,不需要复杂的系统能力,小程序完全够用。

当然,小程序的劣势也很明显:调试要用微信开发者工具,上线需要域名备案和 HTTPS,本地开发时只能在工具里预览。这些坑后面会细讲。总之,如果你做的是工具型产品,微信小程序是一个性价比很高的选择。

3. 从零搭环境:不会卡在第一步

3.1 Python 安装与虚拟环境

很多入门者在环境这一步就劝退了,其实问题就两个:装什么版本,装到哪里。我建议直接装 Python 3.10 以上版本,去官网下载安装包,安装时务必勾选“Add Python to PATH”这个选项,否则后面命令行找不到 python 命令。安装完成后,打开终端敲:

python --version

能看到版本号就说明成功了。Windows 如果提示找不到命令,常见原因是 PATH 没配上,去系统环境变量里手动把 Python 安装目录和 Scripts 子目录加进去就行。

接下来创建虚拟环境。虚拟环境的作用是为当前项目隔离依赖版本,避免不同项目之间互相污染。我习惯在每个项目里都建一个:

python -m venv venv

激活它。Windows 下执行:

venv\Scripts\activate

macOS 或 Linux 下执行:

source venv/bin/activate

激活后命令行的前面会出现(venv)标识,说明已经进入了隔离环境。这一步看起来很琐碎,但相信我,养成习惯之后能避免大量第三方库版本冲突的悲剧。

3.2 依赖库的选择与安装

这个项目的依赖控制在四个以内:Flask 用于提供 API,jieba 用于中文分词,scikit-learn 其实没用到,我后来发现自己手写 TF-IDF 和余弦相似度反而更轻,所以最终只保留了前三个实用的库(Flask、jieba、requests)。安装命令:

pip install flask jieba requests

如果你身处网络环境比较特殊的情况,pip 下载慢,可以临时指定镜像源,比如:

pip install flask jieba requests -i https://pypi.tuna.tsinghua.edu.cn/simple

装完之后,把依赖清单写到 requirements.txt 里,方便以后在其他机器上复现环境:

pip freeze > requirements.txt

3.3 VSCode 配置与调试

编辑器我用 VSCode,配置 Python 环境总共分三步。第一步,装官方 Python 插件,提供代码补全和语法检查。第二步,打开命令面板,输入Python: Select Interpreter,选择刚才创建的虚拟环境中的解释器,路径一般是venv/Scripts/python.exe(Windows)或venv/bin/python(macOS/Linux)。第三步,创建.vscode/launch.json配置文件,调试时按 F5 就能跑起来。

一个经常被忽略的细节是终端编码问题。Windows 默认编码是 GBK,而 Python 3 源码默认 UTF-8,一旦代码里出现中文注释或者中文字符串,控制台可能乱码甚至报错。我建议在项目根目录下创建.vscode/settings.json,写入:

{ "terminal.integrated.profiles.windows": { "PowerShell": { "env": { "PYTHONIOENCODING": "utf-8" } } }, "python.terminal.activateEnvironment": true }

这样终端环境统一走 UTF-8 编码,中文显示就不会乱。

4. 核心代码实现:智能笔记的三板斧

4.1 存储层:SQLite 的增删改查

先建立数据库连接模块。注意我在连接时设置了check_same_thread=False,因为后面 Flask 会开多线程处理请求,SQLite 默认不允许跨线程使用同一个连接,这个参数允许我们共享连接,代价是自己控制并发安全。对于单用户笔记工具来说,这个取舍是划算的。

import sqlite3 from contextlib import contextmanager DB_PATH = "notes.db" def get_conn(): conn = sqlite3.connect(DB_PATH, check_same_thread=False) conn.row_factory = sqlite3.Row return conn @contextmanager def get_db(): conn = get_conn() try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def init_db(): with get_db() as db: db.execute(""" CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, tags TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """)

这里有个我踩过的坑:datetime('now', 'localtime')在 SQLite 里是字符串拼接,不是真正的 datetime 类型,所以排序时要用字符串排序也能用,因为格式是YYYY-MM-DD HH:MM:SS,字典序就是时间序。但如果你需要做时间范围过滤,还是建议用时间戳整数存储,后续处理更灵活。

4.2 写入与搜索:从 insert 到 LIKE 查询

新增笔记的核心方法是先做标签提取,再写入数据库:

from intelligence import extract_tags, cosine_similarity def add_note(title, content): tags = extract_tags(title + " " + content) with get_db() as db: cur = db.execute( "INSERT INTO notes (title, content, tags) VALUES (?, ?, ?)", (title, content, tags) ) return cur.lastrowid

搜索笔记最开始我用的是最朴素的 LIKE 查询:

def search_notes(keyword): keyword = f"%{keyword}%" with get_db() as db: rows = db.execute( "SELECT * FROM notes WHERE title LIKE ? OR content LIKE ? ORDER BY updated_at DESC", (keyword, keyword) ).fetchall() return [dict(row) for row in rows]

数据量在几百篇以内这个方案完全够用。等笔记超过几千篇,LIKE 查询会开始变慢,到时候再上 SQLite 的 FTS5 全文索引也不迟。个人工具不要过早优化,先解决有没有的问题,再解决快不快的问题。

4.3 标签自动提取:基于 jieba 的 TF-IDF

自动标签是“智能”的第一印象。我用的是 jieba 自带的 TF-IDF 关键词提取接口,短短几行就能获取一篇笔记的核心关键词。注意提取时我把标题和正文拼在一起,因为标题里的词往往比正文里的词权重更高。

import jieba from jieba import analyse def extract_tags(text, top_k=5): text = text.replace("\n", " ") tags = analyse.extract_tags(text, topK=top_k, withWeight=False) return ",".join(tags)

jieba.analyse.extract_tags的原理是基于 TF-IDF,它会给高频词降权、给低频且重要的词加权。比如你写了一篇关于“布隆过滤器”的笔记,全库只有这一篇提到这个词,它就会被提取出来作为标签;而“今天”“我们”这类常见词会被自动过滤掉。

第一次调用会比后续调用慢一些,因为 jieba 要加载词典。如果你发现每次启动都要等一两秒,可以把 jieba 的初始化结果缓存成文件,或者用常驻进程(Flask 服务就是常驻的),这样只有第一次请求会慢。我实测下来,Flask 模式下第一次请求约 1.5 秒,之后就稳定在毫秒级。

4.4 相似笔记推荐:余弦相似度

找相似笔记的逻辑是:把两篇笔记分别分词,统计词频,得到两个向量,然后计算余弦相似度。这里为了清楚展示原理,我手写了一个轻量实现,没有引入额外的向量库:

import math from collections import Counter import jieba STOP_WORDS = set(["的", "了", "是", "我", "你", "他", "这", "那", "和", "与", "也", "就", "但", "而", "或"]) def tokenize(text): words = [w.strip() for w in jieba.cut(text) if w.strip() and w.strip() not in STOP_WORDS] return words def cosine_similarity(text1, text2): vec1 = Counter(tokenize(text1)) vec2 = Counter(tokenize(text2)) common = set(vec1.keys()) & set(vec2.keys()) dot = sum(vec1[w] * vec2[w] for w in common) norm1 = math.sqrt(sum(v * v for v in vec1.values())) norm2 = math.sqrt(sum(v * v for v in vec2.values())) if norm1 == 0 or norm2 == 0: return 0.0 return dot / (norm1 * norm2)

我给列表筛掉了常见停用词,因为“的”“了”“是”这些词对相似度计算没有任何帮助,反而会拉低准确率。实际使用中,这份停用词表还可以继续扩充,越用越贴合自己的笔记风格。

针对一篇笔记查相似时,我把它和全库所有笔记两两计算相似度,排个序返回 Top 3。虽然看起来是 O(n) 的扫描,但个人笔记库通常只有几百上千篇,每篇分词也只花几毫秒,实测完全无压力。

def similar_notes(note_id, top_k=3): with get_db() as db: rows = db.execute("SELECT * FROM notes").fetchall() target = next((dict(row) for row in rows if row["id"] == note_id), None) if not target: return [] target_text = target["title"] + " " + target["content"] scored = [] for row in rows: if row["id"] == note_id: continue other_text = row["title"] + " " + row["content"] score = cosine_similarity(target_text, other_text) scored.append((score, dict(row))) scored.sort(key=lambda x: x[0], reverse=True) return [item[1] for item in scored[:top_k]]

4.5 Flask API:把本地能力开放出去

命令行工具做得再好,也只在电脑上能用。要让手机查笔记,就得包装一层 HTTP 接口。我选了 Flask,因为它足够轻,一个文件就能起服务,而且代码的阅读负担小。

from flask import Flask, request, jsonify app = Flask(__name__) init_db() @app.route("/api/notes", methods=["GET"]) def list_notes(): page = request.args.get("page", 1, type=int) page_size = request.args.get("page_size", 10, type=int) keyword = request.args.get("q", "", type=str) offset = (page - 1) * page_size if keyword: notes = search_notes(keyword) start = offset end = offset + page_size return jsonify({"total": len(notes), "items": notes[start:end], "page": page}) with get_db() as db: rows = db.execute( "SELECT * FROM notes ORDER BY updated_at DESC LIMIT ? OFFSET ?", (page_size, offset) ).fetchall() total = db.execute("SELECT COUNT(*) FROM notes").fetchone()[0] return jsonify({"total": total, "items": [dict(r) for r in rows], "page": page}) @app.route("/api/notes/<int:note_id>/similar", methods=["GET"]) def similar(note_id): return jsonify({"items": similar_notes(note_id)}) @app.route("/api/notes", methods=["POST"]) def create_note(): data = request.get_json(force=True) if not data.get("title") or not data.get("content"): return jsonify({"error": "title and content are required"}), 400 note_id = add_note(data["title"], data["content"]) return jsonify({"id": note_id}), 201 if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)

这里做了两个重要设计:第一,列表接口支持分页参数page和page_size,避免一次返回全量数据;第二,搜索参数q走同一套逻辑,小程序端分页行为统一。你可能会问为什么不用LIMIT加OFFSET直接做 SQL 层分页,因为 LIKE 搜索出来后我还要用 Python 二次处理(比如高亮关键词、排序),在内存里分页更简单。

启动 API 时用host="0.0.0.0"很关键,这表示监听所有网卡。默认只监听 127.0.0.1,那样只能本机访问,手机连不上。不过这样启动之后,同一局域网内的任何设备都能访问你的 API,个人工具用是没问题,但如果部署到公网,一定要加密码保护,最简单的方式是加一个 token 校验。这算是我给自己的工具加的一道保险。

5. 小程序端对接实战:把笔记搬进手机

5.1 API 请求封装与 Promise 化

微信小程序原生的wx.request是回调风格,写多了容易嵌套地狱。我在开始写页面之前,先把请求封装成了 Promise 风格的工具函数,这样所有页面都能复用同一套逻辑:

// utils/request.js const BASE_URL = 'http://127.0.0.1:5000/api'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json' }, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { reject(new Error('请求失败,状态码:' + res.statusCode)); } }, fail: (err) => reject(err) }); }); } module.exports = { request };

一个很重要的点:在小程序后台的“开发设置”里,如果你没有配置合法域名,本地开发时需要在开发者工具中勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。勾上之后,http://127.0.0.1:5000才能访问。真机预览时,本地 API 只能用局域网 IP 访问,不能再用 127.0.0.1 了,手机和设备必须在同一个 WiFi 下。

5.2 列表页加载更多:onReachBottom 分页

笔记列表我用了最简单的“上拉加载更多”模式,微信小程序正好提供了onReachBottom触底事件。通过维护页面状态里的 page 和 hasMore 两个变量来控制:

Page({ data: { notes: [], page: 1, pageSize: 10, hasMore: true, total: 0 }, onLoad() { this.loadNotes(true); }, onReachBottom() { if (this.data.hasMore) { this.loadNotes(false); } }, loadNotes(reset) { const page = reset ? 1 : this.data.page + 1; request(`/notes?page=${page}&page_size=${this.data.pageSize}`) .then((res) => { this.setData({ notes: reset ? res.items : this.data.notes.concat(res.items), page, total: res.total, hasMore: this.data.notes.length + res.items.length < res.total }); }) .catch((err) => { wx.showToast({ title: '加载失败', icon: 'none' }); }); } });

有个体验优化的细节:当reset为 true 时(也就是初次加载或下拉刷新),应该先清空旧列表再插入新数据,避免出现数据叠加。另外hasMore的判断逻辑要考虑当前已加载条数是否小于总条数,否则最后几页会出现反复请求空数据的现象。

5.3 动态设置标题:wx.setNavigationBarTitle

笔记详情页的标题会随着笔记内容变化。这个功能微信小程序提供了现成接口wx.setNavigationBarTitle,我在详情页加载完数据之后调用:

wx.setNavigationBarTitle({ title: note.title });

标题最长可以显示多大?小程序导航栏标题有长度限制,实践下来十几个中文字符还勉强看得清,再长就会显示省略号。我的处理是,如果标题长度超过 12 个字,就截取前 12 个字加省略号再设置:

const shortTitle = note.title.length > 12 ? note.title.slice(0, 12) + '...' : note.title; wx.setNavigationBarTitle({ title: shortTitle });

这里需要掌握热词里提到的“小程序动态设置标题”的频率问题:不要在onShow里每次都调用,否则每次从后台切回前台都会闪一下标题。只在笔记数据真正加载完成、发生变化时设置一次就好。

5.4 缓存时间:给本地存储加过期策略

笔记详情每次打开都请求服务器太浪费,我加了一层本地缓存。微信小程序本身的wx.setStorageSync没有过期时间概念,我封装了一个带过期时间的缓存函数:

const CACHE_PREFIX = 'note_cache_'; const CACHE_EXPIRE_MS = 5 * 60 * 1000; // 5分钟 function getCache(key) { const raw = wx.getStorageSync(CACHE_PREFIX + key); if (!raw) return null; const data = JSON.parse(raw); if (Date.now() > data.expire) { wx.removeStorageSync(CACHE_PREFIX + key); return null; } return data.value; } function setCache(key, value) { wx.setStorageSync(CACHE_PREFIX + key, JSON.stringify({ value, expire: Date.now() + CACHE_EXPIRE_MS })); }

说到缓存时间的选择,5 分钟对于笔记场景是我实测下来比较舒服的数值。太短了网络请求频繁,太长了一旦笔记改了手机端不能及时看到新内容。如果你对自己电脑上编辑笔记的频率有数,可以调整。核心在于把“缓存时间”写成一个常量,未来想统一改时间只动一行代码。

5.5 监听用户离开:自动保存草稿

很多人用笔记工具有个痛点:写了一半切走应用,再回来内容没了。小程序可以在页面onHide和onUnload生命周期里捕获离开事件,把当前编辑的草稿存起来,下次进来时恢复:

Page({ data: { draft: '' }, onInput(e) { this.setData({ draft: e.detail.value }); }, onHide() { this.saveDraft(); }, onUnload() { this.saveDraft(); }, saveDraft() { if (this.data.draft) { wx.setStorageSync('editing_draft', this.data.draft); } }, onShow() { const draft = wx.getStorageSync('editing_draft'); if (draft) { this.setData({ draft }); wx.showToast({ title: '已恢复草稿', icon: 'none' }); } } });

这里要注意onHide和onUnload的区别:onHide是页面被切入后台时触发,比如用户切到微信聊天再切回来;onUnload是页面被销毁时触发,比如用户点击返回。两个都监听才是最稳妥的方案。另外,不要在保存完草稿后立刻清空 storage,否则用户如果只是想短暂切出去看个信息,回来见不到草稿反而会慌。

6. 常见问题与排查技巧实录

6.1 Windows 下中文乱码:控制台与文件写入

这个问题在开发中遇到过不下十次。原因就一句话:Python 3 默认源码是 UTF-8,但 Windows 控制台默认编码是 GBK。解决的思路有两个方向。

第一,给 Python 设置环境变量强制 UTF-8 输出。在 PowerShell 里执行:

$env:PYTHONIOENCODING="utf-8"

第二,代码里显式绕开标准输出编码问题。比如在 Flask 中返回 JSON 时,jsonify会自动处理编码,一般不会乱码;但如果你自己调用print()打印中文调试信息,就很容易踩坑。

文件写入的乱码问题同样常见。如果你把笔记内容落盘成.md文件,务必用 UTF-8 编码写入:

with open("note.md", "w", encoding="utf-8") as f: f.write(content)

不写encoding参数的话,Python 在不同操作系统上会采用不同默认编码,Windows 上写出来的文件很多软件打开都是乱码。

6.2 依赖安装失败的三种情形

第一,pip 找不到版本。这种一般发生在 Python 版本太老或者太新,库还没适配。我的建议是直接使用 Python 3.10 或 3.11,目前第三方库的兼容性最稳。第二,网络超时。解决方案就是换镜像源。第三,权限不足。在 macOS 或 Linux 上,不要用系统 Python 直接pip install,最好在虚拟环境里装,或者加--user参数。Windows 上如果提示“拒绝访问”,多半是因为没以管理员身份运行终端。

还有一个小技巧,如果你需要在一个全新的机器上快速复现环境,使用 requirements.txt 安装:

pip install -r requirements.txt

不要手工一个一个装,人肉枚举依赖肯定会漏。

6.3 性能问题:笔记量大了怎么办

当笔记数量到几千篇以后,每个搜索请求都用 LIKE 扫描全表,每个相似推荐都把全部笔记重新算一遍相似度,性能会明显下降。我实测的体感分界线在 3000 篇左右,超过这个量,搜索会从毫秒级退化到几百毫秒,相似推荐更是从几十毫秒涨到三四秒。

针对这个问题的优化路径有三步:

  • 第一步,给updated_at字段加索引,优化按时间倒序分页查询。
  • 第二步,升级到 SQLite FTS5 全文搜索,中文全文索引效率比 LIKE 提高一个数量级。
  • 第三步,做预计算。新增笔记时就把标签和向量缓存到数据库里,相似推荐时不实时分词,只查缓存值。

这三步里面,前两步都很快能落地,第三步需要写一点增量更新的逻辑。我个人建议:先加索引,再上 FTS5,真到这一步基本就能支撑个人笔记几千上万篇的量了。

6.4 Flask 端口占用与跨域问题

Flask 默认跑在 5000 端口,如果被别的应用占用,启动会直接报错。排查命令:

netstat -ano | findstr :5000

找到占用端口的进程号后,要么杀掉进程,要么换一个端口。我在代码里用的port=5000是硬编码的,更好的做法是把端口放到环境变量里:

import os port = int(os.environ.get("PORT", 5000)) app.run(host="0.0.0.0", port=port)

跨域问题主要出现在你用小程序的开发者工具调试、或者用浏览器直接访问 API 的时候。浏览器默认不允许跨域请求,需要在 Flask 端添加响应头:

@app.after_request def add_cors_headers(response): response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" response.headers["Access-Control-Allow-Headers"] = "Content-Type" return response

注意,这只是开发阶段的便利措施。如果这个 API 被部署到公网,Access-Control-Allow-Origin: *意味着任何网站都能跨域调用你的接口,这时候请务必加上 token 鉴权,否则你的笔记数据等于裸奔。

6.5 数据备份策略:单文件就是最好的备份

因为这个项目的全部数据都在notes.db这一个文件里,备份非常简单。我的习惯是用系统定时任务把notes.db每天复制一份到另一个磁盘目录,加上日期后缀:

copy notes.db backups\notes_%date:~0,4%%date:~5,2%%date:~8,2%.db

macOS 或 Linux 下就是:

cp notes.db backups/notes_$(date +%Y%m%d).db

如果你想更稳一点,每周手动导出一次全量 JSON。我用 Python 写了个简单的导出脚本,把所有笔记转成notes_backup.json,这样就算 SQLite 文件损坏,数据还是可读的纯文本。课程工具保持简单就是最好的工程实践。

写在最后:一些我对个人工具开发的体会

这个项目看起来不大,但带给我的收获其实挺多的。最核心的一点体会是,做个人工具不要一开始就追求大而全的设计。我最初甚至连“相似笔记推荐”都没打算做,是先把命令行版跑起来用了一个星期,真的感受到“想找记过的内容找不到”的痛苦之后,才决定加这个功能。功能是长在真实需求上的,不是长在想象里的。

另一个体会是数据格式和接口的设计要保持克制。我的 API 路径就三四个,字段就那几列,但因为这个接口同时服务了命令行、网页和微信小程序三端,每加一个字段都要想清楚会不会破坏既有调用方。后来我养成了一个习惯:任何接口变更,先把旧的字段保留一段时间,给前端留出升级缓冲期。个人项目虽然不怕破坏,但少几次灾难式调整,开发体验会好很多。

最后再分享一个实操技巧:像这种“Python + 小程序”的组合,建议先写核心算法和命令行工具,再包 API,最后再做前端页面。每一层都能独立测试和验证,而不是等到所有代码写完才第一次跑起来。这种渐进式开发方式,帮我省掉的排错时间可能比我写代码的时间还多。如果看到这里你也想动手做一个自己的笔记工具,我强烈建议从本地命令行版开始,一个add一个search跑通之后,你就会发现真正想加什么功能了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询