使用 pymysql 的時候如何正確的處理轉義字元

ponponon發表於2021-12-26

方案一:使用 %s 佔位符

這也是官方推薦的方案,優點:

  • 不需要自己關注需不需要加引號的問題(自動對字串型別加引號,不會對數字型別加引號)
  • 對不同型別的引數都可以自動轉義(數字、字串、位元組等等)
"自動對字串型別加引號,不會對數字型別加引號",加引號這個操作是 python 語言的特性,而是 pymysql 幫我們處理的,文後有解釋

示例程式碼:

import pymysql.cursors

# Connect to the database
connection = pymysql.connect(host='localhost',
                             user='user',
                             password='passwd',
                             database='db',
                             cursorclass=pymysql.cursors.DictCursor)

with connection:
    with connection.cursor() as cursor:
        sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s"
        cursor.execute(sql, ('webmaster@python.org',))
        result = cursor.fetchone()
        print(result)

關於佔位符風格的更多內容請參考:pep249:paramstyle

方案二:手動呼叫 escape 方法

佔位符雖好,但用諸如 f-stringformat 來拼接字串的時候,就要手動來處理轉義字元的問題了!

先通過原始碼來看看 cursor.execute 背後都對引數引數做了什麼加工!

?當呼叫 execute 方法的時候,會使用 mogrify 對引數 args 進行加工。

mogrify 通過谷歌翻譯的結果是:升級

pymysql/cursors.py

def execute(self, query, args=None):
    """Execute a query

    :param str query: Query to execute.

    :param args: parameters used with query. (optional)
    :type args: tuple, list or dict

    :return: Number of affected rows
    :rtype: int

    If args is a list or tuple, %s can be used as a placeholder in the query.
    If args is a dict, %(name)s can be used as a placeholder in the query.
    """
    while self.nextset():
        pass

    query = self.mogrify(query, args)

    result = self._query(query)
    self._executed = query
    return result

?當呼叫 mogrify 方法的時候,會使用 _escape_args 對引數 args 進行加工。
我們只討論 if isinstance(args, (tuple, list)): 這種條件,這也是最常用的方式,進入該條件之後,會呼叫 literal 方法來對每個引數進行加工

pymysql/cursors.py

def _escape_args(self, args, conn):
    if isinstance(args, (tuple, list)):
        return tuple(conn.literal(arg) for arg in args)
    elif isinstance(args, dict):
        return {key: conn.literal(val) for (key, val) in args.items()}
    else:
        # If it's not a dictionary let's try escaping it anyways.
        # Worst case it will throw a Value error
        return conn.escape(args)

def mogrify(self, query, args=None):
    """
    Returns the exact string that is sent to the database by calling the
    execute() method.

    This method follows the extension to the DB API 2.0 followed by Psycopg.
    """
    conn = self._get_db()

    if args is not None:
        query = query % self._escape_args(args, conn)

    return query

?當呼叫 literal 方法的時候,會使用 escape 方法對引數 arg (此處換了一個稱呼:obj)進行加工。不同型別的引數的處理方案不同,對於字串型別會採用 escape_string 方法,位元組型別會採用 escape_bytes 方法(_quote_bytes 呼叫的就是 escape_bytes),其他型別就是 escape_item 方法。

所以我們可以根據引數型別自己選擇要呼叫哪個方法來處理轉義字元問題,一般來說,只需要關注字串即可。

最好的方式就是我們直接呼叫 escape 方法,免得我們自己去處理資料型別的問題,但是 escape 方法是一個類方法,不直接暴露給我呼叫。
escape_stringescape_bytesescape_item 方法是在 pymysql/converters.py 中的函式,可以直接呼叫。

pymysql/connections.py

def escape(self, obj, mapping=None):
    """Escape whatever value you pass to it.

    Non-standard, for internal use; do not use this in your applications.
    """
    if isinstance(obj, str):
        return "'" + self.escape_string(obj) + "'"
    if isinstance(obj, (bytes, bytearray)):
        ret = self._quote_bytes(obj)
        if self._binary_prefix:
            ret = "_binary" + ret
        return ret
    return converters.escape_item(obj, self.charset, mapping=mapping)

def literal(self, obj):
    """Alias for escape()

    Non-standard, for internal use; do not use this in your applications.
    """
    return self.escape(obj, self.encoders)

所以,當我們需要手動轉義的時候,就可以直接呼叫 escape 方法。

從 escape 方法程式碼中可以看到,當引數是字串的時候,就會在前後加上 ' 單引號,這也就是回答了文章開頭那個 "自動對字串型別加引號,不會對數字型別加引號" 問題

if isinstance(obj, str):
   return "'" + self.escape_string(obj) + "'"

? 通過下面的程式碼,我們就通過使用非常 pythonic 的 f-string 來處理 sql 了,但是需要注意的是 {} 需要自己新增外面的引號了

示例程式碼:

import pymysql.cursors
from pymysql.converters import escape_string
# Connect to the database
connection = pymysql.connect(host='localhost',
                             user='user',
                             password='passwd',
                             database='db',
                             cursorclass=pymysql.cursors.DictCursor)


user = escape_string('webmaster@python.org')
password = escape_string('very-secret')

with connection:
    with connection.cursor() as cursor:
        # Create a new record
        sql = "INSERT INTO `users` (`email`, `password`) VALUES ('{user}', '{password}')"
        cursor.execute(sql)

    # connection is not autocommit by default. So you must commit to save
    # your changes.
    connection.commit()

擴充套件:使用佔位符的同時獲取完整的 sql 語句

假設有如下需求:需要獲取完整的 sql 語句記錄在日誌中,又想使用佔位符來處理轉義字元問題,也可以用上面的方法來處理!

相關文章