Appearance
Python 是什么与环境
Shell 适合把现成命令串起来,Python 更适合把数据、分支、异常、报告组织成清楚的脚本。脚本一旦开始出现 JSON 解析、接口调用、失败重试、批量处理这种逻辑,继续用 Shell 拼下去就会越来越难改——Python 的位置大概就在这。命令照样可以交给系统工具执行,判断、整理、记录、输出这些交给 Python。
写 Python 脚本其实就涉及三个对象:.py 文件、Python 解释器、依赖环境。很多"本地能跑、定时任务里失败"的问题,根子都出在这三者没对齐。
.py 文件只是文本,操作系统不能直接理解里面的 Python 语法——负责执行代码的是解释器。解释器再去加载标准库和第三方包,最后调用文件、网络、进程这些系统能力。把这个链路搞清楚,后面"为什么找不到模块""为什么版本不对"这些问题就有排查方向了。
一、环境检查
写脚本前先看机器上有哪些 Python 命令,以及它们分别指向哪里。
Linux:
bash
python --version
python3 --version
which python
which python3Windows PowerShell:
powershell
py -0p
py -3 --version
py -3 -m pip --versionpython 这个命令在不同机器上含义不稳定。老系统里它可能指向 Python 2,也可能根本不存在。所以服务器脚本里更稳的写法是用 python3、uv run python,或者直接写虚拟环境里的解释器路径——别假设 python 一定是 Python 3。
常见问题按三层看:
| 层面 | 现象 | 检查入口 |
|---|---|---|
| 解释器 | python 找不到、版本太旧、f-string 报语法错误 | python3 --version、which python3 |
| 依赖包 | ModuleNotFoundError: No module named ... | uv pip list、虚拟环境路径 |
| 系统能力 | 文件没权限、命令不存在、端口不通 | 文件权限、PATH、防火墙、系统日志 |
二、解释器安装
Python 3 是现在的默认选择。脚本里只要用了 f-string、类型注解、新版标准库功能,解释器版本太低就会直接报语法错误,所以版本要先确认对。
Rocky / RHEL / CentOS:
bash
# 安装解释器和传统 pip 工具
dnf install -y python3 python3-pip
python3 --version
python3 -m pip --versionUbuntu / Debian:
bash
apt-get update
# python3-venv 用于创建虚拟环境
apt-get install -y python3 python3-venv python3-pip
python3 --version
python3 -m pip --versionWindows 通常用 python.org 安装包或 winget。安装包里的"Add Python to PATH"影响 PowerShell 能不能直接找到 python 命令——这个勾选项很多人漏勾,装完发现命令找不到。已经装了但命令找不到时,先用 py -0p 看 Python 启动器能不能找到解释器。
三、依赖隔离
Python 自带一部分库,叫标准库。文件路径、JSON、日志、子进程这些能力标准库里就有:pathlib(路径拼接、判断文件)、json(读写 JSON)、logging(带时间和级别的日志)、subprocess(调用系统命令)、shutil(复制文件、查磁盘)、configparser(读 ini 配置)。
还有一些能力不在标准库里——HTTP 请求、SSH、MySQL、Redis、YAML 这些,要装第三方包:requests(调 HTTP API)、paramiko(SSH)、pymysql(连 MySQL)、redis(连 Redis)、pyyaml(处理 YAML)。
这里有个特别容易踩的坑:所有包都装进系统 Python,不同脚本的依赖版本会互相打架。脚本 A 需要 requests 2.28,脚本 B 升级到 requests 3.0——pip install 会直接覆盖,脚本 A 可能因为 API 变化跑不起来。系统自带的 Python 版本还可能有约束(比如 CentOS 7 的 yum 依赖系统 Python 2),往里面装包风险更大。
虚拟环境就是解决这个问题的:给每个项目单独准备一套包目录,脚本用哪个解释器启动,就从哪个环境里找包。
虚拟环境不是另一个操作系统,只是一套独立的 Python 包目录。这个概念理解了,后面环境相关的坑就少一半。
四、项目依赖管理
传统做法是 python3 -m venv、pip install、requirements.txt 加手动 activate。这套流程的常见问题是:依赖装到哪个环境不清楚、换机器后版本不一致、cron 或 systemd 里没加载虚拟环境。
uv 把这些事整合到一个入口里:uv add 写依赖并更新锁文件、uv run 用项目环境执行脚本、uv sync 在新机器上恢复环境。机器上先装 uv:
bash
# 安装脚本会提示 uv 被放到哪个用户目录
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --versionWindows PowerShell:
powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
uv --version装完提示 uv: command not found,通常是终端还没刷新 PATH。重新打开终端,或者按安装输出把 uv 所在目录加进 PATH。
一个最小脚本项目长这样:
text
todo-tools/
├── pyproject.toml
├── uv.lock
└── scripts/
└── count.pypyproject.toml 记录项目和依赖,uv.lock 锁定具体版本,scripts/ 放脚本文件。换机器运行时,锁文件能减少"同一个包不同版本导致行为不一样"的问题。
创建项目并装依赖:
bash
uv init todo-tools
cd todo-tools
# requests 会写入 pyproject.toml,并安装到项目虚拟环境
uv add requests
# 用项目环境执行 Python
uv run python --version
uv pip list传统虚拟环境也能干同样的事:
bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install requests区别在于 uv 项目里通常不用手动 activate,直接 uv run 就进项目环境。cron 或 systemd 里执行脚本时这点特别有用——非交互环境不会自动加载交互式终端里的虚拟环境,用 uv run 能避免"手动跑没问题、定时任务里找不到包"的经典坑。
五、包索引配置
访问 PyPI 慢或超时时,给 pip 或 uv 配个国内镜像。
pip 临时用镜像:
bash
python3 -m pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simplepip 用户级配置:
bash
python3 -m pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
python3 -m pip config set global.timeout 60
python3 -m pip config listuv 临时用镜像:
bash
uv add requests --default-index https://pypi.tuna.tsinghua.edu.cn/simpleuv 当前 shell 临时生效:
bash
export UV_DEFAULT_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple
uv add requestsWindows PowerShell:
powershell
$env:UV_DEFAULT_INDEX = "https://pypi.tuna.tsinghua.edu.cn/simple"
uv add requests常用镜像源:清华 https://pypi.tuna.tsinghua.edu.cn/simple、阿里云 https://mirrors.aliyun.com/pypi/simple/、腾讯云 https://mirrors.cloud.tencent.com/pypi/simple、华为云 https://repo.huaweicloud.com/repository/pypi/simple。
https 镜像源通常不需要额外设 trusted-host。公司内部 http 源或证书异常的源,才会涉及证书和信任配置。
六、入口结构
一个 .py 文件通常把主要流程放进 main()。先看一个最简单的:
新增文件 scripts/hello.py:
python
#!/usr/bin/env python3
"""最小 Python 脚本入口。"""
def main():
# 主要逻辑放在 main 里,之后更容易拆函数和测试
print("hello python")
return 0
if __name__ == "__main__":
raise SystemExit(main())几个部分解释一下:
#!/usr/bin/env python3是 shebang。Linux 上给脚本加执行权限后,系统按这一行找解释器def main()把主流程收进函数,后面脚本变长了好拆分if __name__ == "__main__":区分直接执行和被别的文件 import——直接执行时进入main(),被 import 时只加载函数不自动跑主流程raise SystemExit(main())用 main 的返回值当退出码
bash
chmod +x scripts/hello.py
./scripts/hello.py
# 或者直接用解释器跑
uv run python scripts/hello.pyif __name__ == "__main__": 这个写法一开始看着别扭,但要养成习惯。脚本拆成多个文件后,它能避免"import 一个模块结果主流程被意外执行"的麻烦。
七、第一个脚本
用一个中性例子把前面串起来:读一份待办清单文本文件,统计有多少条任务。这个脚本只用标准库,适合当起点。
先准备输入文件 todo.txt:
text
买菜
写周报
回复邮件
跑步中间有个空行,统计时应该跳过。
新增文件 scripts/count.py:
python
#!/usr/bin/env python3
"""统计待办清单里有多少条任务(跳过空行)。"""
from pathlib import Path
import sys
def count_todos(path):
text = path.read_text(encoding="utf-8")
count = 0
for line in text.splitlines():
if line.strip(): # 跳过空行
count += 1
return count
def main():
path = Path("todo.txt")
if not path.exists():
print(f"文件不存在: {path}", file=sys.stderr)
return 1
total = count_todos(path)
print(f"待办清单共 {total} 条")
return 0
if __name__ == "__main__":
raise SystemExit(main())运行:
bash
uv run python scripts/count.py
echo $?退出码 0 表示正常,非零表示异常。cron、systemd、CI、监控探针都靠退出码判断脚本结果——脚本只打印"文件不存在",但退出码还是 0,外层系统就会把失败当成成功。这个"退出码要和结果一致"的习惯,从第一个脚本就该养成。