【问题标题】:psycopg2.ProgrammingError: incomplete placeholder: '%(' without ')'psycopg2.ProgrammingError: 不完整的占位符: '%(' without ')'
【发布时间】:2020-05-18 16:45:15
【问题描述】:

我有几个不同的函数,它们用 pandas 抓取不同的表,将每个表保存到数据框,然后将它们保存到 PostgreSQL 数据库。我能够成功地抓取每个表并将其保存为数据框,但是在将其保存到 SQL 时遇到了一些问题。我正在尝试使用以下内容来做到这一点:

from sqlalchemy import create_engine

# Opening sql connection
engine = create_engine('postgresql://postgres:pw@localhost/name')
con = engine.connect()

def df1():
    df = scraped_data
    df.to_sql(table_name, con, if_exists='replace')
df1()

def df2():
    df = scraped_data
    df.to_sql(table_name, con, if_exists='replace')
df2()

# Closing connection
con.close()

我能够成功地将df1 保存到SQL,但是在运行df2 时出现错误。 这两个函数之间唯一真正的区别是它们从不同来源抓取数据。其他一切基本相同。

我还有其他几个函数用于其他数据帧,但无论我调用函数的顺序如何,只有第一个函数有效。

我调用的所有其他函数都出现同样的错误:

psycopg2.ProgrammingError: incomplete placeholder: '%(' without ')'

他们还链接了一个关于错误的背景页面:http://sqlalche.me/e/f405),尽管我仍然不知道该怎么做。

当唯一改变的是我从中抓取的 url 时,我只是觉得它对一个函数而不是其他函数的工作方式很奇怪。

编辑

我正在从 NFL 的网站上抓取数据。

df1 遍历来自http://www.nfl.com/stats/categorystats?archive=false&conference=null&role=TM&offensiveStatisticCategory=GAME_STATS&defensiveStatisticCategory=null&season=2019&seasonType=REG&tabSeq=2&qualified=false&Submit=Go 的表中的年份。

df2 做了一个非常相似的事情,但从http://www.nfl.com/stats/categorystats?archive=false&conference=null&role=TM&offensiveStatisticCategory=TEAM_PASSING&defensiveStatisticCategory=null&season=2019&seasonType=REG&tabSeq=2&qualified=false&Submit=Go 中提取数据。

看起来主要区别在于df1 使用Pct 表示列标题中的百分比,而df2 使用%

【问题讨论】:

  • 我认为这与 con.close 选项有关。尝试以两种函数在到达 con.close() 行之前执行的方式运行它。或者更好的是,在没有 con.close 的情况下尝试它,看看它是否写入成功。如果是这样,那么你可以对连接子句进行调整
  • to_sql 的第二个参数必须是 engine,而不是 con
  • 我尝试了上述两个建议,但仍然遇到我在帖子中描述的错误。
  • 如果唯一的区别是数据,那么请在问题中包含所述数据的最小样本,或者换句话说,生成minimal reproducible example。它通常还有助于包含完整的回溯,而不仅仅是错误消息。在这种情况下,它也会包含基本信息。

标签: python pandas postgresql sqlalchemy pandas-to-sql


【解决方案1】:

TL;DR:您有一个潜在的 SQL 注入漏洞。

问题是您的列名之一包含%。这是一个最小的可重现示例:

In [8]: df = pd.DataFrame({"%A": ['x', 'y', 'z']})

In [9]: df.to_sql('foo', engine, if_exists='replace')

产生以下日志和回溯:

...
INFO:sqlalchemy.engine.base.Engine:
DROP TABLE foo
INFO:sqlalchemy.engine.base.Engine:{}
INFO:sqlalchemy.engine.base.Engine:COMMIT
INFO:sqlalchemy.engine.base.Engine:
CREATE TABLE foo (
        index BIGINT, 
        "%%A" TEXT
)


INFO:sqlalchemy.engine.base.Engine:{}
INFO:sqlalchemy.engine.base.Engine:COMMIT
INFO:sqlalchemy.engine.base.Engine:BEGIN (implicit)
INFO:sqlalchemy.engine.base.Engine:INSERT INTO foo (index, "%%A") VALUES (%(index)s, %(%A)s)
INFO:sqlalchemy.engine.base.Engine:({'index': 0, '%A': 'x'}, {'index': 1, '%A': 'y'}, {'index': 2, '%A': 'z'})
INFO:sqlalchemy.engine.base.Engine:ROLLBACK
---------------------------------------------------------------------------
ProgrammingError                          Traceback (most recent call last)
~/Work/sqlalchemy/lib/sqlalchemy/engine/base.py in _execute_context(self, dialect, constructor, statement, parameters, *args)
   1239                     self.dialect.do_executemany(
-> 1240                         cursor, statement, parameters, context
   1241                     )

~/Work/sqlalchemy/lib/sqlalchemy/dialects/postgresql/psycopg2.py in do_executemany(self, cursor, statement, parameters, context)
    854         if self.executemany_mode is EXECUTEMANY_DEFAULT:
