Python个人博客程序开发实例框架设计
前言写博客程序的重点不在能渲染出一个页面而在框架的分层请求怎么进来、路由怎么匹配、业务逻辑放哪、模板怎么渲染、数据怎么存取。把这几件事拆清楚代码就只有几百行却能轻松加功能拆不清楚两千行也是一团浆糊。这里有一个常见误解以为框架必须用第三方 Web 框架。其实标准库自带的http.server加上sqlite3、string.Template就足以搭出一个结构完整的小博客而且因为少了大框架的抽象层每个环节都看得见。本文用纯标准库实现一个分层清晰的博客骨架路由表、视图函数、模板、数据访问四个部分各司其职。需要事先声明边界http.server的定位是开发调试和教学演示它没有经过生产级的并发与安全加固。真要对外提供服务应该换成 WSGI 服务器配合成熟框架。本文的目的是让你理解一个 Web 框架到底在做什么而不是让你拿它去扛流量。另外安全上只说防御侧所有从页面输入进来的内容输出到 HTML 之前必须转义否则访问者提交的脚本会在别人浏览器里执行跨站脚本XSS所有 SQL 一律用参数化占位符绝不拼接。如果你手上的老教程还是 Python 2 的写法要注意BaseHTTPServer、SimpleHTTPServer这两个模块名在 Python 3 里已经不存在了统一成了http.serverprint xxx这种语句写法也必须改成print(xxx)。一、分层的职责层职责本文对应路由把「方法 路径」映射到处理函数Router类视图组装数据、返回响应view_index等函数模板把数据套进 HTMLstring.Template数据访问读写数据库connect()/init_db()服务器适配解析请求、写回响应BlogHandler关键约束视图函数不该知道self.wfile是什么。视图只返回一个状态码 内容类型 正文的响应对象由适配层负责真正写回 socket。这样视图可以被单元测试直接调用不需要起服务器。二、路由用正则匹配路径路由表就是一张列表每项是「HTTP 方法 正则 处理函数」。匹配时把路径和解码后的查询串分开路径交给正则做fullmatch查询串用urllib.parse.parse_qs解析成字典。用正则的命名分组可以把路径里的动态片段直接变成处理函数的参数比如/post/(?Ppost_id\d)匹配/post/42时处理函数就收到{post_id: 42}。三、模板为什么用string.Template拼接 HTML 有两种常见写法str.format和string.Template。推荐后者。原因是str.format会把用户内容里的花括号当成占位符来解析——用户只要在正文里写一个{渲染就会抛KeyError或IndexError。string.Template用的是$name语法且只在模板文本里解析占位符替换进去的值里出现$也不会被二次解释写法占位符用户内容含{或$时str.format{}/{0}可能抛KeyError%格式化%s需要转义%string.Template$name不受影响当然模板安全的前提是先转义再拼。html.escape()会把、、以及引号变成实体用户提交的脚本就只能当纯文本显示。四、完整实现以下代码存成blog.py即可运行需要 Python 3.8全部依赖标准库# 适用于 Python 3.8import htmlimport reimport sqlite3from dataclasses import dataclass, fieldfrom datetime import datetime, timezonefrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServerfrom string import Templatefrom urllib.parse import parse_qs, urlparseDB_PATH blog.dbSCHEMA CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT,title TEXT NOT NULL,body TEXT NOT NULL,created_at TEXT NOT NULL);PAGE Template(!doctype htmlhtml langzhheadmeta charsetutf-8title$title/title/headbodyh1$title/h1$content/body/html)NEW_FORM form methodpost action/newp标题: input nametitle size40/pp正文: textarea namebody rows8 cols60/textarea/ppbutton typesubmit发布/button/p/formdataclassclass Response:status: intcontent_type: strbody: str headers: dict field(default_factorydict)def connect():conn sqlite3.connect(DB_PATH, timeout5)conn.row_factory sqlite3.Rowreturn conndef init_db():with connect() as conn:conn.executescript(SCHEMA)def now_iso():return datetime.now(timezone.utc).isoformat(timespecseconds)# ---------------- 视图只返回 Response不碰 socket ----------------def view_index(params, query, form):with connect() as conn:rows conn.execute(SELECT id, title, created_at FROM posts ORDER BY id DESC).fetchall()if rows:items .join(lia href/post/{id}{title}/a - {ts}/li.format(idrow[id],titlehtml.escape(row[title]),tshtml.escape(row[created_at]))for row in rows)else:items li还没有文章/licontent ul{}/ulpa href/new写一篇/a/p.format(items)return Response(200, text/html; charsetutf-8,PAGE.substitute(title我的博客, contentcontent))def view_post(params, query, form):with connect() as conn:row conn.execute(SELECT title, body, created_at FROM posts WHERE id ?,(params[post_id],)).fetchone()if row is None:return Response(404, text/plain; charsetutf-8, 文章不存在)body html.escape(row[body]).replace(\n, br)content p{}/pp{}/ppa href/返回首页/a/p.format(html.escape(row[title]), body)# title 里也是用户内容同样必须转义return Response(200, text/html; charsetutf-8,PAGE.substitute(titlehtml.escape(row[title]),contentcontent))def view_new(params, query, form):if not form:return Response(200, text/html; charsetutf-8,PAGE.substitute(title写文章, contentNEW_FORM))title form.get(title, ).strip()body form.get(body, ).strip()if not title or not body:hint p标题和正文都不能为空。/preturn Response(400, text/html; charsetutf-8,PAGE.substitute(title写文章, contentNEW_FORM hint))with connect() as conn:conn.execute(INSERT INTO posts (title, body, created_at) VALUES (?, ?, ?),(title, body, now_iso()))# 303 让浏览器改用 GET 重新请求避免刷新时重复提交return Response(303, text/plain; charsetutf-8, 已创建,{Location: /})# ---------------- 路由 ----------------class Router:def __init__(self):self.routes []def add(self, method, pattern, func):self.routes.append((method, re.compile(pattern), func))def match(self, method, path):for allowed, regex, func in self.routes:if allowed ! method:continuefound regex.fullmatch(path)if found:return func, found.groupdict()return None, Nonerouter Router()router.add(GET, r/, view_index)router.add(GET, r/post/(?Ppost_id\d), view_post)router.add(GET, r/new, view_new)router.add(POST, r/new, view_new)# ---------------- 服务器适配 ----------------class BlogHandler(BaseHTTPRequestHandler):protocol_version HTTP/1.1def do_GET(self):self._dispatch(GET)def do_POST(self):self._dispatch(POST)def _read_form(self):length int(self.headers.get(Content-Length) or 0)if length 0:return {}raw self.rfile.read(length).decode(utf-8, replace)return {k: v[0] for k, v in parse_qs(raw, keep_blank_valuesTrue).items()}def _dispatch(self, method):parsed urlparse(self.path)func, params router.match(method, parsed.path)if func is None:self._send(Response(404, text/plain; charsetutf-8, 404 Not Found))returnquery parse_qs(parsed.query, keep_blank_valuesTrue)form self._read_form() if method POST else {}try:response func(params, query, form)except Exception as exc: # 别把堆栈吐给浏览器print(处理 {} 出错: {!r}.format(self.path, exc))response Response(500, text/plain; charsetutf-8, 服务器内部错误)self._send(response)def _send(self, response):payload response.body.encode(utf-8)self.send_response(response.status)self.send_header(Content-Type, response.content_type)self.send_header(Content-Length, str(len(payload)))for key, value in response.headers.items():self.send_header(key, value)self.end_headers()if self.command ! HEAD:self.wfile.write(payload)def log_message(self, fmt, *args):print([{}] {}.format(self.address_string(), fmt % args))def main():init_db()# 只绑定本机回环地址不要随手写 0.0.0.0server ThreadingHTTPServer((127.0.0.1, 8000), BlogHandler)print(已启动请访问 http://127.0.0.1:8000/)try:server.serve_forever()except KeyboardInterrupt:server.shutdown()if __name__ __main__:main()常见坑点1. 把用户输入直接拼进 HTML❌h2{}/h2.format(row[title])✅h2{}/h2.format(html.escape(row[title]))标题里写一段脚本就会在所有访客的浏览器里执行这就是 XSS。转义是输出侧的底线。2. 多线程共享一个sqlite3连接❌ 在模块顶层建一个全局conn所有请求复用 ✅ 每个请求在自己的视图里connect()用完即关with语句结束就提交sqlite3的连接不能跨线程使用ThreadingHTTPServer下会抛ProgrammingError。3. HTTP/1.1 不写Content-Length❌protocol_version HTTP/1.1但响应里没有Content-Length✅ 每次响应都先算好字节数再send_header(Content-Length, ...)HTTP/1.1 靠Content-Length判断响应结束缺失会让浏览器一直等页面卡住不渲染。4. 直接int(Content-Length)❌length int(self.headers[Content-Length])✅int(self.headers.get(Content-Length) or 0)GET 请求和小体积 POST 都可能没有这个头取不到时int(None)会抛TypeError。5. POST 成功后直接返回页面内容❌ 处理完创建逻辑就return一个 200 的 HTML ✅ 返回 303 并带上Location让浏览器重新用 GET 拉首页 否则用户按 F5 会重复提交表单同一篇文章被写进去好几次。6. 服务器绑定0.0.0.0❌ThreadingHTTPServer((0.0.0.0, 8000), Handler)✅ 开发时写127.0.0.1只监听本机0.0.0.0会把服务暴露给同网段所有人而这个服务器没有任何鉴权。7. 把异常堆栈写回浏览器❌except Exception as exc: self.wfile.write(str(exc).encode())✅ 前端只回一句通用错误详情打到服务端日志 堆栈里可能包含文件路径、数据库结构甚至配置信息。8. 用str.format渲染含用户内容的模板❌TEMPLATE.format(contentuser_text)用户输入里有{}就崩 ✅ 用string.Template的$name占位符或先转义str.format会把替换值之外的整段文本都当模板解析。总结问题错误做法正确做法输出用户内容直接拼 HTML先html.escape数据库连接全局共享每请求一个连接响应长度不写Content-Length按字节数显式声明表单提交后返回 200 页面返回 303 重定向监听地址0.0.0.0127.0.0.1错误处理堆栈回显日志记录前端通用提示博客程序的骨架就四样路由表、视图函数、模板、数据访问。把这四层分开之后你会发现成熟框架里的route、view、template、model无非是同一套分工的工业化版本。理解了这层对应关系再去学任何 Web 框架都会快得多。

相关新闻

三个月前我还在熬夜做开题答辩PPT,现在30分钟就能搞定

三个月前我还在熬夜做开题答辩PPT,现在30分钟就能搞定

一、分点拆解:原来熬夜耗在哪,现在省在哪 手搓开题PPT的时间成本通常不在"写",而在四类重复劳动:从空白页搭框架 封面、目录、选题依据、现状、内容、方法、创新、进度、参考……每页标题层级要重排。正确做法是开题报告…

2026/10/15 6:52:50 阅读更多 →
工程机械钢丝绳选型与维护全攻略

工程机械钢丝绳选型与维护全攻略

1. 工程机械钢丝绳选型核心逻辑打桩机和旋挖钻机作为重型工程机械,其钢丝绳选型直接关系到设备安全性和施工效率。我在桩基工程行业摸爬滚打十二年,处理过上百起钢丝绳相关案例,发现90%的早期断裂事故都源于选型不当。不同于普通起重设备&…

2026/10/15 6:52:50 阅读更多 →
计算机图形学:三角形绘制与帧缓冲区技术详解

计算机图形学:三角形绘制与帧缓冲区技术详解

1. 三角形绘制的基本原理在计算机图形学中,三角形是最基础的图元之一。几乎所有复杂的3D模型最终都会被分解为三角形网格进行渲染。这种选择并非偶然——三角形具有几个关键优势:三个点必然共面(不会产生扭曲)、边缘检测算法简单高…

2026/10/15 6:52:50 阅读更多 →

最新新闻

正弦波转方波方案对比:比较器、施密特、运放与专用芯片选型指南

正弦波转方波方案对比:比较器、施密特、运放与专用芯片选型指南

简介:这份PPT资料面向电子工程、测控仪器相关专业的学生与工程师,围绕正弦波转方波这一常见电路设计问题,系统比较了四种实现方案并给出详细电路图。内容以220V/50Hz正弦波转10V/50Hz方波为设计目标,依次讲解由555定时器构成的施密…

2026/10/15 10:14:14 阅读更多 →
无可挑剔执行法:用清单与检查提升工作交付可靠性

无可挑剔执行法:用清单与检查提升工作交付可靠性

1. 项目概述与“impeccable”的重新定义1.1 为什么叫“impeccable”:从词源到执行哲学“Impeccable”这个词,字面意思是“无可挑剔的、完美无瑕的”,但它真正的词源很有意思——拉丁语“peccare”是“犯错”的意思,加上否定前缀“…

2026/10/15 10:14:14 阅读更多 →
SpringBoot整合NapCatQQ实现高效QQ机器人开发

SpringBoot整合NapCatQQ实现高效QQ机器人开发

1. 项目概述:当QQ机器人遇上SpringBoot去年接手公司内部自动化通知系统改造时,我第一次将NapCatQQ与SpringBoot整合。这个组合带来的开发效率提升让我印象深刻——原本需要两周完成的客服机器人原型,用这个方案三天就跑了通。NapCatQQ作为轻量…

2026/10/15 10:14:14 阅读更多 →
从夹逼定理到残差连接:AI模型的边界感、灰度地带与泛化真相

从夹逼定理到残差连接:AI模型的边界感、灰度地带与泛化真相

1. 从夹逼定理说起:为什么AI模型的“边界感”比准确率更重要第一次听到有人把夹逼定理和残差连接放在一起聊,是在一个做数值计算的老哥那里。他当时正在调一个物理仿真模型,发现神经网络在拟合某些函数时,输出值总是会“飘”出理论…

2026/10/15 10:14:14 阅读更多 →
OpenClaw 接入 Qwen 免费层:qwen-portal OAuth 插件启用、设备码认证与模型切换完全指南

OpenClaw 接入 Qwen 免费层:qwen-portal OAuth 插件启用、设备码认证与模型切换完全指南

人工智能大模型LLM 网关AI Agent即时通讯 【免费下载链接】openclaw-zero-token OpenClaw: Use All Major AI Models NO API Token! Claude/ChatGPT/Gemini/DeepSeek/Doubao/Grok/Qwen/Manus/Kimi 项目地址: https://gitcode.com/gh_mirrors/op/openclaw-zero-token…

2026/10/15 10:14:14 阅读更多 →
HDFS面试避坑指南:从NameNode内存到DataNode心跳的实战解析

HDFS面试避坑指南:从NameNode内存到DataNode心跳的实战解析

1. 为什么“HDFS面试基础知识点”这个标题背后藏着一场隐性淘汰赛刚带完一批某高校实验室的分布式系统实训,有个细节让我印象深刻:A同学在模拟面试中被问到“HDFS写入流程里,Client和NameNode交互几次?DataNode之间要不要通信&…

2026/10/15 10:13:13 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →