
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统与网络知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》 《cpolar知识学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

项目进度常常先记在一张CSV表里.文件放在Ubuntu上,换到Mac查看时,通常要再复制一份.这次我把CSV做成一个简单的网页:Ubuntu负责读取文件,Mac通过星空组网地址打开报表,文件改完后刷新页面就能看到更新.整个项目只有
server.py和report.csv两个文件,使用Python标准库,不安装第三方包.本次使用模拟项目记录,验证范围是Mac到Ubuntu组网地址的访问、文件更新和服务清理.这个网页没有单独的登录鉴权,能够访问该端口的组网成员都可查看内容,适合临时演示.

目录
- 1.从官网登录入口,确认两台设备在线
- 2.在Ubuntu检查环境,再创建项目
- 3.写一个读取CSV的网页服务
- 4.启动服务,从Mac打开报表
- 5.修改Ubuntu文件,验证Mac刷新结果
- 6.打不开或数据不对时,按现象排查
- 7.验证结束后停止服务,删除演示项目
1.从官网登录入口,确认两台设备在线
打开星空组网官网,进入账号登录页.已有账号可直接登录,没有账号则从页面进入注册流程.下图是本次打开的空白登录表单,未填写账号或密码.

本次浏览器已有后台登录状态,后续沿用该会话.进入后台首页后,"组网状态"显示组网成功、两台设备在线.截图仅保留状态区域,账号信息没有放进配图.

设备列表的入口是"设备管理 → 成员列表".本次核对了Mac和Ubuntu-Board的虚拟IP,两端客户端均为 6.1.0,分别使用已有成员.按照官方设备与成员管理说明,多台长期在线设备应使用各自的成员账号.客户端还未安装的设备,可以先参考Linux 使用说明或Mac 使用说明完成接入.
| 设备或配置 | 本次实际值 | 分工 |
|---|---|---|
| Ubuntu-Board | Ubuntu 22.04 LTS,Python 3.10.12 | 保存 CSV,运行网页服务 |
| Ubuntu 组网 IP | 192.168.188.5 |
服务监听地址 |
| Mac 组网 IP | 192.168.188.1 |
浏览器访问、刷新验证 |
| 网页端口 | 18101 |
本次检查为空闲端口 |
这些地址来自本次设备,复现时请换成自己的组网IP.浏览器中的地址应指向运行Python服务的 Ubuntu.
2.在Ubuntu检查环境,再创建项目
打开Ubuntu终端,先确认系统、Python、组网网卡地址和端口:
bash
lsb_release -ds
python3 --version
ip -4 -brief addr show StarVPN
ss -ltn '( sport = :18101 )'
本次输出是 Ubuntu 22.04 LTS、Python 3.10.12,StarVPN 网卡具有 192.168.188.5/24 地址。ss 只有表头,没有监听记录,说明检查时端口没有被占用.网卡状态中的 UNKNOWN 要结合地址和实际访问判断.

