星空组网+Python实战:在另一台电脑上查看CSV报表


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

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

目录

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.

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容


每日心灵鸡汤: 真正高级的人生,是懂得给自己留白!

留白不只是一种高级的审美,也是一种很稀缺的能力.很多人总想把话说尽、把空间占满、把关系抓紧、把人生安排得滴水不漏、把交易账户始终塞满仓位,本质上是无法忍受空白与不确定.真正有分寸的人,反而知道什么时候该停,什么不必解释,什么不必拥有.留白的本质,不是空,而是克制;不是缺少,而是知道什么可以不要.

相关推荐
liangshanbo12151 小时前
JavaScript 面试题:Object.keys() 的返回顺序是无序的吗?
开发语言·javascript·ecmascript
朝朝辞暮i7 小时前
C++ 第 23 课:class —— 开始真正进入面向对象
开发语言·c++·算法
ai小陈7 小时前
GPU服务器租用存储验收:检查点写入与磁盘吞吐实战
运维·服务器·人工智能·ai·ssh·gpu算力
言乐68 小时前
HTML视频审核模型
python·django·virtualenv·pygame·tornado
小师兄吃牛肉8 小时前
C语言篇:宏
c语言·开发语言
郑同学的笔记8 小时前
【c++随笔28】strcmp 和 memcmp对比
开发语言·c++
言乐69 小时前
Python根据无法识别搜索词找出可能输入内容模型
开发语言·python·django·virtualenv·pygame
weixin_307779139 小时前
有限产能智能排产与动态重排智能体:从需求解构到技术实现
开发语言·人工智能·算法·架构
kakakahahahaha9 小时前
Windows C盘临时文件清理:%temp%、Windows Temp、Prefetch、Windows.old与hiberfil.sys处理指南
c语言·开发语言·windows
朝朝辞暮i9 小时前
C++ 第 27 课:智能指针 shared_ptr
开发语言·c++·算法