--> 855             cursor.executemany(statement, parameters)
    856             return

ProgrammingError: incomplete placeholder: '%(' without ')'

The above exception was the direct cause of the following exception:

ProgrammingError                          Traceback (most recent call last)
<ipython-input-9-88cf8a93ad8c> in <module>()
----> 1 df.to_sql('foo', engine, if_exists='replace')

...

ProgrammingError: (psycopg2.ProgrammingError) incomplete placeholder: '%(' without ')'
[SQL: INSERT INTO foo (index, "%%A") VALUES (%(index)s, %(%A)s)]
[parameters: ({'index': 0, '%A': 'x'}, {'index': 1, '%A': 'y'}, {'index': 2, '%A': 'z'})]
(Background on this error at: http://sqlalche.me/e/f405)

可以看出,SQLAlchemy/Pandas 使用列名作为占位符键:%(%A)s这意味着您可能对 SQL 注入持开放态度,尤其是当您正在处理抓取的数据时:

In [3]: df = pd.DataFrame({"A": [1, 2, 3], """A)s);
   ...: DO $$
   ...: BEGIN
   ...: RAISE 'HELLO, BOBBY!';
   ...: END;$$ --""": ['x', 'y', 'z']})

In [4]: df.to_sql('foo', engine, if_exists='replace')

结果:

...
INFO sqlalchemy.engine.base.Engine INSERT INTO foo (index, "A", "A)s);
DO $$
BEGIN
RAISE 'HELLO, BOBBY!';
END;$$ --") VALUES (%(index)s, %(A)s, %(A)s);
DO $$
BEGIN
RAISE 'HELLO, BOBBY!';
END;$$ --)s)
INFO sqlalchemy.engine.base.Engine ({'index': 0, 'A': 1, "A)s);\nDO $$\nBEGIN\nRAISE 'HELLO, BOBBY!';\nEND;$$ --": 'x'}, {'index': 1, 'A': 2, "A)s);\nDO $$\nBEGIN\nRAISE 'HELLO, BOBBY!';\nEND;$$ --": 'y'}, {'index': 2, 'A': 3, "A)s);\nDO $$\nBEGIN\nRAISE 'HELLO, BOBBY!';\nEND;$$ --": 'z'})
INFO sqlalchemy.engine.base.Engine ROLLBACK
---------------------------------------------------------------------------
RaiseException                            Traceback (most recent call last)
...

InternalError: (psycopg2.errors.RaiseException) HELLO, BOBBY!
CONTEXT:  PL/pgSQL function inline_code_block line 3 at RAISE

[SQL: INSERT INTO foo (index, "A", "A)s);
DO $$
BEGIN
RAISE 'HELLO, BOBBY!';
END;$$ --") VALUES (%(index)s, %(A)s, %(A)s);
DO $$
BEGIN
RAISE 'HELLO, BOBBY!';
END;$$ --)s)]
[parameters: ({'index': 0, 'A': 1, "A)s);\nDO $$\nBEGIN\nRAISE 'HELLO, BOBBY!';\nEND;$$ --": 'x'}, {'index': 1, 'A': 2, "A)s);\nDO $$\nBEGIN\nRAISE 'HELLO, BOBBY!';\nEND;$$ --": 'y'}, {'index': 2, 'A': 3, "A)s);\nDO $$\nBEGIN\nRAISE 'HELLO, BOBBY!';\nEND;$$ --": 'z'})]
(Background on this error at: http://sqlalche.me/e/2j85)

如果您使用具有足够权限的数据库用户,这允许在您的机器上执行例如任意命令:

In [11]: df = pd.DataFrame({"A": [1, 2, 3], """A)s);
    ...: CREATE TEMPORARY TABLE IF NOT EXISTS evil (state text);
    ...: DO $$
    ...: BEGIN
    ...: IF NOT EXISTS (SELECT * FROM evil) THEN
    ...: COPY evil (state) FROM PROGRAM 'send_ssh_keys | echo done';
    ...: END IF;
    ...: END;$$ --""": ['x', 'y', 'z']})

这似乎是对 SQLAlchemy(和/或 Pandas)部分的疏忽,但通常您并不打算允许用户或外部数据定义您的架构,因此表和列名是“可信的”。 鉴于此,唯一合适的解决方案是将列列入白名单,即对照已知集检查您的数据框是否只有允许的列。

【讨论】:

  • 感谢您的详细解释。我在原始帖子中进行了编辑,试图解释df1df2 之间的区别。似乎解决此问题的方法是将df2 中的% 符号替换为Pct
  • 这肯定是一种解决方法,您可能还想删除或替换任何()。我不是 100% 确定这是否会关闭这个洞,但至少它会让它变得更难。白名单和查找替换是安全的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-09
  • 2018-05-23
  • 1970-01-01
  • 1970-01-01
  • 2013-10-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多