新建一个独立目录;如果同名目录已经存在,先检查内容,不要覆盖已有文件.
bash
mkdir ~/starvpn-csv-report-20261001
cd ~/starvpn-csv-report-20261001
创建模拟数据文件:
bash
cat > report.csv <<'CSV'
项目,负责人,状态,备注
登录页面,小林,已完成,账号入口已确认
报表接口,小周,进行中,等待页面验收
数据校验,小陈,待开始,"检查中文、逗号"
发布说明,小林,待开始,整理使用步骤
CSV
CSV 第一行是表头.字段里包含英文逗号 , 时,需要用双引号包住整个字段.程序使用标准库 csv.reader 解析,并以 newline="" 打开文件;这些写法可对照Python CSV 文档.
3.写一个读取CSV的网页服务
把以下完整代码保存为同目录的 server.py:
python
import argparse
import csv
import logging
from datetime import datetime
from html import escape
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
REPORT = Path(__file__).with_name("report.csv")
DEFAULT_PORT = 18101
MAX_FILE_BYTES = 64 * 1024
def render_report(path: Path) -> bytes:
if path.stat().st_size > MAX_FILE_BYTES:
raise ValueError("CSV 超过 64 KiB,请先缩小演示数据")
with path.open(encoding="utf-8-sig", newline="") as file:
rows = [row for row in csv.reader(file, strict=True) if row]
if not rows or not any(rows[0]):
raise ValueError("CSV 缺少表头")
if any(len(row) != len(rows[0]) for row in rows[1:]):
raise ValueError("CSV 数据行与表头列数不一致")
head = "".join(f'<th scope="col">{escape(cell)}</th>' for cell in rows[0])
body = "".join(
"<tr>" + "".join(f"<td>{escape(cell)}</td>" for cell in row) + "</tr>"
for row in rows[1:]
)
if not body:
body = f'<tr><td colspan="{len(rows[0])}">暂无数据</td></tr>'
updated = datetime.fromtimestamp(path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
return f'''<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>项目进度报表</title>
<style>
body{{margin:0;background:#f4f5f0;color:#24332e;font:16px/1.7 system-ui,sans-serif}}
main{{max-width:960px;margin:64px auto;padding:36px;background:white;border:1px solid #dce2da}}
header{{display:flex;justify-content:space-between;align-items:center;gap:24px}}
h1{{font-size:32px;margin:4px 0 12px}}p{{color:#617168;margin:8px 0}}
a{{display:inline-block;padding:10px 20px;background:#245844;color:white;text-decoration:none}}
a:focus-visible{{outline:3px solid #f2ac42;outline-offset:4px}}
.table-wrap{{overflow:auto;margin:28px 0}}table{{width:100%;border-collapse:collapse;text-align:left}}
caption{{text-align:left;font-weight:600;padding-bottom:12px}}
th,td{{padding:15px 18px;border-bottom:1px solid #e3e9e1}}th{{background:#edf2eb}}
small{{color:#617168}}@media(max-width:640px){{main{{margin:20px;padding:20px}}header{{flex-wrap:wrap}}}}
</style>
<main>
<header><div><small>CSV REPORT / 跨设备查看</small><h1>项目进度报表</h1>
<p>刷新页面即可读取 Ubuntu 上的最新文件。</p></div><a href="/">刷新报表</a></header>
<p>数据文件:report.csv | 记录数:{len(rows) - 1} | 文件更新时间:{updated}</p>
<div class="table-wrap"><table><caption>模拟项目记录</caption>
<thead><tr>{head}</tr></thead><tbody>{body}</tbody></table></div>
<small>演示数据,非真实业务记录。页面仅提供查看。</small>
</main></html>'''.encode("utf-8")
class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
if self.path != "/":
self.send_error(404, "Not found")
return
try:
body = render_report(REPORT)
except (OSError, UnicodeError, csv.Error, ValueError):
logging.exception("读取 report.csv 失败")
self.send_error(500, "Cannot read report.csv; check server log")
return
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.end_headers()
self.wfile.write(body)
def main() -> None:
parser = argparse.ArgumentParser(description="临时 CSV 报表页")
parser.add_argument("--bind", required=True, help="本机星空组网 IPv4 地址")
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
args = parser.parse_args()
if not 1 <= args.port <= 65535:
parser.error("端口必须在 1 到 65535 之间")
try:
with ThreadingHTTPServer((args.bind, args.port), Handler) as server:
print(f"报表地址:http://{args.bind}:{args.port}/", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
print("报表服务已停止", flush=True)
except OSError as error:
parser.exit(2, f"启动失败:{error}\n")
if __name__ == "__main__":
main()
程序固定读取脚本旁边的 report.csv,只开放首页 /,没有目录浏览或任意文件下载功能.CSV 按 UTF-8 读取,也兼容带 BOM 的 UTF-8 文件;转成网页前,每个字段经过 escape 处理.每次请求都会重新读取文件,并返回 Cache-Control: no-store.
为了让文件出错时容易排查,程序检查表头和列数,限制演示CSV大小为64KiB.读取失败时,浏览器收到 HTTP 500,具体异常留在Ubuntu终端.只有表头、没有数据的文件会显示"暂无数据".
保存后核对两个文件,并检查 Python 语法:
bash
ls -lh server.py report.csv
python3 -c "import ast; from pathlib import Path; ast.parse(Path('server.py').read_text()); print('Python syntax: OK')"
cat report.csv

4.启动服务,从Mac打开报表
在 Ubuntu 的项目目录里运行:
bash
python3 server.py --bind 192.168.188.5 --port 18101
出现 报表地址:http://192.168.188.5:18101/ 后,保持终端运行.这里使用 Ubuntu 自己的组网 IP 作为监听地址.本次没有修改防火墙配置.
在已经接入同一组网的Mac 上,可以先用命令检查 HTTP 响应:
bash
curl --noproxy '*' --max-time 5 -f -o /dev/null -w 'HTTP %{http_code}\n' http://192.168.188.5:18101/
本次返回HTTP200,Ubuntu 终端也记录了来自Mac组网地址 192.168.188.1 的请求.

接着在 Mac 浏览器中输入 http://192.168.188.5:18101/.初版页面显示4条记录,"报表接口"一行的状态为"进行中",备注为"等待页面验收".中文内容也能正常显示.

5.修改Ubuntu文件,验证Mac刷新结果
另开一个 Ubuntu 终端,进入同一目录,修改模拟记录并查看文件:
bash
cd ~/starvpn-csv-report-20261001
sed -i 's/报表接口,小周,进行中,等待页面验收/报表接口,小周,已完成,Mac 访问验证通过/' report.csv
cat report.csv
ss -ltn '( sport = :18101 )'
这条 sed 命令只针对本文给出的模拟行.处理自己的CSV时,建议直接编辑文件或使用CSV读写工具,避免误替换其他记录.
下面的终端截图显示,文件中的状态已变为"已完成",服务仍在 192.168.188.5:18101 上监听.查看 ss 时应读 Local Address:Port 列,右侧的 Peer Address:Port 不是服务的监听地址.

回到Mac,点击"刷新报表".同一行显示"已完成",备注变为"Mac访问验证通过",页面的文件更新时间也随之变化.本次更新过程中没有重启 Python 服务.

补充验证中,带双引号的字段正常解析,HTML特殊字符经过转义.访问 /report.csv、/server.py 和其他未开放路径均返回 404.临时移走CSV后首页返回500,恢复文件后重新返回 200.
6.打不开或数据不对时,按现象排查
| 现象 | 检查位置 |
|---|---|
| 浏览器连接失败 | 两台客户端是否在线;地址是否为 Ubuntu 组网 IP;启动终端是否还在运行;ss 是否有目标端口监听 |
| 启动时报地址错误 | --bind 是否填写了本机已有地址;网卡名称和 IP 以实际输出为准 |
| 启动时报端口占用 | 先查明现有进程;为演示换一个空闲端口,不要直接结束其他服务 |
| 首页返回 500 | 查看 Ubuntu 异常日志,检查文件存在、编码、CSV 引号和每行列数 |
| 刷新后内容没有变化 | 确认改的是 server.py 同目录的 report.csv,再点击"刷新报表" |
如果服务已监听但连接仍失败,再检查设备防火墙和组网访问策略.需要调整规则时,仅放行访问端到目标端口,不要直接关闭防火墙.
7.验证结束后停止服务,删除演示项目
回到运行Python的Ubuntu终端,按 Ctrl+C.程序输出"报表服务已停止".再检查端口,并只删除这次创建的两个文件和空目录:
bash
ss -ltn '( sport = :18101 )'
cd ~
rm -- ~/starvpn-csv-report-20261001/server.py ~/starvpn-csv-report-20261001/report.csv
rmdir ~/starvpn-csv-report-20261001
test ! -e ~/starvpn-csv-report-20261001 && printf 'Project directory removed\n'
删除前请确认目录里只有本次演示文件.本次清理后,ss 没有端口监听记录,项目目录已不存在;Mac 再次请求同一地址时连接失败,curl 退出码为 7.


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容
每日心灵鸡汤: 真正高级的人生,是懂得给自己留白!
留白不只是一种高级的审美,也是一种很稀缺的能力.很多人总想把话说尽、把空间占满、把关系抓紧、把人生安排得滴水不漏、把交易账户始终塞满仓位,本质上是无法忍受空白与不确定.真正有分寸的人,反而知道什么时候该停,什么不必解释,什么不必拥有.留白的本质,不是空,而是克制;不是缺少,而是知道什么可以不要.
