如何在python中转义sqlite表/列名的字符串?

fruv7luv  于 2021-06-25  发布在  Mysql
关注(0)|答案(7)|浏览(530)

在sqlite查询中使用变量值的标准方法是“问号样式”,如下所示:

import sqlite3
with sqlite3.connect(":memory:") as connection:
    connection.execute("CREATE TABLE foo(bar)")
    connection.execute("INSERT INTO foo(bar) VALUES (?)", ("cow",))

    print(list(connection.execute("SELECT * from foo")))
    # prints [(u'cow',)]

但是,这只适用于将值替换到查询中。用于表或列名时失败:

import sqlite3
with sqlite3.connect(":memory:") as connection:
    connection.execute("CREATE TABLE foo(?)", ("bar",))
    # raises sqlite3.OperationalError: near "?": syntax error

也不是 sqlite3 模块nor pep 249提到了一个用于转义名称或值的函数。想必这是为了阻止用户用字符串组合查询,但这让我不知所措。
什么函数或技术最适合在sqlite中为列或表使用变量名?我非常希望能够做到这一点,而不需要任何其他依赖项,因为我将在自己的 Package 器中使用它。
我寻找了sqlite语法的相关部分,但是找不到一个清晰完整的描述,可以用来编写我自己的函数。我想确保这对sqlite允许的任何标识符都有效,所以试错解决方案对我来说太不确定了。
sqlite使用 " 引用标识符,但我不确定仅仅转义就足够了。php的 sqlite_escape_string 函数的文档表明某些二进制数据可能也需要转义,但这可能是php库的一个怪癖。

v2g6jxz6

v2g6jxz61#

如果您非常确定需要动态地指定列名,那么应该使用一个可以安全地指定列名的库(并抱怨出错)。炼金术在这方面很在行。

