【问题标题】:How to automate parameters passed into pandas.read_sql?如何自动化传递给 pandas.read_sql 的参数?
【发布时间】:2021-08-15 12:00:50
【问题描述】:

我正在尝试创建一种通过 locals() 之类的方式自动传递参数的方法,类似于 f 字符串的工作方式。

目前的工作方式

import pandas as pd

def my_func(conn, string_id, date, integer_ids):
    sql = f"""    
    select * from RISK a
    where STRING_ID = '{string_id}'
    and DATE = {date}
    and INTEGER_ID in ({','.join(map(str, integer_ids))})"""
    df = pd.read_sql(sql, conn)
    return df

但是,这种方法意味着我无法将 SQL 复制粘贴到 SQL developer 或类似软件中,然后从那里运行它。所以我想要一种使用参数的方法。

这似乎有两个问题

  1. 参数必须是字面量,所以不能传递列表
  2. 我需要手动创建一个字典,不能简单地传递locals()之类的东西

我希望它的工作方式类似于下面的示例(显然行不通)

import pandas as pd

def my_func(conn, string_id, date, integer_ids):
    sql = """    
    select * from RISK
    where STRING_ID = :string_id
    and DATE = :date
    and INTEGER_ID in :integer_ids"""
    df = pd.read_sql(sql, conn, params=locals())
    return df

编辑:经过一番测试,也许我可以使用正则表达式来查找:param 的所有实例并将它们替换为参数值,例如

import re
pattern = '[\n\r].*:\s*([^\n\r]*)'
matches = re.findall(pattern,sql)
for match in matches:
    sql = sql.replace(':'+match, eval(match))

它不是很漂亮,它引入了与对象类型相关的问题。例如。 string_id要封装成',日期也要转成字符串对象

最终编辑:感谢 perl,我现在有一个可行的解决方案来解决我的问题

def read_sql(sql, conn, params):
    # Finds all words following a ":" sign in the sql
    for p in re.findall(':(\w+)', sql):
        if isinstance(params.get(p), (tuple, list)):
            ext_params = {f'{p}_{i:03d}': p_i for i, p_i in enumerate(params.get(p))}
            sql = sql.replace(f':{p}', f"(:{', :'.join(ext_params)})")
            params.update(ext_params)

    sql_text = sqlalchemy.text(sql)
    return pd.read_sql(sql_text, conn, params=params)


def my_func(conn, string_id, date, integer_ids):
    sql = """    
    select * from RISK
    where STRING_ID = :string_id
    and DATE = :date
    and INTEGER_ID in :integer_ids"""
    df = read_sql(sql, conn, locals())
    return df

【问题讨论】:

  • 会返回 df, sql workl?至少你会把带有参数的 sql 变成一个可以 print() 和剪切/粘贴的字符串?
  • @JonathanLeon 这需要我运行代码来获取可以复制粘贴的字符串,我需要一些无需运行代码即可工作的东西

标签: python sql pandas


【解决方案1】:

您可以通过将查询包装在sqlalchemy.text 中并将列表转换为元组来使用参数化查询。例如:

def my_func(conn, min_number, letters):
    # convert lists to tuples
    letters = tuple(letters)
    
    # wrap sql in sqlalchemy.text
    sql = sqlalchemy.text("""    
        SELECT *
        FROM letters
        WHERE
            number >= :min_number AND
            letter in :letters""")
    
    # read and return the resulting dataframe
    df = pd.read_sql(sql, conn, params=locals())
    return df

my_func(conn, 10, ['a', 'b', 'c', 'x', 'y', 'z'])

输出:

  letter  number
0      x      23
1      y      24
2      z      25

为了示例的完整性,以下用作测试表:

df = pd.DataFrame({
    'letter': list(string.ascii_lowercase),
    'number': range(len(string.ascii_lowercase))})
df.to_sql('letters', conn, index=False)

更新:以下是 Oracle 使其适用于列表的可能解决方法:

def get_query(sql, **kwargs):
    for k, v in kwargs.items():
        vs = "','".join(v)
        sql = sql.replace(f':{k}', f"('{vs}')")
    return sql

def my_func(conn, min_number, letters):
    sql_template = """    
        SELECT *
        FROM letters
        WHERE
            number >= :min_number AND
            letter in :letters
    """
    # pass list variables to `get_query` function as named parameters
    # to get parameters replaced with ('value1', 'value2', ..., 'valueN')
    sql = sqlalchemy.text(
        get_query(sql_template, letters=letters))
    
    df = pd.read_sql(sql, conn, params=locals())
    return df

my_func(conn, 10, ['a', 'b', 'c', 'x', 'y', 'z'])

更新 2:这是适用于字符串和数字的 get_query 函数(用引号括起来的字符串,但不是数字):

def get_query(sql, **kwargs):
    # enclose in quotes strings, but not numbers
    def q(x):
        q = '' if isinstance(x, (int, float)) else "'"
        return f'{q}{x}{q}'
    
    # replace with values
    for k, v in kwargs.items():
        sql = sql.replace(f':{k}', f"({','.join([q(x) for x in v])})")

    return sql

例如:

sql = """    
SELECT *
FROM letters
WHERE
    number in :numbers AND
    letters in :letters
"""

get_query(sql,
          numbers=[1, 2, 3],
          letters=['A', 'B', 'C'])

输出:

SELECT *
FROM letters
WHERE
    number in (1,2,3) AND
    letters in ('A','B','C')

【讨论】:

  • 显然它在 Oracle 驱动程序中不受支持,就像在 Postgres 中一样(请参阅here)。他们推荐的解决方法是在代码中生成sql,这当然是我们在这里试图避免的......
  • 我明白了,所以也许我的问题没有解决方案。那很不幸..如果我没有得到其他解决方案,我会接受您的回答 - 谢谢
  • 如果整体思路适合您,我们可以改进get_query 以动态检查不需要用引号括起来的数字类型并相应地生成值列表
  • 看起来不错,是的,自动替换列表变量的动态 get_query 可能会成功
  • @oskros 很酷,请参阅更新 2 以了解更新的 get_query 函数,它也可以处理数字类型
【解决方案2】:

为什么不这样:

import pandas as pd

def my_func(conn, string_id, date, integer_ids):
    sql = """    
    select * from RISK
    where STRING_ID = %s
    and DATE = %s
    and INTEGER_ID in %s"""
    df = pd.read_sql(sql, conn, (string_id, date,integer_ids))
    return df

【讨论】:

  • 因为它不能复制粘贴到 SQL 解释器中
  • @oskros 我没关注,复制粘贴什么?
  • 将 SQL 字符串直接从 .py 文件复制粘贴到 SQL 解释器中 - %s 将给出语法错误
  • 我明白了,但是你想要什么?
  • 我希望 SQL 存储在单独的 .sql 文件中,然后通过输入参数值直接通过 SQL 解释器执行,或者由 python 加载以在脚本中运行 - 这是需要的一些我公司的代码用 pl/sql 运行,其他东西用 python 编码
猜你喜欢
  • 1970-01-01
  • 2018-06-12
  • 2016-03-02
  • 1970-01-01
  • 1970-01-01
  • 2014-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多