Flask搭建HTML文档服务器-轻松共享和浏览文档
本文详细介绍如何用Flask搭建一个美观实用的HTML文档服务器,适合团队共享技术文档、产品手册等HTML内容。
一、什么是HTML文档服务器?
HTML文档服务器是一个专门用于托管和展示HTML文件的Web应用程序。想象一下你的团队有很多技术文档(比如用Markdown生成的手册),需要一个集中存放、方便访问的地方 - 这就是HTML文档服务器的作用。
为什么选择HTML而不是PDF?
- 无缝阅读体验:HTML文档没有PDF的分页问题,内容连续展示
- 响应式设计:自动适应手机、平板和电脑屏幕
- 更大预览区域:充分利用屏幕空间展示内容
- 轻量快速:加载速度比PDF更快
- 内部链接支持:文档内可以方便地添加跳转链接
二、设计思路
我们的服务器基于Python的Flask框架,提供以下核心功能:
- 目录浏览:像文件管理器一样查看文档结构
- 文档预览:直接查看HTML内容
- 智能排序:自动按文档中的数字排序
- 面包屑导航:清晰展示当前位置
- 响应式设计:在手机、平板和电脑上都有良好体验
三、搭建步骤详解
步骤1:创建Flask应用
首先,我们需要编写服务器核心代码。这段代码创建了一个Flask应用,包含文档浏览和预览功能:
cat > /opt/html_doc_server.py <<-'EOF'
import os
from flask import Flask, send_file, render_template_string, request, abort
app = Flask(__name__)
# 从环境变量获取HTML目录,默认为当前目录下的html_files
HTML_DIR = os.getenv('HTML_DIR', os.path.abspath('./html_files'))
def get_sorted_files(path):
"""获取指定路径下的文件和目录,并按数字排序"""
items = []
# 遍历目录内容
for name in os.listdir(path):
full_path = os.path.join(path, name)
# 跳过隐藏文件和目录
if name.startswith('.'):
continue
# 如果是目录,类型设为'dir'
if os.path.isdir(full_path):
items.append({
'name': name,
'type': 'dir',
'num': float('inf') # 目录排在文件后面
})
# 如果是HTML文件,提取数字并排序
elif name.endswith('.html'):
try:
# 提取文件名中的数字部分
num = int(''.join(filter(str.isdigit, name)))
except ValueError:
num = float('inf') # 非数字文件名排在最后
items.append({
'name': name,
'type': 'file',
'num': num
})
# 排序:先数字文件(从小到大),然后非数字文件,最后目录
sorted_items = sorted(items, key=lambda x: (x['type'] != 'dir', x['num']))
return sorted_items
@app.route('/')
@app.route('/browse/')
@app.route('/browse/<path:subpath>')
def browse(subpath=''):
"""浏览目录内容(支持多级目录)"""
try:
# 构建完整路径
base_path = os.path.abspath(HTML_DIR)
full_path = os.path.join(base_path, subpath)
# 安全性检查:确保路径在HTML_DIR内
if not os.path.abspath(full_path).startswith(base_path):
abort(403, "禁止访问该路径")
# 检查路径是否存在
if not os.path.exists(full_path):
abort(404, "路径不存在")
# 如果是文件,直接返回文件内容
if os.path.isfile(full_path):
if full_path.endswith('.html'):
return send_file(full_path)
else:
abort(400, "仅支持HTML文件预览")
# 获取排序后的目录内容
items = get_sorted_files(full_path)
# 生成面包屑导航
breadcrumbs = []
parts = subpath.split('/') if subpath else []
current_path = ''
# 添加根目录
breadcrumbs.append({'name': '根目录', 'path': ''})
# 添加中间路径
for i, part in enumerate(parts):
current_path = os.path.join(current_path, part) if current_path else part
breadcrumbs.append({
'name': part,
'path': current_path if i < len(parts) - 1 else None
})
# 渲染目录浏览页面
return render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>文件浏览器 - {{ subpath or '根目录' }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.header {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #eaeaea;
}
h1 {
font-size: 28px;
color: #202124;
margin-bottom: 15px;
}
.breadcrumb {
display: flex;
flex-wrap: wrap;
align-items: center;
font-size: 15px;
margin-bottom: 20px;
background: #eef2f7;
padding: 10px 15px;
border-radius: 8px;
}
.breadcrumb a {
color: #1a73e8;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb span {
margin: 0 8px;
color: #5f6368;
}
.current-dir {
font-size: 16px;
color: #5f6368;
margin-bottom: 20px;
background: #f1f3f4;
padding: 12px 15px;
border-radius: 8px;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.item {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.item:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
.item-icon {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
background: #f1f8ff;
}
.item-icon.dir {
background: #e6f4ea;
}
.item-icon i {
font-size: 48px;
color: #1a73e8;
}
.item-icon.dir i {
color: #34a853;
}
.item-content {
padding: 15px;
}
.item-name {
font-weight: 500;
font-size: 16px;
margin-bottom: 5px;
word-break: break-word;
}
.item-type {
font-size: 13px;
color: #5f6368;
}
a.item-link {
text-decoration: none;
color: inherit;
display: block;
height: 100%;
}
.empty {
text-align: center;
padding: 40px;
grid-column: 1 / -1;
}
.empty i {
font-size: 60px;
color: #dadce0;
margin-bottom: 15px;
}
.footer {
margin-top: 30px;
text-align: center;
color: #5f6368;
font-size: 14px;
padding: 20px;
}
@media (max-width: 600px) {
.grid-container {
grid-template-columns: 1fr;
}
}
</style>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div class="header">
<h1>HTML文件浏览器</h1>
<div class="current-dir">
<strong>当前目录:</strong> /{{ subpath or '根目录' }}
</div>
</div>
<div class="breadcrumb">
{% for bc in breadcrumbs %}
{% if bc.path is not none %}
<a href="/browse/{{ bc.path }}">{{ bc.name }}</a>
{% else %}
<span>{{ bc.name }}</span>
{% endif %}
{% if not loop.last %}
<span>/</span>
{% endif %}
{% endfor %}
</div>
<div class="grid-container">
{% if items %}
{% for item in items %}
<div class="item">
<a href="{% if item.type == 'dir' %}/browse/{{ subpath }}/{{ item.name }}{% else %}/view/{{ subpath }}/{{ item.name }}{% endif %}" class="item-link">
<div class="item-icon {% if item.type == 'dir' %}dir{% endif %}">
<i class="material-icons">{% if item.type == 'dir' %}folder{% else %}description{% endif %}</i>
</div>
<div class="item-content">
<div class="item-name">{{ item.name }}</div>
<div class="item-type">
{% if item.type == 'dir' %}
目录
{% else %}
HTML文件
{% endif %}
</div>
</div>
</a>
</div>
{% endfor %}
{% else %}
<div class="empty">
<i class="material-icons">folder_open</i>
<h3>目录为空</h3>
<p>当前目录下没有HTML文件或子目录</p>
</div>
{% endif %}
</div>
<div class="footer">
服务器目录: {{ html_dir }} | 当前路径: /{{ subpath or '' }}
</div>
</body>
</html>
''', items=items, subpath=subpath, breadcrumbs=breadcrumbs, html_dir=HTML_DIR)
except Exception as e:
abort(500, f"服务器错误: {str(e)}")
@app.route('/view/<path:filepath>')
def view_html(filepath):
"""预览HTML文件(支持多级目录)"""
try:
# 构建完整路径
full_path = os.path.join(HTML_DIR, filepath)
# 安全性检查
if not os.path.abspath(full_path).startswith(os.path.abspath(HTML_DIR)):
abort(403, "禁止访问该路径")
# 检查文件是否存在
if not os.path.exists(full_path):
abort(404, "文件不存在")
# 检查是否是HTML文件
if not full_path.endswith('.html'):
abort(400, "仅支持HTML文件预览")
# 发送文件内容
return send_file(full_path)
except Exception as e:
abort(500, f"服务器错误: {str(e)}")
@app.errorhandler(400)
@app.errorhandler(403)
@app.errorhandler(404)
@app.errorhandler(500)
def handle_error(e):
code = e.code
name = e.name
description = e.description
return render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>{{ code }} {{ name }}</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
padding: 20px;
text-align: center;
}
.error-container {
max-width: 600px;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.error-code {
font-size: 80px;
font-weight: bold;
color: #ea4335;
margin-bottom: 20px;
}
.error-name {
font-size: 24px;
color: #202124;
margin-bottom: 15px;
}
.error-description {
font-size: 18px;
color: #5f6368;
margin-bottom: 30px;
line-height: 1.6;
}
.btn {
display: inline-block;
padding: 12px 24px;
background: #1a73e8;
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 16px;
transition: background 0.3s;
}
.btn:hover {
background: #0d62cb;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">{{ code }}</div>
<div class="error-name">{{ name }}</div>
<div class="error-description">{{ description }}</div>
<a href="/" class="btn">返回首页</a>
</div>
</body>
</html>
''', code=code, name=name, description=description), code
if __name__ == '__main__':
# 确保HTML目录存在
os.makedirs(HTML_DIR, exist_ok=True)
print(f"服务目录: {HTML_DIR}")
# 运行服务器
app.run(host='0.0.0.0', port=80, debug=True)
EOF
关键功能解析
安全防护:
# 防止路径遍历攻击 if not full_path.startswith(base_path): abort(403, "禁止访问该路径")
这段代码确保用户无法访问服务器指定目录外的文件
智能排序:
# 提取文件名中的数字 num = int(''.join(filter(str.isdigit, name)))
自动识别类似"Chapter1.html"、"Chapter2.html"这样的文件名并按数字顺序排序
响应式界面:
使用CSS Grid布局确保在各种设备上都能良好显示:.grid-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
步骤2:配置系统服务(开机自启)
我们需要创建一个系统服务,这样服务器就能在系统启动时自动运行:
# 创建启动脚本 /opt/run_html_doc_server.sh
cat > /opt/run_html_doc_server.sh <<-'EOF'
#!/bin/bash
# 设置HTML文档存储位置
export HTML_DIR=/home/public/read_write/doc
python3 /opt/html_doc_server.py
EOF
# 创建服务
cat <<EOF | sudo tee /etc/systemd/system/html_doc_server.service
[Unit]
Description=HTML文档服务器
[Service]
Type=simple
ExecStart=/usr/bin/bash /opt/run_html_doc_server.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
启用服务
# 设置可执行权限
chmod +x /opt/run_html_doc_server.sh
# 启用并启动服务
sudo systemctl enable html_doc_server
sudo systemctl start html_doc_server
# 检查状态
sudo systemctl status html_doc_server
四、使用指南
服务器部署完成后:
- 将HTML文档放入配置的目录(默认为
/home/public/docs
) - 通过浏览器访问服务器IP地址
- 浏览目录结构,点击HTML文件查看内容
五、实际应用场景
- 技术团队文档共享:存放API文档、开发手册
- 产品说明中心:产品使用指南、教程
- 知识库系统:公司内部知识积累和分享
- 电子书服务器:托管HTML格式的电子书
功能亮点
直观的界面:
- 文件夹和文件采用卡片式设计
- 悬停动画提升交互体验
- 清晰的图标标识文件类型
面包屑导航:
根目录 / 技术文档 / API参考
随时了解当前位置,快速返回上级
移动端优化:
@media (max-width: 600px) { .grid-container { grid-template-columns: 1fr; } }
在手机上自动切换为单列布局
错误处理:
- 友好的404页面
- 清晰的权限错误提示
- 服务器错误日志记录
六、总结
通过这个Flask文档服务器,你可以:
✅ 轻松托管HTML文档集合
✅ 实现团队内文档共享
✅ 享受比PDF更好的阅读体验
✅ 自动排序整理文档
✅ 随时随地通过浏览器访问
这个解决方案轻量高效,只需基本的Python环境即可运行,特别适合中小团队快速搭建内部文档平台。
小贴士:你可以进一步扩展此服务器,比如添加搜索功能、访问权限控制或文档评论功能,打造更完善的文档管理系统。