>>> import sqlalchemy
>>> from sqlalchemy import *
>>> metadata = MetaData()
>>> dynamic_column = "cow"
>>> foo_table = Table('foo', metadata,
...     Column(dynamic_column, Integer))
>>>
``` `foo_table` 现在用动态模式表示表,但是您只能在实际数据库连接的上下文中使用它(这样sqlalchemy就知道方言以及如何处理生成的sql)。

metadata.bind = create_engine('sqlite:///:memory:', echo=True)

然后你可以发布 `CREATE TABLE ...` . 与 `echo=True` ,sqlalchemy将记录生成的sql,但一般来说,sqlalchemy会特意将生成的sql从您的手中移开(以免您考虑将其用于邪恶的目的)。

foo_table.create()
2011-06-28 21:54:54,040 INFO sqlalchemy.engine.base.Engine.0x...2f4c
CREATE TABLE foo (
cow INTEGER
)
2011-06-28 21:54:54,040 INFO sqlalchemy.engine.base.Engine.0x...2f4c ()
2011-06-28 21:54:54,041 INFO sqlalchemy.engine.base.Engine.0x...2f4c COMMIT

是的,sqlalchemy会处理任何需要特殊处理的列名,比如当列名是sql保留字时

dynamic_column = "order"
metadata = MetaData()
foo_table = Table('foo', metadata,
... Column(dynamic_column, Integer))
metadata.bind = create_engine('sqlite:///:memory:', echo=True)
foo_table.create()
2011-06-28 22:00:56,267 INFO sqlalchemy.engine.base.Engine.0x...aa8c
CREATE TABLE foo (
"order" INTEGER
)
2011-06-28 22:00:56,267 INFO sqlalchemy.engine.base.Engine.0x...aa8c ()
2011-06-28 22:00:56,268 INFO sqlalchemy.engine.base.Engine.0x...aa8c COMMIT

也能让你免于可能的邪恶:

dynamic_column = "); drop table users; -- the evil bobby tables!"
metadata = MetaData()
foo_table = Table('foo', metadata,
... Column(dynamic_column, Integer))
metadata.bind = create_engine('sqlite:///:memory:', echo=True)
foo_table.create()
2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec
CREATE TABLE foo (
"); drop table users; -- the evil bobby tables!" INTEGER
)
2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec ()
2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec COMMIT

(显然,有些奇怪的东西在sqlite中是完全合法的标识符)
pu82cl6c

pu82cl6c2#

占位符仅用于值。列名和表名是结构化的,类似于变量名;不能使用占位符来填充它们。
您有三种选择:
在使用列名的任何地方适当地转义/引用列名。这是脆弱和危险的。
使用类似sqlalchemy的orm,它将为您处理转义/引用。
理想情况下,只是没有动态列名。表和列用于结构;任何动态的东西都是数据,应该在表中,而不是表的一部分。

csga3l58

csga3l583#

来自sqlite faq,问题24(问题的表述当然不会给出答案对您的问题有用的线索):
sql在包含特殊字符或关键字的标识符(列名或表名)周围使用双引号。所以双引号是一种转义标识符名称的方法。
如果名称本身包含双引号,请用另一个双引号转义该双引号。

pgvzfuti

pgvzfuti4#

要将任何字符串转换为sqlite标识符,请执行以下操作:
确保字符串可以编码为utf-8。
确保字符串不包含任何nul字符。
全部替换 """ .
把整件事用双引号括起来。

实施

import codecs

def quote_identifier(s, errors="strict"):
    encodable = s.encode("utf-8", errors).decode("utf-8")

    nul_index = encodable.find("\x00")

    if nul_index >= 0:
        error = UnicodeEncodeError("NUL-terminated utf-8", encodable,
                                   nul_index, nul_index + 1, "NUL not allowed")
        error_handler = codecs.lookup_error(errors)
        replacement, _ = error_handler(error)
        encodable = encodable.replace("\x00", replacement)

    return "\"" + encodable.replace("\"", "\"\"") + "\""

给定一个字符串单参数,它将转义并正确引用它或引发异常。第二个参数可用于指定在中注册的任何错误处理程序 codecs 模块。内置的有: 'strict' :在编码错误的情况下引发异常 'replace' :用适当的替换标记替换格式错误的数据,例如 '?' 或者
'\ufffd' 'ignore' :忽略格式错误的数据并继续,恕不另行通知 'xmlcharrefreplace' :替换为适当的xml字符引用(仅用于编码) 'backslashreplace' :替换为反斜杠转义序列(仅用于编码)
这不会检查保留的标识符,因此如果您尝试创建一个新的 SQLITE_MASTER 它不会阻止你。

示例用法

import sqlite3

def test_identifier(identifier):
    "Tests an identifier to ensure it's handled properly."

    with sqlite3.connect(":memory:") as c:
        c.execute("CREATE TABLE " + quote_identifier(identifier) + " (foo)")
        assert identifier == c.execute("SELECT name FROM SQLITE_MASTER").fetchone()[0]

test_identifier("'Héllo?'\\\n\r\t\"Hello!\" -☃") # works
test_identifier("北方话") # works
test_identifier(chr(0x20000)) # works

print(quote_identifier("Fo\x00o!", "replace")) # prints "Fo?o!"
print(quote_identifier("Fo\x00o!", "ignore")) # prints "Foo!"
print(quote_identifier("Fo\x00o!")) # raises UnicodeEncodeError
print(quote_identifier(chr(0xD800))) # raises UnicodeEncodeError

观察和参考

sqlite标识符是 TEXT ,不是二进制的。 SQLITE_MASTER 常见问题解答中的架构
python2sqliteapi在我给它不能作为文本解码的字节时对我大喊大叫。
Python3 sqlite api要求查询 str s、 不是 bytes .
sqlite标识符使用双引号引用。
sqlite所理解的sql
sqlite标识符中的双引号转义为两个双引号。
sqlite标识符保留大小写,但对ascii字母不区分大小写。可以启用unicode感知大小写不敏感。
sqlite常见问题18
sqlite不支持字符串或标识符中的nul字符。
sqlite票证57c971fc74 sqlite3 可以处理任何其他unicode字符串,只要它可以正确编码为utf-8。无效的字符串可能会导致python3.0和python3.1.2之间的崩溃。python2接受这些无效字符串,但这被认为是一个bug。
python问题#12569
modules/\u sqlite/cursor.c
我做了很多测试。

iqjalb3h

iqjalb3h5#

如果您发现需要一个变量实体名(relvar或field),那么您可能做错了什么。另一种模式是使用属性Map,例如:

j0pj023g

j0pj023g6#

首先要理解的是,表/列名的转义不能与作为数据库值存储的字符串的转义相同。
原因是您必须:
接受/拒绝潜在的表/列名,即不能保证字符串是可接受的列/表名,而不是要存储在某个数据库中的字符串;或者,
清理字符串,这将具有与创建摘要相同的效果:使用的函数是满射函数,而不是双射函数(同样,对于要存储在某个数据库中的字符串,则相反);因此,不仅不能确定是否要从经过清理的名称返回到原始名称,而且还可能无意中尝试创建两个同名的列或表。
理解了这一点之后,第二件要理解的事情是,最终如何“转义”表/列名取决于您的特定上下文,因此有多种方法可以做到这一点,但无论采用哪种方法,您都需要深入研究,以确定在sqlite中什么是可接受的列/表名,什么是不可接受的列/表名。
为了让您开始,这里有一个条件:
以“sqlite\”开头的表名保留供内部使用。尝试创建名称以“sqlite”开头的表是错误的。
更好的是,使用某些列名可能会产生意想不到的副作用:
每个sqlite表的每一行都有一个64位带符号整数键,它唯一地标识表中的行。这个整数通常称为“rowid”。可以使用一个与大小写无关的特殊名称“rowid”、“oid”或“rowid”代替列名来访问rowid值。如果表包含名为“rowid”、“oid”或“rowid”的用户定义列,则该名称始终引用显式声明的列,不能用于检索整型rowid值。
引用的两个文本都来自http://www.sqlite.org/lang_createtable.html

wribegjk

wribegjk7#

这个 psycopg2 文档明确建议使用普通python%或{}格式替换表和列名(或其他动态语法位),然后使用参数机制替换查询中的值。
我不同意任何人说“永远不要使用动态表/列名,如果需要的话,您正在做一些错误的事情”。我每天都写程序来自动化数据库,而且我一直都这么做。我们有很多数据库和很多表,但是它们都是基于重复的模式构建的,所以处理它们的通用代码非常有用。每次手工编写查询将更容易出错,也更危险。
归根结底,“安全”是什么意思。传统的看法是,使用普通的python字符串操作将值放入查询中并不“安全”。这是因为如果你这样做的话,会有各种各样的事情出错,而这样的数据往往来自用户,不在你的控制之下。您需要一种100%可靠的方法来正确地转义这些值,这样用户就不能在数据值中插入sql并让数据库执行它。所以图书馆的作者们做这项工作;你永远不应该。
但是,如果您正在编写通用帮助程序代码来操作数据库中的东西,那么这些注意事项就不太适用了。您隐式地给予任何可以调用此类代码的人访问数据库中所有内容的权限;这就是帮助程序代码的要点。因此,现在的安全问题是确保用户生成的数据永远不能在这样的代码中使用。这是编码中的一个普遍安全问题,与盲目编程是同一个问题 exec 正在初始化用户输入字符串。这与在查询中插入值是截然不同的问题,因为在那里您希望能够安全地处理用户输入数据。
所以我的建议是:做任何你想动态组装你的查询的事情。使用普通的python字符串模板生成表和列名,粘贴where子句和连接,所有好的(调试起来很糟糕)东西。但请确保您知道,这些代码涉及的任何价值观都必须来自您,而不是您的用户[1]。然后使用sqlite的参数替换功能将用户输入值作为值安全地插入到查询中。
[1] 如果(就像我写的很多代码一样)你的用户是那些可以完全访问数据库的人,而代码是为了简化他们的工作,那么这种考虑就不适用了;您可能正在用户指定的表上组装查询。但是,您仍然应该使用sqlite的参数替换来避免不可避免的真实值(最终包含引号或百分号)。

相关问题