-1 为什么要强调「在 Mac OS X」下
据江湖传言,在 Linux 下没有这个问题……没想到 Mac 也会入这种坑,大跌眼镜
0 背景
目前 Python 社区中最流行的用于和 Postgres 打交道的模块非 psycopg2 莫属了,连 Postgres 官方 wiki 都在介绍该模块
猜:psycopg2 = Python + SYstem + COnnection + PostGres + 2
- https://wiki.postgresql.org/wiki/Psycopg
- https://wiki.postgresql.org/wiki/Using_psycopg2_with_PostgreSQL
- https://wiki.postgresql.org/wiki/Psycopg2_Tutorial
顺便安利一下好友珂皓专门写的一个中间层 pgdb 用它来和 Postgres 打交道可以省点力气。
为了方便配置,我在连接数据库一般是在配置文件中这么写:
[conn01_name]
database = conn01_name
host = 192.168.0.123
port = 5678
user = user_sssj
password =
然后在需要连接数据库的文件中这么写
# db.py
import pgdb
conn_01_name = pgdb.Connection(
database=config.get('conn_01_name', 'database'),
user=config.get('conn_01_name', 'user'),
password=config.get('conn_01_name', 'password'),
host=config.get('conn_01_name', 'host'),
port=config.get('conn_01_name', 'port')
)
当数据库名即 conn_01_name 为英文名如 zoo 时,这样做没什么问题。问题就在数据库名是中文的时候:
下面是在交互式shell中。这里使用的数据库名为:大象系统
>>> from db import conn_01_name
Traceback (most recent call last):
File "", line 1, in
File "/path/to/db.py", line 52, in
port=config.get('conn_01_name', 'port')
File "/path/to/py35venv/lib/python3.5/site-packages/pgdb/pgdb.py", line 28, in __init__
self._reconnect()
File "/path/to/py35venv/lib/python3.5/site-packages/pgdb/pgdb.py", line 63, in _reconnect
self.connection = psycopg2.connect(*self.db_args, **self.db_kwargs)
File "/path/to/py35venv/lib/python3.5/site-packages/psycopg2/__init__.py", line 129, in connect
dsn = _ext.make_dsn(dsn, **kwargs)
File "/path/to/py35venv/lib/python3.5/site-packages/psycopg2/extensions.py", line 177, in make_dsn
parse_dsn(dsn)
psycopg2.ProgrammingError: invalid dsn: missing "=" after "大象系统" in connection info string
报错啦!
01 尝试
作为一个面向 StackOverFlow 和面向 Google 编程的程序猿,我当然是把错误码(最下方那行)复制起来丢到 Google 上去搜索。可惜无论我如何修改英文关键字,结果寥寥。遂想:
这类坑可能也只有我们中文圈比较有可能趟上吧……?
干脆加点中文关键字,结果依旧少得可怜……嗯其实是几乎就没有结果,不过好歹找到一篇中文博客,大意是说这可能是编码类型造成的?唔这我之前也有想过,但怎么设置呢?是哪里的编码类型?文章中说的是要去设置 connection.extensions.set_client_encoding
,顾名思义是客户端的编码类型,设为 "UTF8" 即可。
我当时就像看到了救命稻草,也没再仔细看,尝试了一下
conn_01_name = pgdb.Connection(
database=config.get('conn_01_name', 'database'),
user=config.get('conn_01_name', 'user'),
password=config.get('conn_01_name', 'password'),
host=config.get('conn_01_name', 'host'),
port=config.get('conn_01_name', 'port')
)
conn_01_name.connection.set_client_encoding('UTF-8')
(注:这里用 Pgdb 实验其实没有太大影响,因为 Connection 用于包装的中间层非常简单,见下可知)
# pgdb.py
import psycopg2
class Connection:
def __init__(self, *args, **kwargs):
self.connection = None
self.db_args = args
self.db_kwargs = kwargs
self._reconnect()
def _reconnect(self):
self._close()
self.connection = psycopg2.connect(*self.db_args, **self.db_kwargs)
self.autocommit = True
呃,还是连不上。我想了一下就知道自己思路不对:
是因为那个方法要执行,必须要建立连接【后】才有用?而我的连接都还没建立就报错了,自然无法到执行
set_client_encoding
的那步就报错了
要不我能不能看看建立连接【时】有没有相应参数来初始化?又扫了一遍那篇中文博客,文中确实提到的是「取数据时」,情况和我不同,也侧面验证了一下我的猜测。查了一下官方文档,是有这个参数的:即在初始化 psycopg2.connect 类时,可使用 client_encoding 来指定编码类型:
client_encoding
This sets the
client_encoding
configuration parameter for this connection. In addition to the values accepted by the corresponding server option, you can useauto
to determine the right encoding from the current locale in the client (LC_CTYPE
environment variable on Unix systems).
(注意到下面这份代码直接改用 psycopg2 来连接了,这样比 pgdb 实验更快一点)
import psycopg2
c = psycopg2.connect(dbname="大象系统", user="ssj", password="",
port="5678", host="192.168.0.123", client_encoding="UTF-8")
结果也是失败的:仍然和上面是同样的报错。我甚至考虑了会不会是参数的位置导致的?当然事实证明也没有用。
02 转机
不死心,继续找答案,一边看文档。我想了一下,报错一直说 dsn:
psycopg2.ProgrammingError: invalid dsn: missing "=" after "大象系统"
in connection info string
上面强行换行,方便看
dsn是啥?可能不是很重要,但是上面这,以及刚才在看官方文档时的线索似乎给了我提示:(先看文档,我当时注意到了这部分)
# psycopg2 初始化连接的 2 种方式
conn = psycopg2.connect("dbname=test user=postgres password=secret")
# 或者
conn = psycopg2.connect(dbname="test", user="postgres", password="secret")
结合报错提示,它是说实际上 psycopg2 的解析过程是把关键字参数转换成字符串什么的一种叫 dsn 的东西,然后用 parse_dsn (见上面的报错提示)解析时报错是吗?于是我尝试重新连接:
import psycopg2
conn = psycopg2.connect("dbname=大象系统 user=ssj password='' port=5678 host=192.168.0.123")
仍是失败的。与此同时,我看到了一个提示:
import psycopg2
conn_string = "host='localhost' dbname='my_database' user='postgres' password='secret'"
conn = psycopg2.connect(conn_string)
啊!引号嵌套,我可能就是少了这个。那么其实我可以这样写
import psycopg2
conn = psycopg2.connect("dbname='%(dbname)s' user='%(user)s' password='%(password)s'\
port='%(port)s' host='%(host)s'"
% {'dbname': '大象系统', 'user': 'ssj', 'password': '', 'port': '5678',
'host': '192.168.0.123'})
这样尝试之后就成功了。对应的 pgdb 写法是
import pgdb
conn = pgdb.Connection("dbname='%(dbname)s' user='%(user)s' password='%(password)s'\
port='%(port)s' host='%(host)s'"
% {'dbname': '大象系统', 'user': 'ssj', 'password': '', 'port': '5678',
'host': '192.168.0.123'})
03 尾声和风波
终于能够成功连接中文名称的数据库了,别提有多高兴。这当然马上要查询一下某些表看看效果啦:
>>> from db import conn
>>> sql = "SELECT animalcode FROM 动物园"
>>> ret = conn.query(sql,)
Traceback (most recent call last):
File "", line 1, in
File "/path/to/py35venv/lib/python3.5/site-packages/pgdb/pgdb.py", line 76, in query
raise e
File "/path/to/py35venv/lib/python3.5/site-packages/pgdb/pgdb.py", line 69, in query
cursor.execute(*args, **kwargs)
psycopg2.ProgrammingError: relation "动物园" does not exist
LINE 1: SELECT animalcode FROM 动物园
啥?这也行?我还以为是中文表名又出错了。尝试了一些英文表名,结果也是 relation xxx does not exist
难道是我被「骗」了吗?我没有实际建立连接但绕开了报错?于是我用了 conn_zoo.ensure_connected 和 cursor 等方法查询了一下,好像是有建立起来呀……我索性用同样的连接法(字符串初始化连接法)去尝试连接其他数据库(非中文名),连上了且成功查询到其他英文表名的记录。
这说明我这种初始化方法应该是没有大问题的。但到底是哪里出错我一下子也想不起来。后来我看了一下配置文件,突然想到这两天配服务器时看各种文档,专门说到一些数据库的端口值会设为非默认值(有时候可能是为了安全,有时候是为了方便隧道访问什么的)——会不会是端口值错了?啊!那么,可能用户名之类的也有问题。
然后我去核对了 DataGrip 中可以正确连接数据库的配置,和我写到程序配置文件中的数据库配置……就知道是我大意了:确实是配置文件写错了。
改完配置文件后,就能正常连接中文名称的数据库,查询 SQL 语句啦!