
之前用 Notion、语雀、各种云笔记整理技术资料经常遇到一个问题资料越存越多分类越来越乱想找一个半年前记过的命令翻半天目录都找不到。网上现成的知识库系统不少但要么功能太重要么数据不在自己手里要么配置起来对新手不太友好。后来干脆自己动手写了一个轻量级个人知识库系统只保留最常用的“写文章、存笔记、搜内容”三个能力数据全在本地想怎么改就怎么改心里踏实得多。这篇文章就围绕这套思路完整拆解一个“从零手搓个人知识库系统”的过程。我用的是 Python Flask SQLite再加 Markdown 渲染代码量不大核心逻辑不复杂纯小白也能跟着一步步搭起来。读完这篇文章你会理解个人知识库系统的基本结构能够独立完成一个可用的 Web 版本地知识库也掌握了一套笔记类应用增删改查、搜索、分类的通用写法。整个过程大约十分钟可以跑通。1. 背景与核心概念1.1 个人知识库系统到底是什么个人知识库系统简单说就是一套用于“存储、组织、检索个人知识内容”的软件或工具。和普通的文件夹分类不同知识库系统通常具备三个能力内容录入把零散的想法、笔记、技术文档保存成结构化内容。内容组织通过分类、标签、日期、标题等方式把内容组织起来。内容检索根据关键词快速找到历史记录解决“记过但找不到”的问题。如果从技术实现角度看一个最简知识库系统就是“数据库 内容展示页面 内容编辑页面”。数据库负责存数据页面负责读写数据。个人知识库系统的应用场景很广程序员记录踩坑笔记、运维人员保存命令手册、产品经理沉淀竞品分析、学生整理课程重点都可以用。很多团队的内部 Wiki本质上也是一个多人的知识库系统。1.2 为什么不用现成笔记软件很多朋友第一反应是Notion、Obsidian、语雀不香吗不是不香而是不同需求对应不同工具。现成笔记软件的优势非常明显部署简单、功能丰富、多端同步顺手。但如果你有下面这些诉求自建一个轻量级系统反而更合适数据主权笔记内容存在自己电脑里不依赖第三方云端服务。自由定制想加一个“知识卡片”功能、想改列表排序规则自己改代码就能实现。学习目的通过动手写一个系统理解 Web 开发、数据库设计、增删改查、权限控制等基础知识。轻量部署一台小服务器就能跑起来不占太多内存和资源。自建方案的代价是需要维护代码、手动备份、处理兼容问题。但如果只是想存技术笔记、个人文章这套成本完全可控。1.3 自己搭建的优缺点自己动手写知识库系统优点刚才已经提到了。这里提醒大家注意几个潜在问题功能边界控制不要一上来就想着做多用户、权限系统、在线协作先做好单机版。数据备份写的第一天就要想好备份策略否则数据丢了会很麻烦。安全问题如果部署到公网需要关注口令、SQL 注入、暴力破解等风险如果只在本地使用风险小很多。本文的示例默认在本地运行适合学习和个人使用。如果想部署到公网服务器建议先读完第 8 节的安全建议。2. 系统设计与技术选型2.1 功能需求拆解做项目前先做减法。一个“纯小白版个人知识库系统”只需要四个功能文章列表页展示全部知识笔记支持按标题或内容搜索。文章详情页展示某一篇笔记的完整内容支持 Markdown 渲染。新建/编辑文章提供表单录入标题、分类、正文。删除文章删除不需要的记录。数据库表只需要一张articles表字段包括标题、分类、内容、创建时间、更新时间。这个需求拆解对应到开发流程就是一个标准的“单表 CRUD 搜索”应用。别看它简单把这张表的逻辑做扎实以后扩展标签、多用户、评论功能时思路会非常清晰。2.2 技术选型为什么选 Flask SQLite技术选型要考虑“新手友好 代码量少 数据安全”。FlaskPython 生态中非常轻量的 Web 框架。不需要复杂配置一个.py文件就能启动一个 Web 应用。路由写法直观适合把精力放在业务逻辑上。SQLitePython 内置的轻量级数据库不需要单独安装数据库服务数据保存在一个本地文件里。对于个人知识库这种低并发、单机使用的场景性能和稳定性完全够用。Python-Markdown把 Markdown 文本渲染成 HTML 的核心库支持表格、代码高亮、目录等扩展。这套组合的好处是依赖少、环境简单、纯本地运行。如果你对 Java 更熟悉也可以用 Spring Boot H2 Thymeleaf 实现同样效果但依赖和配置会多一些新手容易在环境环节卡住。2.3 为什么不选更重的方案有朋友可能问既然只是做知识库直接装一个 Wiki.js 或 Outline 不就行了不是不行而是定位不同。现成 Wiki 系统解决的是“快速拥有产品能力”本文解决的是“理解底层实现 定制个人工具”。自己用 Flask 写一套以后想加一个“按标签筛选”或“自动生成目录”的功能改动成本非常低用现成系统时这些改动往往受限于插件机制或权限体系。另外把一套小系统从 0 到 1 写出来对学习 Web 开发非常有帮助。数据库连接、参数化查询、路由设计、模板渲染、异常处理这些正是项目实战中最核心的技能。3. 环境准备与项目结构3.1 开发环境说明本文示例以常见环境为例操作系统Windows 10/11、macOS、Linux 均可。Python 版本建议 3.9 及以上本文代码使用 Python 3.10 测试。Flask 版本2.3 或 3.x 均可。浏览器Chrome、Edge、Firefox 等现代浏览器。版本需要根据你的实际环境调整。如果已经安装过其他版本保持既有环境即可重点看代码思路。3.2 安装依赖先创建一个项目目录比如knowledge-base然后进入目录。在命令行中执行mkdir knowledge-base cd knowledge-base创建虚拟环境推荐python -m venv venvWindows激活虚拟环境venv\Scripts\activatemacOS / Linux激活虚拟环境source venv/bin/activate安装依赖pip install flask markdown安装完成后可以通过下面的命令确认版本python -c import flask; print(flask.__version__) python -c import markdown; print(markdown.__version__)这里只依赖flask和markdown两个库其他功能都使用 Python 标准库。如果网络环境受限可以切换 pip 镜像源但不影响代码逻辑。3.3 项目结构为了让代码清晰这里采用一个最简单的 Flask 单文件结构所有后端逻辑放在app.pyHTML 模板直接写在 Python 文件中。这样做的好处是降低文件数量适合十分钟跑通缺点是模板和代码混在一个文件里。等逻辑变复杂后再拆成templates/目录和static/目录也不迟。knowledge-base/ ├── venv/ # 虚拟环境目录自动生成 ├── app.py # Flask 应用入口包含全部后端逻辑 └── knowledge.db # SQLite 数据库文件首次运行后生成4. 数据库设计与初始化4.1 表结构说明数据库使用 SQLite表名articles字段设计如下字段类型说明idINTEGER PRIMARY KEY AUTOINCREMENT文章主键自增titleTEXT NOT NULL文章标题categoryTEXT DEFAULT 默认文章分类contentTEXT NOT NULL文章正文Markdown 格式created_atTIMESTAMP DEFAULT CURRENT_TIMESTAMP创建时间updated_atTIMESTAMP DEFAULT CURRENT_TIMESTAMP更新时间这么设计已经足够支撑“记录笔记 分类 搜索”的核心场景。title和content都加了NOT NULL避免写入空数据。id作为主键删除和编辑时定位记录非常方便。4.2 数据库连接与初始化SQLite 在 Python 里有三种常用写法每次操作都手动sqlite3.connect()然后conn.close()。每次请求创建一个连接放在 Flask 的g对象上请求结束后关闭。使用 SQLAlchemy 等 ORM 框架。本文使用第二种方式同时借助 Flask 的app.context()在应用启动时自动建表。先看数据库连接部分的代码写入app.py# 文件路径knowledge-base/app.py import sqlite3 from flask import Flask, request, render_template_string, redirect, url_for, g import markdown app Flask(__name__) DATABASE knowledge.db def get_db(): 获取数据库连接同一个请求复用同一个连接 db getattr(g, _database, None) if db is None: db g._database sqlite3.connect(DATABASE) db.row_factory sqlite3.Row return db app.teardown_appcontext def close_connection(exception): 请求结束后关闭数据库连接 db getattr(g, _database, None) if db is not None: db.close() def init_db(): 初始化数据库表结构 with app.app_context(): db get_db() db.execute( CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, category TEXT DEFAULT 默认, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) db.commit()这里的sqlite3.Row很关键它让查询结果支持article[title]这种字典式取值在模板渲染时非常方便。init_db()中使用CREATE TABLE IF NOT EXISTS意思是表不存在时才创建已经存在就跳过。这个逻辑保证了多次启动应用不会重复建表或报错。5. 编写核心代码5.1 首页与搜索首页要完成两件事展示全部文章列表。根据 URL 参数q搜索标题和正文。路由代码如下app.route(/) def index(): keyword request.args.get(q, ) db get_db() if keyword: articles db.execute( SELECT * FROM articles WHERE title LIKE ? OR content LIKE ? ORDER BY updated_at DESC, (f%{keyword}%, f%{keyword}%) ).fetchall() else: articles db.execute( SELECT * FROM articles ORDER BY updated_at DESC ).fetchall() return render_template_string(INDEX_HTML, articlesarticles, keywordkeyword)这里有一个非常重要的细节LIKE模糊查询中的?是参数占位符不要直接用字符串拼接 SQL否则很可能引入 SQL 注入漏洞。参数化查询是必须养成的习惯。搜索逻辑是如果q为空查出全部文章如果不为空在标题和正文里进行模糊匹配。搜索范围可以根据需求调整比如后面可以加上分类搜索。5.2 文章详情与 Markdown 渲染详情页接收文章 ID从数据库查出对应记录把 Markdown 文本转成 HTML 后交给模板渲染。app.route(/article/int:article_id) def detail(article_id): db get_db() article db.execute( SELECT * FROM articles WHERE id ?, (article_id,) ).fetchone() if article is None: return 文章不存在或已被删除, 404 html_content markdown.markdown( article[content], extensions[extra, codehilite, toc] ) return render_template_string(DETAIL_HTML, articlearticle, contenthtml_content)这里用到 Python-Markdown 的扩展extra包含表格、删除线、代码块、脚注等常用扩展。codehilite代码高亮需要配合 Pygments 使用。toc自动生成目录结构。如果某些环境没有安装 Pygments代码高亮可能不生效但不会报错。可以执行下面的命令补装pip install pygmentsint:article_id是 Flask 内置的 URL 转换器它保证article_id一定是整数。如果访问/article/abcFlask 会自动返回 404不会进入这个函数。5.3 新建文章新建文章使用 GET 和 POST 两种方法GET 请求返回空白编辑表单。POST 请求读取表单数据校验后写入数据库然后跳转到首页。app.route(/new, methods[GET, POST]) def new_article(): if request.method POST: title request.form.get(title, ).strip() category request.form.get(category, ).strip() or 默认 content request.form.get(content, ).strip() if not title or not content: return 标题和内容不能为空, 400 db get_db() db.execute( INSERT INTO articles (title, category, content) VALUES (?, ?, ?), (title, category, content) ) db.commit() return redirect(url_for(index)) return render_template_string(EDIT_HTML, articleNone)这里有几个值得注意的点用.strip()去掉首尾空格避免用户误输入空白字符。分类如果为空回退为“默认”。标题和正文都为空时返回 400 错误。db.commit()必须调用否则数据不会真正写入数据库。5.4 编辑与删除文章编辑文章和新建文章逻辑相似区别是需要根据 ID 查出旧数据回显到表单。app.route(/edit/int:article_id, methods[GET, POST]) def edit_article(article_id): db get_db() article db.execute( SELECT * FROM articles WHERE id ?, (article_id,) ).fetchone() if article is None: return 文章不存在或已被删除, 404 if request.method POST: title request.form.get(title, ).strip() category request.form.get(category, ).strip() or 默认 content request.form.get(content, ).strip() if not title or not content: return 标题和内容不能为空, 400 db.execute( UPDATE articles SET title ?, category ?, content ?, updated_at CURRENT_TIMESTAMP WHERE id ?, (title, category, content, article_id) ) db.commit() return redirect(url_for(detail, article_idarticle_id)) return render_template_string(EDIT_HTML, articlearticle)删除文章使用 POST 方法避免 GET 请求误触发删除。前端通过 confirm 弹窗二次确认后端也做了基础校验。app.route(/delete/int:article_id, methods[POST]) def delete_article(article_id): db get_db() db.execute(DELETE FROM articles WHERE id ?, (article_id,)) db.commit() return redirect(url_for(index))更新操作中手动维护updated_at字段这样列表按更新时间倒序排列时最近编辑的文章会排在最前面。5.5 页面模板虽然使用render_template_string模板本身仍然是标准的 HTML Jinja2 语法。下面给出三个页面的完整模板。先看首页模板INDEX_HTML !DOCTYPE html html langzh-CN head meta charsetUTF-8 title我的个人知识库/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; color: #333; } h1 { font-size: 28px; } form { display: flex; gap: 10px; margin-bottom: 20px; } input[typetext] { flex: 1; padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; font-size: 14px; } button { padding: 8px 16px; border: none; background: #1677ff; color: #fff; border-radius: 6px; cursor: pointer; } a { color: #1677ff; text-decoration: none; } ul { list-style: none; padding: 0; } li { background: #fff; padding: 14px 16px; margin-bottom: 10px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); } .meta { color: #999; font-size: 13px; margin-left: 10px; } .actions { margin-bottom: 20px; } /style /head body h1 我的个人知识库/h1 form methodget action/ input typetext nameq value{{ keyword }} placeholder搜索文章标题或内容 button typesubmit搜索/button /form div classactions a href/new classbtn✍️ 新建文章/a /div {% if articles %} ul {% for article in articles %} li a href/article/{{ article[id] }}{{ article[title] }}/a span classmeta{{ article[category] }} · {{ article[updated_at] }}/span /li {% endfor %} /ul {% else %} p还没有任何文章先写第一篇吧。/p {% endif %} /body /html 详情页模板DETAIL_HTML !DOCTYPE html html langzh-CN head meta charsetUTF-8 title{{ article[title] }}/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; max-width: 860px; margin: 40px auto; padding: 0 20px; color: #333; } a { color: #1677ff; text-decoration: none; } .back { display: inline-block; margin-bottom: 20px; } .meta { color: #999; font-size: 13px; margin-bottom: 20px; } .content { background: #fff; padding: 20px 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); line-height: 1.7; } .content pre { background: #f6f8fa; padding: 14px; border-radius: 6px; overflow-x: auto; } .actions { margin-top: 20px; display: flex; gap: 10px; align-items: center; } .btn { padding: 6px 14px; background: #1677ff; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; text-decoration: none; } .danger { background: #f5222d; } /style /head body a href/ classback← 返回首页/a h1{{ article[title] }}/h1 div classmeta {{ article[category] }} · 创建于 {{ article[created_at] }} · 更新于 {{ article[updated_at] }} /div div classcontent{{ content|safe }}/div div classactions a classbtn href/edit/{{ article[id] }}编辑/a form action/delete/{{ article[id] }} methodpost onsubmitreturn confirm(确定删除这篇文章吗) button classbtn danger typesubmit删除/button /form /div /body /html 编辑页模板EDIT_HTML !DOCTYPE html html langzh-CN head meta charsetUTF-8 title{{ 编辑文章 if article else 新建文章 }}/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; max-width: 860px; margin: 40px auto; padding: 0 20px; color: #333; } a { color: #1677ff; text-decoration: none; } .form-group { margin-bottom: 16px; } label { display: block; margin-bottom: 6px; font-weight: 600; } input[typetext], textarea { width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; } textarea { font-family: SFMono-Regular, Consolas, monospace; } .btn { padding: 8px 18px; background: #1677ff; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; text-decoration: none; display: inline-block; } .cancel { background: #999; margin-left: 8px; } /style /head body a href/ classback← 返回首页/a h1{{ 编辑文章 if article else 新建文章 }}/h1 form methodpost div classform-group label标题/label input typetext nametitle value{{ article[title] if article else }} required /div div classform-group label分类/label input typetext namecategory value{{ article[category] if article else }} placeholder默认 /div div classform-group label内容支持 Markdown/label textarea namecontent rows18 required{{ article[content] if article else }}/textarea /div button classbtn typesubmit保存/button a classbtn cancel href/取消/a /form /body /html 最后在文件末尾加入启动入口if __name__ __main__: init_db() app.run(debugTrue, host127.0.0.1, port8000)debugTrue会在代码变动时自动重启服务方便本地开发。端口选择 8000 是为了避免和常见的 5000 端口冲突。6. 运行与功能验证6.1 启动应用在knowledge-base目录下执行python app.py如果环境配置正常终端会输出类似下面的信息* Running on http://127.0.0.1:8000打开浏览器访问http://127.0.0.1:8000就能看到首页。6.2 功能测试流程依次验证以下几个环节新建文章点击“新建文章”填写标题、分类和 Markdown 正文保存后跳转到首页。详情查看点击文章标题确认 Markdown 是否渲染成了 HTML代码块是否有背景色。编辑文章修改标题和正文保存后确认详情页内容更新。搜索验证在搜索框输入一个关键词确认能匹配到标题或内容中包含该词的文章。删除文章点击删除确认弹窗提示确认后列表不再显示该文章。建议测试时写入一条包含 Markdown 表格、代码块、列表的笔记这样可以直观检查extra和codehilite扩展是否生效## 测试笔记 这是一个**粗体**和*斜体*的示例。 | 序号 | 内容 | | --- | --- | | 1 | 表格测试 | python print(Hello Knowledge Base)列表项 A列表项 B### 6.3 查看数据库内容 数据库文件 knowledge.db 会生成在项目目录下。可以使用 SQLite 命令行查看数据 bash sqlite3 knowledge.db在 sqlite3 交互环境中执行SELECT id, title, category, created_at FROM articles;看到类似输出即表示写入成功1 | 测试笔记 | 默认 | 2025-01-01 12:00:00如果本机没有安装 sqlite3 命令可以直接用 Python 查看python -c import sqlite3; print(sqlite3.connect(knowledge.db).execute(select id,title from articles).fetchall())7. 常见问题与排查下面整理几个新手最容易遇到的问题。问题现象常见原因解决思路启动时报 ModuleNotFoundError: No module named flask没有安装 Flask 或虚拟环境未激活执行pip install flask markdown确认当前终端已在虚拟环境中页面样式错乱或黑色文字CSS 未生效或浏览器缓存用无痕模式打开检查 HTML 中 CSS 是否在style标签内Markdown 表格不渲染未启用extra扩展在markdown.markdown()中填写extensions[extra, ...]代码块没有高亮未安装 Pygments执行pip install pygments端口被占用其他程序占用了 8000 端口修改app.run(port8000)为其他端口500 错误日志显示 OperationalError: no such table数据库未初始化检查程序启动时是否执行了init_db()修改数据库后搜索不到结果标题或内容中不存在该关键词清空搜索框重新搜索或用sqlite3命令确认表中数据SQLite 数据库文件被锁定多个进程同时在写数据库Flask 的debugTrue会自动启动 reloader避免重复运行多个python app.py排查任何问题时第一步永远是看终端里的报错日志。Flask 的 debug 模式会输出完整的 Python Traceback定位到具体行号后按图索骥即可。8. 安全、备份与最佳实践8.1 SQL 注入与参数化查询写任何涉及数据库的代码都必须遵守一条底线不要拼接 SQL 字符串。错误示范# 错误示范千万不要这样写 cur db.execute(fSELECT * FROM articles WHERE title {title})如果title的内容是 OR 11这段 SQL 会变成SELECT * FROM articles WHERE title OR 11所有文章都会被查出来造成数据越权。更严重的情况下会被拖库、删表。正确写法是使用?占位符cur db.execute(SELECT * FROM articles WHERE title ?, (title,))这种参数化查询会把用户输入当作纯数据而不是 SQL 的一部分从而避开 SQL 注入问题。8.2 本地使用与公网部署的差异本文示例默认只在127.0.0.1上监听外部机器无法访问安全性很高。如果你想部署到公网需要注意以下几点不要继续使用debugTrue否则服务器代码一旦报错会暴露详细堆栈存在安全风险。增加简单的登录口令比如用 Flask-Login 或 Session 实现单用户认证。使用waitress或gunicorn等生产级 WSGI 服务器而不是 Flask 自带的开发服务器。在防火墙层面限制端口访问范围只允许自己的 IP 访问或者通过 Nginx 加一层访问控制。定期备份数据库文件。8.3 数据备份方案因为整个系统的数据都保存在一个 SQLite 文件中备份策略非常简单。最简单的备份方式是手动复制文件cp knowledge.db knowledge-$(date %Y%m%d).dbWindows 命令为copy knowledge.db knowledge-backup-20250101.db更稳妥的方案是结合 SQLite 的备份 APIsqlite3 knowledge.db .backup knowledge-backup.db这个命令不会断掉正在运行的应用适合在应用运行期间做备份。如果愿意还可以把备份文件同步到云端网盘、Git 私有仓库或 NAS形成多副本。8.4 生产环境最小变更原则如果你把这个系统部署到公司服务器或生产环境任何变更都遵循三个原则先备份再变更。在测试环境验证。变更前做好回滚方案。数据库字段的增删、Python 依赖的升级、Flask 版本的大版本切换都属于高影响操作不要在产品数据上直接冒险。9. 总结与扩展方向这篇文章围绕“个人知识库系统”从零实现了一个完整的最小版本使用 Flask 搭建 Web 服务使用 SQLite 存储数据使用 Python-Markdown 渲染内容实现了文章的新增、查看、编辑、删除和搜索功能。整段代码不到 300 行但涵盖了 Web 开发中最核心的“路由 数据库 模板”三件事。继续往下走可以考虑这些扩展方向标签系统为文章增加tags字段实现多对多标签管理。全文检索引入whoosh或升级为 SQLite FTS5 全文搜索解决中文搜索效率问题。导出功能将文章批量导出为 Markdown 文件或 HTML 文件。多用户支持加入登录注册、Session、权限控制。简化体验加入批量导入、分类统计、置顶功能。动手改造成自己的工具比下载一个“全家桶”知识库系统更清楚它的每一个环节。你可以先跑通这篇文章的代码再按自己的使用习惯加一个标签、改一版样式、加一个导出按钮。等到哪天你发现自己开始翻阅自己的代码来找笔记时这个知识库系统才算真正长成了你自己的形状。如果这篇文章对你有帮助记得收藏备用后面改造成多用户版或全文搜索时还能回来对照着改。