1. 为什么 Python 连 PostgreSQL 总在凭证上翻车
先说结论:Python 连 PostgreSQL 本身不难,难的是凭证散落各处。你写一个小脚本,psycopg2.connect(database="mydb", user="postgres", password="xxx", host="192.168.31.181", port="5432")一行就通了。可一旦项目从本地开发走到 CI,从一个人写变成三个人协作,问题就来了——密码硬编码在代码里、.env忘了同步、CI 里环境变量拼错一个字母,报错信息还特别含糊,你盯着connection refused和password authentication failed根本分不清是网络问题还是凭证问题。
我试过最原始的做法:把连接参数写进database.ini,用configparser读。这确实比硬编码强,但每个项目都要维护一份 ini,CI 里还得单独注入,时间一长就变成"这个仓库的 ini 和那个仓库的 ini 到底哪个是对的"。
这篇要解决的问题很具体:用 TaoToken 统一 Key 通道来管理 PostgreSQL 的访问凭证,让本地开发和 CI 用同一套配置骨架,一次配好就能跑通连接测试。核心交付三样东西——可复制的settings.json配置骨架、连接验证脚本、报错排查清单。
适合谁看:正在用 Python + psycopg2 连 PostgreSQL、被凭证管理折磨过的后端开发;需要给 CI 配数据库连接但不想把密码写进 YAML 的运维;以及刚接触 psycopg2、想搞清楚 cursor 到底怎么回事的新手。
TaoToken 在这里扮演的角色是"统一凭证入口"——你把数据库访问相关的 Key 和通道配置集中到一处管理,Python 侧只认一个配置来源,不用再在代码、ini、环境变量之间来回对。它不是什么魔法,就是把"凭证从哪来"这件事标准化了。
下面从环境准备开始,一步步把配置骨架、验证脚本、排错清单都落地。
2. TaoToken 前置准备与 psycopg2 环境搭建
动手之前,先把两件事理清楚:TaoToken 侧要拿到什么,Python 侧要装什么。
2.1 TaoToken 侧:拿到统一 Key 和 API 通道
TaoToken 的核心价值是把访问凭证收敛到一个入口。你需要先登录控制台,在 API Keys 页面创建一个 Key。这个 Key 就是你后续在settings.json里引用的凭证来源。
具体操作路径:打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,进入控制台,找到 API Keys 管理页。创建 Key 的时候建议按用途命名,比如pg-dev-local、pg-ci-runner,这样后面排查问题时一眼能看出这个 Key 是给谁用的。
创建完成后,你会拿到两样东西:一个是 Key 本身(一串字符),一个是 API 基础地址https://taotoken.net/api。注意这个 API 地址后面不加任何 UTM 参数,就是干净的https://taotoken.net/api。
注意:Key 只在创建时完整显示一次,创建后请立即复制保存到安全位置。如果丢了,只能重新生成。
如果你还想在配置过程中验证模型通道是否正常,可以顺手打开模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 发一条测试消息,确认 Key 有效。这一步不是必须的,但能帮你提前排除"Key 本身就没生效"这种低级问题。
2.2 Python 侧:安装 psycopg2
psycopg2 是目前 Python 生态里最成熟的 PostgreSQL 适配器。安装有两种方式:
# 方式一:纯 Python 实现,安装简单,无需编译 pip3 install psycopg2-binary # 方式二:源码编译版,性能略好,但需要本机有 pg_config 和编译工具链 pip3 install psycopg2新手直接用psycopg2-binary,省去编译依赖的麻烦。生产环境如果对性能敏感,再考虑源码版。
装完之后验证一下:
python3 -c "import psycopg2; print(psycopg2.__version__)"能打印出版本号就说明装好了。如果报ModuleNotFoundError,检查一下你用的pip3和python3是不是同一个环境——这是新手最常踩的坑,尤其在同时装了系统 Python 和 conda 的机器上。
2.3 理解 cursor:它不是 PL/pgSQL 的游标
在写连接脚本之前,必须把 cursor 这个概念说清楚,因为很多人第一次看到conn.cursor()会跟数据库里的游标混淆。
psycopg2 里的 cursor 是一个客户端对象,它负责在 Python 进程里持有查询结果、逐行取数据。而 PL/pgSQL 里的 cursor 是服务端游标,是数据库内部用来遍历结果集的机制。两者名字一样,但完全不是一回事。
你可以把 cursor 理解成"结果集的遥控器":cursor.execute(sql)是按下播放键,cursor.fetchone()取一条,cursor.fetchmany(3)取三条,cursor.fetchall()全取回来。取完之后cursor.close()关掉遥控器,conn.close()关掉整个连接。
理解了这层,后面写验证脚本就不会迷糊。
3. 可复制的 settings.json 配置骨架与 psycopg2 接入
这一节是全文的核心。目标是把数据库连接参数从代码里彻底剥离,统一走settings.json,而settings.json里的敏感凭证又通过 TaoToken 统一 Key 通道来管理。
3.1 settings.json 配置骨架
在项目根目录建一个config/settings.json,内容如下:
{ "taotoken": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "channel": "unified" }, "postgresql": { "host": "192.168.31.181", "port": 5432, "database": "mydb", "user": "postgres", "password_env": "PG_PASSWORD", "connect_timeout": 10, "sslmode": "prefer" }, "app": { "env": "development", "log_level": "INFO" } }这个骨架的设计思路:配置文件里不出现明文密码。password_env指向一个环境变量名,真正的密码从环境变量读。同理,TaoToken 的 Key 也通过api_key_env指向环境变量TAOTOKEN_API_KEY。
这样做的好处是:settings.json可以安全地提交到 Git 仓库,团队成员拉下来就能用,只需要各自在本地设置环境变量。CI 环境里则通过 CI 平台的 Secret 机制注入这两个环境变量。
注意:
host、port、database、user这些非敏感信息可以直接写在 JSON 里,方便团队统一。只有密码和 Key 走环境变量。
3.2 配置加载模块
写一个config_loader.py,负责读 JSON 并把环境变量解析进来:
import json import os def load_settings(path="config/settings.json"): with open(path, "r", encoding="utf-8") as f: settings = json.load(f) pg = settings["postgresql"] # 从环境变量解析密码 pg_password = os.environ.get(pg["password_env"]) if not pg_password: raise EnvironmentError( f"环境变量 {pg['password_env']} 未设置,无法获取数据库密码" ) pg["password"] = pg_password # 解析 TaoToken Key tt = settings["taotoken"] tt_key = os.environ.get(tt["api_key_env"]) if not tt_key: raise EnvironmentError( f"环境变量 {tt['api_key_env']} 未设置,无法获取 TaoToken Key" ) tt["api_key"] = tt_key return settings这个模块做了两件关键事:一是把 JSON 里的password_env字段替换成真实密码,二是提前校验环境变量是否存在,避免后面连接时报一堆看不懂的错。
3.3 psycopg2 连接封装
接下来写db.py,把连接逻辑封装起来:
import psycopg2 from config_loader import load_settings def get_connection(): settings = load_settings() pg = settings["postgresql"] conn = psycopg2.connect( database=pg["database"], user=pg["user"], password=pg["password"], host=pg["host"], port=pg["port"], connect_timeout=pg["connect_timeout"], sslmode=pg["sslmode"], ) return conn def query_version(): conn = get_connection() try: cursor = conn.cursor() cursor.execute("SELECT VERSION()") data = cursor.fetchone() cursor.close() return data[0] finally: conn.close()注意这里用了try/finally,保证即使查询出错,连接也会被关闭。这是比原始示例更稳妥的写法。
3.4 环境变量设置
本地开发时,在 shell 里设置:
export TAOTOKEN_API_KEY="你的TaoToken Key" export PG_PASSWORD="你的数据库密码"CI 环境里,在流水线配置的 Secret 部分设置同名变量。这样本地和 CI 用的是同一套代码、同一份settings.json,只有环境变量注入方式不同。
如果你用的是 Claude Code 或 Cline 这类工具做辅助开发,可以在项目里放一份.env.example作为模板,但真正的.env要加进.gitignore。
4. 验证请求与成功结果:跑通连接测试
配置写完了,得验证它真的能跑通。这一节给你一个完整的验证脚本,以及每一步的预期输出。
4.1 完整验证脚本
新建verify_connection.py:
import sys from db import get_connection def main(): print("[1/4] 加载配置...") try: conn = get_connection() print(" 配置加载成功,连接已建立") except EnvironmentError as e: print(f" 环境变量缺失: {e}") sys.exit(1) except Exception as e: print(f" 连接失败: {e}") sys.exit(1) print("[2/4] 查询数据库版本...") cursor = conn.cursor() cursor.execute("SELECT VERSION()") version = cursor.fetchone()[0] print(f" {version}") print("[3/4] 查询当前数据库和用户...") cursor.execute("SELECT current_database(), current_user") db_name, user = cursor.fetchone() print(f" database={db_name}, user={user}") print("[4/4] 关闭连接...") cursor.close() conn.close() print(" 连接已关闭,测试通过") if __name__ == "__main__": main()4.2 预期输出
设置好环境变量后运行:
python3 verify_connection.py成功的话你会看到类似这样的输出:
[1/4] 加载配置... 配置加载成功,连接已建立 [2/4] 查询数据库版本... PostgreSQL 15.4 on x86_64-pc-linux-gnu, compiled by gcc ... [3/4] 查询当前数据库和用户... database=mydb, user=postgres [4/4] 关闭连接... 连接已关闭,测试通过四步全过,说明从配置加载到连接建立、查询执行、资源释放整条链路都通了。
4.3 用 fetchmany 取多行数据
连接通了之后,顺手验证一下 cursor 的取数能力。假设你有个cities表:
import psycopg2 from db import get_connection conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT * FROM cities;") # 取前 3 条 rows = cursor.fetchmany(3) for row in rows: print(row) cursor.close() conn.close()fetchmany(3)每次取 3 条,适合结果集很大、不想一次性全加载到内存的场景。fetchall()则一次性全取回来,小结果集用着方便,大表要慎用。
4.4 在 CI 里跑验证
CI 流水线里,把验证脚本作为一步:
- name: Verify PostgreSQL connection env: TAOTOKEN_API_KEY: ${{ secrets.TAOTOKEN_API_KEY }} PG_PASSWORD: ${{ secrets.PG_PASSWORD }} run: python3 verify_connection.py这样每次提交代码,CI 都会自动验证数据库连接是否正常。如果哪天有人改了settings.json导致配置错位,CI 会第一时间报出来,而不是等到部署后才炸。
5. 本篇常见报错排查清单
连接跑不通时,报错信息往往很含糊。这一节把最常见的几类错误和对应排查方法列出来,对照着查。
5.1 环境变量缺失类
报错:EnvironmentError: 环境变量 PG_PASSWORD 未设置,无法获取数据库密码
这是配置加载模块主动抛出的,说明环境变量没设。排查步骤:
# 确认变量是否存在 echo $PG_PASSWORD echo $TAOTOKEN_API_KEY如果输出为空,说明当前 shell 没设。注意一个常见陷阱:你在一个终端里export了,但运行脚本的是另一个终端或 IDE 的内置终端,环境变量不共享。IDE 里需要在运行配置里单独设置环境变量。
5.2 认证失败类
报错:psycopg2.OperationalError: FATAL: password authentication failed for user "postgres"
密码错了。排查方向:
第一,确认PG_PASSWORD的值和数据库实际密码一致。有时候密码里有特殊字符,在 shell 里export时被转义了,建议用单引号包裹。
第二,确认settings.json里的user字段和密码匹配。PostgreSQL 是按用户+密码校验的,用户写错了密码再对也没用。
第三,检查pg_hba.conf里的认证方式。如果是md5或scram-sha-256,密码必须正确;如果是trust,则根本不校验密码,这时候报认证失败反而说明配置有问题。
5.3 连接被拒类
报错:psycopg2.OperationalError: could not connect to server: Connection refused
这个错误和密码无关,是网络层的问题。排查顺序:
先确认 PostgreSQL 服务在跑:
# Linux systemctl status postgresql # macOS brew services list | grep postgresql再确认postgresql.conf里的listen_addresses是否允许外部连接。默认可能只监听localhost,如果你的host写的是192.168.31.181,而服务只监听127.0.0.1,就会连接被拒。
最后确认防火墙有没有放行 5432 端口。
5.4 超时类
报错:psycopg2.OperationalError: connection to server at "192.168.31.181", port 5432 failed: timeout expired
我们在settings.json里设了connect_timeout: 10,10 秒连不上就报这个。超时通常意味着网络不通,或者目标主机不可达。先用ping和telnet确认基础连通性:
ping 192.168.31.181 telnet 192.168.31.181 5432telnet能连上说明端口通,问题在 PostgreSQL 配置;连不上说明网络或防火墙有问题。
5.5 cursor 相关错误
报错:psycopg2.ProgrammingError: no results to fetch
这个错误通常发生在execute之后没有结果却调用了fetchone。比如执行的是INSERT、UPDATE、DELETE这类不返回结果集的语句,就不该调fetch。只有SELECT才有结果可取。
另一个常见的是psycopg2.InterfaceError: cursor already closed,说明你在cursor.close()之后又调用了它。检查代码里 cursor 的生命周期,确保关闭后不再使用。
5.6 排查清单速查表
| 报错关键词 | 大概率原因 | 第一步排查 |
|---|---|---|
| 环境变量未设置 | 变量没 export 或 IDE 未配置 | echo $变量名 |
| password authentication failed | 密码错或用户不匹配 | 核对 PG_PASSWORD 和 user |
| Connection refused | 服务没跑或监听地址不对 | systemctl status postgresql |
| timeout expired | 网络不通或防火墙拦截 | telnet host port |
| no results to fetch | 对非 SELECT 语句调 fetch | 检查 SQL 类型 |
| cursor already closed | cursor 关闭后仍被使用 | 检查生命周期 |
把这张表存下来,下次报错先对号入座,能省不少时间。
6. 把统一 Key 通道用起来:从本地到 CI 的完整落地
配置骨架和验证脚本都跑通了,最后说说怎么把这套东西真正用起来,以及 TaoToken 统一 Key 通道在其中的价值。
6.1 本地开发的标准流程
新成员加入项目时,标准流程应该是这样:
第一步,克隆仓库,settings.json已经在里面了,不需要改。
第二步,找项目负责人要两个值:TaoToken Key 和数据库密码。或者更规范的做法是,让每个人自己在 TaoToken 控制台创建自己的 Key,数据库密码则通过内部密码管理工具分发。
第三步,在本地 shell 或 IDE 里设置环境变量:
export TAOTOKEN_API_KEY="个人Key" export PG_PASSWORD="数据库密码"第四步,运行python3 verify_connection.py,四步全过就说明环境配好了。
整个过程不需要改任何代码,不需要动settings.json,凭证和配置彻底分离。
6.2 CI 环境的配置
CI 里把两个 Secret 配好,流水线里注入环境变量,剩下的和本地完全一样。这就是统一配置骨架的价值——本地和 CI 用同一份settings.json,只有环境变量注入方式不同,不会出现"本地能跑 CI 跑不了"的经典问题。
如果你在 CI 里用 Claude Code 或类似工具做自动化代码检查,可以在流水线里额外配一个 Coding Plan 相关的 Key,走 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 了解具体方案。这样数据库凭证和编码辅助工具的凭证各管各的,互不干扰。
6.3 凭证轮换
安全实践要求定期轮换凭证。用这套架构,轮换变得很简单:
数据库密码要换,只需在数据库侧改密码,然后更新 CI 的 Secret 和通知团队成员更新本地环境变量。settings.json一个字都不用动。
TaoToken Key 要换,同理,在控制台重新生成,更新环境变量即可。代码和配置文件完全无感。
这就是"凭证从配置里剥离"带来的直接好处——轮换成本从"改代码、改配置、重新部署"降到"改一个环境变量"。
6.4 关于 settings.json 的版本管理
settings.json应该提交到 Git,因为里面没有敏感信息。但要注意两点:
一是host、port、database这些字段如果不同环境(开发/测试/生产)不一样,可以拆成settings.dev.json、settings.prod.json,通过环境变量APP_ENV决定加载哪个。加载模块里加一行判断即可。
二是如果团队里有人不小心把密码写进了settings.json并提交了,要立即轮换密码,因为 Git 历史里已经留下了记录。这也是为什么我们坚持用password_env而不是直接写password。
6.5 下一步可以做什么
连接跑通只是起点。接下来你可以:
把db.py扩展成连接池,用psycopg2.pool.SimpleConnectionPool管理连接复用,避免每次查询都新建连接。
把验证脚本接入监控,定期跑一次,连接异常时告警。
把settings.json的加载逻辑抽成独立的小库,多个项目共用。
如果你需要更细的接入文档,可以看 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。API Keys 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。控制台入口是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。
最后留一个我踩过的坑:sslmode设成prefer时,如果服务端不支持 SSL,psycopg2 会静默降级到非加密连接,不会报错。如果你明确要求加密,要设成require,这样服务端不支持时会直接报错,而不是悄悄降级。生产环境建议用require或更严格的verify-full。