flask搭建轻量级本地小说网站

在Debian系统上部署Flask应用的步骤如下:

1. 安装Python和pip

1
2
sudo apt update
sudo apt install python3 python3-pip

2. 创建虚拟环境并安装依赖

1
2
3
4
mkdir myproject && cd myproject
python3 -m venv venv
source venv/bin/activate
pip install flask gunicorn

3. 网站结构

novel_site/
├── app.py # 主应用文件
├── templates/ # HTML模板
│ ├── index.html # 首页
│ ├── category.html # 分类页
│ ├── reader.html # 小说阅读页
│ └── search.html # 搜索页
├── static/ # 静态资源
│ ├── css/ # CSS文件
│ │ └── style.css # 自定义样式
│ └── js/ # JavaScript文件
└── books/ # 小说存储目录(保持现有结构)
├── category1/
│ ├── book1.txt
│ └── …
└── category2/
└── …

4. 创建Flask应用

创建 app.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from flask import Flask, render_template, send_file, abort, request
from pathlib import Path
from itertools import islice
import os

app = Flask(__name__)
BASE_DIR = Path(r"/home/alin/Desktop/myproject/books/") # 小说根目录

def scan_books(path):
"""递归扫描目录结构"""
structure = {
"name": path.name,
"type": "directory",
"children": [],
"path": str(path.relative_to(BASE_DIR))
}
for item in path.iterdir():
if item.is_dir():
structure["children"].append(scan_books(item))
elif item.suffix == ".txt":
structure["children"].append({
"name": item.stem,
"type": "file",
"path": str(item.relative_to(BASE_DIR))
})
return structure

def validate_path(relative_path):
"""防止路径穿越攻击"""
target = (BASE_DIR / relative_path).resolve()
try:
target.relative_to(BASE_DIR.resolve())
except ValueError:
abort(404)
return target

@app.route("/")
def index():
return render_template("index.html",
tree=scan_books(BASE_DIR),
current_path="")

@app.route("/browse/<path:subpath>")
def browse(subpath):
target = validate_path(subpath)
if not target.is_dir():
abort(404)

breadcrumbs = [("首页", "")]
parts = subpath.split('/')
for i in range(len(parts)):
breadcrumbs.append((parts[i], '/'.join(parts[:i+1])))

return render_template("index.html",
tree=scan_books(target),
current_path=subpath,
breadcrumbs=breadcrumbs)

@app.route("/read/<path:filename>")
def read_book(filename):
filepath = validate_path(filename)
if not filepath.is_file():
abort(404)

# 分页阅读(每页1000行)
page = request.args.get('page', 1, type=int)
per_page = 1000

with open(filepath, 'r', encoding='utf-8') as f:
lines = list(islice(f, (page-1)*per_page, page*per_page))

return render_template("reader.html",
title=filepath.stem,
content=''.join(lines),
page=page,
total_pages=(os.path.getsize(filepath)//(per_page*50)+1))

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>百阅天下</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

.breadcrumb {
padding: 15px;
background: #f5f5f5;
font-size: 14px;
}
.breadcrumb a {
color: #666;
text-decoration: none;
transition: color 0.3s;
}
.breadcrumb a:hover {
color: #007bff;
}

.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
padding: 15px;
}
.grid-item {
background: white;
border-radius: 8px;
padding: 15px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.2s;
position: relative;
}
.grid-item::after {
content: attr(data-type);
display: block;
font-size: 12px;
color: #666;
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid #eee;
}
.grid-item[data-type="directory"] {
background: #f8f9fa;
}
.grid-item a {
text-decoration: none;
color: #333;
font-size: 14px;
word-break: break-word;
}

@media (max-width: 480px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.grid-item {
padding: 12px;
}
.grid-item::after {
font-size: 11px;
}
}
</style>
</head>
<body>
<div class="breadcrumb">
{% for name, path in breadcrumbs %}
/ <a href="{{ url_for('browse', subpath=path) }}">{{ name }}</a>
{% endfor %}
</div>

<div class="grid-container">
{% for child in tree.children %}
<div class="grid-item" data-type="{{ child.type }}">
<a href="{% if child.type == 'directory' %}{{ url_for('browse', subpath=child.path) }}{% else %}{{ url_for('read_book', filename=child.path) }}{% endif %}">
{{ child.name }}
</a>
</div>
{% endfor %}
</div>
</body>
</html>

reader.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<style>
/* 基础通用样式 */
body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
color: #1a1a1a;
}

h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 5px;
}

.pagination {
margin-top: 20px;
text-align: center;
background-color: #f8f9fa;
padding: 10px;
border-radius: 5px;
}

/* 移动端适配 */
@media (max-width: 768px) {
body { padding: 10px; }
h1 { font-size: 1.8rem; }

pre {
max-width: 100%;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
background-color: #f8f9fa;
padding: 10px;
border-radius: 5px;
}
}

/* 分页按钮样式 */
.pagination a {
display: inline-block;
padding: 8px 12px;
margin: 0 5px;
text-decoration: none;
color: #3498db;
border: 1px solid #3498db;
border-radius: 4px;
transition: background-color 0.3s ease;
}

.pagination a:hover,
.pagination a:focus {
background-color: #3498db;
color: white;
}

.current-page {
color: #2c3e50;
font-weight: bold;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>{{ title }}</h1>

<!-- 小说内容区域 -->
<pre>{{ content }}</pre>

<!-- 分页导航 -->
<div class="pagination">
{% if page > 1 %}
<a href="?page={{ page - 1 }}" class="pagination-link">上一页</a>
{% endif %}

<span class="current-page">第 {{ page }} 页 / 共 {{ total_pages }} 页</span>

{% if page < total_pages %}
<a href="?page={{ page + 1 }}" class="pagination-link">下一页</a>
{% endif %}
</div>
</body>
</html>

4. 测试Flask应用

1
2
export FLASK_APP=app.py
flask run --host=0.0.0.0 --port=8089

访问 http://服务器IP:8089 确认应用运行。

5. 使用Gunicorn运行应用

1
gunicorn -w 4 -b 0.0.0.0:5000 app:app

使用 Ctrl+C 停止服务。

6. 重启后再次进入应用

1
2
3
source venv/bin/activate
export FLASK_APP=app.py
flask run --host=0.0.0.0 --port=8089

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!