用Python创建简易网站图文教程 前言很多人第一次听到「用 Python 建网站」脑子里立刻浮现出装一堆东西、配一整套环境的画面。其实从「浏览器里能看到一个页面」到「一个能点、能跳转的站」中间的路是一段一段走出来的标准库能带你走完第一步真正让页面变成「站」的是后面加上的路由、模板和静态文件组织方式。另一个常见误解是把「建网站」和「写 API」当成同一件事。API 面向的是程序调用方返回的通常是 JSON网站面向的是人最终要吐出一整页 HTML 交给浏览器渲染。本文讲的是后者——一个有人能看的网页站点。API 的设计思路属于另一类话题本文不展开。本文按三步走先用标准库http.server把页面跑起来理解 HTTP 请求与响应这条最底层链路再引出 WSGI 这个「服务器」与「应用」之间的接口约定解释为什么框架可以换来换去最后用一个轻量框架把路由、模板、静态文件三件套补齐并讲清「服务端渲染」到底指什么。文中涉及第三方框架的部分都需要自行安装安装命令、版本号与具体参数一律以对应框架的官方文档为准本文只讲结构与思路。一、第一步用 http.server 让浏览器看到第一行字HTTP 协议的本质是一问一答浏览器发来一段文本请求服务器回一段文本响应。响应由三部分组成——状态行、若干响应头、以及一个空行之后的响应体。状态行里那个200表示成功404表示资源不存在。标准库里的http.server模块允许我们直接写一个处理类把响应的这三部分自己拼出来。# 适用于 Python 3.7from http.server import BaseHTTPRequestHandler, HTTPServerclass HelloHandler(BaseHTTPRequestHandler):def do_GET(self):if self.path /:body h1你好Python 网站/h1.encode(utf-8)self.send_response(200)self.send_header(Content-Type, text/html; charsetutf-8)self.send_header(Content-Length, str(len(body)))self.end_headers()self.wfile.write(body)else:self.send_error(404, 页面不存在)if __name__ __main__:server HTTPServer((127.0.0.1, 8000), HelloHandler)print(请在浏览器打开 http://127.0.0.1:8000/)server.serve_forever()几个关键点send_response(200)会先写入状态行并把Server与Date两个响应头加进缓冲区后面两次send_header追加自定义头end_headers()负责写入那个表示头部结束的空行——在 3.3 之后头部是缓冲的不调用end_headers()响应发不出去最后往self.wfile写响应体。Content-Length是响应体的字节数不是字符数。中文一个字符占 3 个字节所以必须先encode(utf-8)再取长度否则浏览器会等一段永远等不到的数据。这段代码已经是一个「能访问的页面」但它还不是「网站」每加一个网址就要多写一个if页面内容硬编码在 Python 字符串里图片、CSS 文件没地方放。二、从「一个脚本」到「一个站」WSGI 接口约定如果每个网站都自己实现一套「读请求 → 拼响应」的逻辑那么服务器负责监听端口、解析 HTTP和框架负责业务逻辑就绑死了。WSGIWeb Server Gateway InterfaceWeb 服务器网关接口解决的正是这个耦合问题。WSGI 规定了一个极其简洁的调用约定应用是一个可调用对象接收两个参数并返回一个可迭代对象。第一个参数是environ一个字典装着这次请求的全部信息比如REQUEST_METHOD请求方法、PATH_INFO路径、QUERY_STRING查询串。第二个参数是start_response应用用它来声明状态行和响应头。返回值是可迭代对象每个元素是一段响应体的字节串。标准库的wsgiref提供了这个接口的参考实现。下面这段代码和上一节功能相同但遵循了 WSGI 约定。# 适用于 Python 3.7from wsgiref.simple_server import make_serverdef app(environ, start_response):path environ[PATH_INFO]if path /:status 200 OKbody h1你好Python 网站/h1.encode(utf-8)else:status 404 Not Foundbody h1页面不存在/h1.encode(utf-8)headers [(Content-Type, text/html; charsetutf-8),(Content-Length, str(len(body))),]start_response(status, headers)return [body]if __name__ __main__:with make_server(127.0.0.1, 8000, app) as httpd:httpd.serve_forever()好处是只要遵守这个约定任何 WSGI 服务器都能跑你的应用任何 WSGI 应用也能跑在任何服务器上。生产环境中常见的 Gunicorn、uWSGI 这类服务器都吃这一套接口。框架负责把业务代码包装成 WSGI 应用服务器负责把 HTTP 翻译成environ。不过手写if path ...分派还是太笨。真正的框架把这部分抽象成「路由」——你把「某个路径 某个方法」映射到一个函数上。三、用轻量框架补齐三件套路由、模板、静态文件一个网站最小可用需要三样东西把路径映射到处理函数路由、把数据和网页骨架拼起来模板、把 CSS/图片/JS 这类不变的资源单独提供给浏览器静态文件。主流轻量框架都自带这三样。下面以 Flask 为例安装命令与接口细节请以其官方文档为准。# 安装方式以该框架官方文档为准pip install flask# 适用于 Python 3.8需先安装 Flaskfrom flask import Flask, render_templateapp Flask(__name__)POSTS [{title: 第一篇, body: 网站跑起来了。},{title: 第二篇, body: 模板也接上了。},]app.get(/)def index():return render_template(index.html, postsPOSTS)app.get(/post/int:pid)def post_detail(pid):if pid 0 or pid len(POSTS):return 没有这篇文章, 404return render_template(post.html, postPOSTS[pid])if __name__ __main__:app.run(debugTrue)app.get(...)是 Flask 2.0 之后的写法等价于更早版本的app.route(/, methods[GET])。路径里用尖括号声明的片段是路径参数上面的代码里把文章编号声明成了整数框架会自动把 URL 里对应那一段转成整数再传进函数不用自己切字符串。模板是一个带占位符的 HTML 文件。框架会读它、把数据填进去、生成最终 HTML!-- templates/index.html --!DOCTYPE htmlhtml langzhheadmeta charsetutf-8title我的网站/title/headbodyh1文章列表/h1ul{% for post in posts %}lia href/post/{{ loop.index0 }}{{ post.title }}/a/li{% endfor %}/ul/body/html静态文件放在框架约定的目录Flask 默认是static/通过固定的 URL 前缀访问link relstylesheet href/static/style.css框架会把/static/style.css直接映射到磁盘上的static/style.css不经过你的视图函数。这样做的好处是模板只负责结构CSS 只负责样式同一个样式文件被浏览器缓存一次全站的页面都受益。四、服务端渲染到底是什么上面这套流程有个名字叫服务端渲染SSRServer-Side Rendering。它的意思很直白HTML 是在服务器上完整拼好之后才发给浏览器的。以访问首页为例服务器端的动作顺序是收到 GET 请求 → 路由匹配到index函数 → 读取POSTS数据 → 用模板把数据渲染成一段完整 HTML → 把这段 HTML 作为响应体发出。浏览器收到的已经是一张排好版的页面它不需要再执行多少脚本就能显示内容。对比项服务端渲染SSR客户端渲染CSRHTML 由谁生成服务器上的模板引擎浏览器里的 JavaScript首屏可见速度通常更快响应即内容需等脚本下载并执行完搜索引擎抓取直接看到完整内容依赖爬虫执行脚本的能力服务器压力每次请求都要渲染压力大静态资源可走缓存压力小典型场景内容型站点、后台管理页交互复杂的单页应用需要注意的是SSR 并不等于「不能用 JavaScript」。它只描述首屏 HTML 从哪来。页面加载完之后你依然可以在浏览器里用脚本做局部更新——两种渲染方式经常混着用。常见坑点1. 把http.server当生产服务器❌ 直接拿标准库的那个服务器类对外提供正式服务。 ✅ 官方文档对此有明确警告不推荐用于生产环境因为它只实现了最基本的安全检查。它只适合测试和临时用途要对外就用成熟的 WSGI 生产服务器。2. 把路由写成一路if❌ 每加一个页面就复制一份处理函数、再补一个if path ...分支。 ✅ 用框架的路由装饰器把「路径 方法」声明式地绑到函数上路径参数由框架解析。3. 模板里直接写业务计算❌ 在{% for %}里做数据库查询、算复杂统计。 ✅ 模板只管展示数据和计算在视图函数里准备好再作为参数传进去。4. 静态文件的 URL 和磁盘路径混淆❌ 以为浏览器访问/static/style.css会命中项目根目录下的static/style.css于是在别处又放了一份。 ✅ 以框架约定的目录为准常见约定是static/URL 前缀是框架映射出来的虚拟路径照约定放文件就行。5. 中文页面不声明编码❌ 返回中文 HTML 却不写Content-Type浏览器按本地编码猜出现乱码。 ✅ 响应头写text/html; charsetutf-8HTML 里的meta charset也写一遍两者保持一致。6. 以为服务端渲染就不能用 JavaScript❌ 担心「选了 SSR 就再也不能做局部更新」于是把交互逻辑全塞到服务器。 ✅ 服务端渲染只描述首屏 HTML 从哪里来页面加载完之后照样可以在浏览器里做局部更新两种方式经常混用。总结阶段用什么你得到什么缺什么起步http.server一个能响应的地址路由、模板、静态文件都要手写解耦wsgiref WSGI 约定服务器与应用解耦业务分派仍是手写的if成型轻量框架路由、模板、静态文件三件套需要按官方文档安装与配置从「一个脚本」到「一个站」本质上是把重复劳动一层层收进约定里HTTP 的细节收进服务器分派逻辑收进路由页面拼接收进模板。理解 WSGI 这层接口你就明白了为什么 Python 的 Web 框架能百花齐放却又能共用同一批服务器理解服务端渲染你就明白了「网站」和「API」在交付物上的根本差别。