psycopg2.ProgrammingError: неполный заполнитель: '% (' без ')' - PullRequest
1 голос
/ 03 февраля 2020

У меня есть несколько различных функций, которые очищают разные таблицы с помощью 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, с которого я переписываюсь.

РЕДАКТИРОВАТЬ

Я собираю данные с веб-сайта НФЛ.

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 использует %

1 Ответ

2 голосов
/ 03 февраля 2020

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 '), но обычно вы не должны позволять пользователям или внешним данным определять вашу схему, поэтому имена таблиц и столбцов являются «доверенными». В этом свете единственное правильное решение - это столбцы белого списка , т. Е. Проверить по известному набору, что ваш фрейм данных имеет только допустимые столбцы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...