Flask搭建HTML文档服务器-轻松共享和浏览文档

Flask搭建HTML文档服务器-轻松共享和浏览文档

本文详细介绍如何用Flask搭建一个美观实用的HTML文档服务器,适合团队共享技术文档、产品手册等HTML内容。

一、什么是HTML文档服务器?

HTML文档服务器是一个专门用于托管和展示HTML文件的Web应用程序。想象一下你的团队有很多技术文档(比如用Markdown生成的手册),需要一个集中存放、方便访问的地方 - 这就是HTML文档服务器的作用。

为什么选择HTML而不是PDF?

  1. 无缝阅读体验:HTML文档没有PDF的分页问题,内容连续展示
  2. 响应式设计:自动适应手机、平板和电脑屏幕
  3. 更大预览区域:充分利用屏幕空间展示内容
  4. 轻量快速:加载速度比PDF更快
  5. 内部链接支持:文档内可以方便地添加跳转链接

二、设计思路

我们的服务器基于Python的Flask框架,提供以下核心功能:

  • 目录浏览:像文件管理器一样查看文档结构
  • 文档预览:直接查看HTML内容
  • 智能排序:自动按文档中的数字排序
  • 面包屑导航:清晰展示当前位置
  • 响应式设计:在手机、平板和电脑上都有良好体验

三、搭建步骤详解

步骤1:创建Flask应用

首先,我们需要编写服务器核心代码。这段代码创建了一个Flask应用,包含文档浏览和预览功能:

python 复制代码
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
关键功能解析
  1. 安全防护

    python 复制代码
    # 防止路径遍历攻击
    if not full_path.startswith(base_path):
        abort(403, "禁止访问该路径")

    这段代码确保用户无法访问服务器指定目录外的文件

  2. 智能排序

    python 复制代码
    # 提取文件名中的数字
    num = int(''.join(filter(str.isdigit, name)))

    自动识别类似"Chapter1.html"、"Chapter2.html"这样的文件名并按数字顺序排序

  3. 响应式界面

    使用CSS Grid布局确保在各种设备上都能良好显示:

    css 复制代码
    .grid-container {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        gap: 20px;
    }

步骤2:配置系统服务(开机自启)

我们需要创建一个系统服务,这样服务器就能在系统启动时自动运行:

bash 复制代码
# 创建启动脚本 /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
启用服务
bash 复制代码
# 设置可执行权限
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

四、使用指南

服务器部署完成后:

  1. 将HTML文档放入配置的目录(默认为/home/public/docs
  2. 通过浏览器访问服务器IP地址
  3. 浏览目录结构,点击HTML文件查看内容

五、实际应用场景

  1. 技术团队文档共享:存放API文档、开发手册
  2. 产品说明中心:产品使用指南、教程
  3. 知识库系统:公司内部知识积累和分享
  4. 电子书服务器:托管HTML格式的电子书

功能亮点

  1. 直观的界面

    • 文件夹和文件采用卡片式设计
    • 悬停动画提升交互体验
    • 清晰的图标标识文件类型
  2. 面包屑导航

    复制代码
    根目录 / 技术文档 / API参考

    随时了解当前位置,快速返回上级

  3. 移动端优化

    css 复制代码
    @media (max-width: 600px) {
        .grid-container {
            grid-template-columns: 1fr;
        }
    }

    在手机上自动切换为单列布局

  4. 错误处理

    • 友好的404页面
    • 清晰的权限错误提示
    • 服务器错误日志记录

六、总结

通过这个Flask文档服务器,你可以:

✅ 轻松托管HTML文档集合

✅ 实现团队内文档共享

✅ 享受比PDF更好的阅读体验

✅ 自动排序整理文档

✅ 随时随地通过浏览器访问

这个解决方案轻量高效,只需基本的Python环境即可运行,特别适合中小团队快速搭建内部文档平台。


小贴士:你可以进一步扩展此服务器,比如添加搜索功能、访问权限控制或文档评论功能,打造更完善的文档管理系统。

相关推荐
weixin_4462608512 小时前
HACO:面向动态部署环境的对冲式智能计算可靠多智能体调度框架
后端·python·flask
weixin_BYSJ198720 小时前
「课设设计」springboot校园超市助购系统26449 (附源码)
java·javascript·spring boot·python·django·flask·php
杨超越luckly21 小时前
Agent应用指南:13年与13线——武汉地铁进化史
数据分析·html·agent·可视化·地铁客流
葡萄城技术团队1 天前
HTML元素单元格:用自定义 CellType 扩展 SpreadJS 的显示能力
前端·javascript·html
逝水无殇1 天前
HTML 属性(HTML Attributes)详解
开发语言·前端·html·html5
暮暮祈安2 天前
Celery 新手入门指南
java·数据库·python·flask·httpx
码农学院2 天前
2026年生成式搜索引擎优化对服务业本地化获客帮助的数字化转型路径
前端·搜索引擎·html
小大宇2 天前
python flask框架 SSE流式返回、跨域、报错
开发语言·python·flask
编码者卢布2 天前
【Azure Policy】Policy修正任务为Azure资源添加诊断日志报错问题的调查
microsoft·flask·azure
浮江雾2 天前
Flutter第十六节-----路由管理(2)
android·开发语言·前端·javascript·flutter·html