标签#Python #pymysql #MySQL #SQL注入 #后端踩坑前言在使用 pymysql 做业务开发时经常遇到文本字段包含单引号、双引号、反斜杠\、换行符等特殊字符。直接使用 f‑string /str.format()拼接 SQL就会出现 SQL 语法报错严重时还会带来 SQL 注入安全漏洞。很多新手会有一个误区pymysql 驱动会自动帮我把字符串转义处理好。实际上 pymysql不会处理你手动拼接进 SQL 字符串里的内容只有通过参数化查询传入的变量驱动才会自动完成转义。本文结合真实业务场景讲解报错根因、多种处理方案、以及生产环境避坑要点。一、复现问题单引号引发的SQL语法错误假设数据库表informationtitle 字段存储新闻标题标题内部包含英文单引号。示例数据SMS groups advanced technology upgrade❌错误写法直接 f‑string 拼接SQLimportpymysql connpymysql.connect(host127.0.0.1,userroot,passwordxxx,databasetest,charsetutf8mb4)cursorconn.cursor()titleSMS groups advanced technology upgrade# 直接字符串拼接sqlf SELECT id FROM information WHERE title {title} cursor.execute(sql)执行直接抛出异常pymysql.err.ProgrammingError: (1064, You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near s advanced technology upgrade at line 2)报错根因拼接之后发送给 MySQL 的SQL片段WHEREtitleSMS groups advanced technology upgrade字符串内部的单引号提前闭合了SQL字符串字面量破坏了SQL语法结构。不止单引号双引号、反斜杠、\0空字节都会带来同类问题。二、最优方案参数化查询生产环境标准方案pymysql 使用%s作为占位符不要给占位符手工加单引号变量通过元组传入execute的第二个参数。驱动底层自动完成特殊字符转义、字符串加引号、防御SQL注入。✅正确示例importpymysql connpymysql.connect(host127.0.0.1,userroot,passwordxxx,databasetest,charsetutf8mb4)cursorconn.cursor()titleSMS groups advanced technology upgradehrefhttps://example.com/demospider_namesms_press_releases_spider# SQL中写 %s占位符不要加 sql SELECT id,uuid,hash,version_no FROM information WHERE title %s AND title_href %s AND spider_name %s # 参数按顺序放入元组cursor.execute(sql,(title,href,spider_name))rowscursor.fetchall()print(rows)cursor.close()conn.close()参数化查询重点注意事项占位符是%s不是问号?不是{}%s外面绝对不要手动写单引号第二个参数接收元组(val1,val2,...)只有一个参数也要写逗号(title,)底层驱动根据连接的charset完成转义兼容utf8mb4。参数化查询同时解决两件事特殊字符语法报错 SQL注入漏洞。业务代码强制优先使用此方案。三、特殊场景非要手动拼接SQL字符串怎么办⚠️警告生产业务尽量避免拼接SQL该方案多用于日志打印、调试场景。pymysql connection 对象提供escape_string()方法专门对字符串做SQL转义。功能→\→\\→\\过滤\0空字节等危险字符注意escape_string()只转义内容不会添加外层单引号拼接SQL的时候你必须自己手动加上单引号。importpymysql connpymysql.connect(host127.0.0.1,userroot,passwordxxx,databasetest,charsetutf8mb4)cursorconn.cursor()titleSMS groups advanced technology upgrade# 使用连接对象的escape_string做转义esc_titleconn.escape_string(title)# 拼接时手动包裹单引号sqlfSELECT id FROM information WHERE title {esc_title}cursor.execute(sql)print(sql)输出拼接后的SQL片段SELECTidFROMinformationWHEREtitleSMS group\s advanced technology upgrade使用escape_string()的致命坑必须依赖真实的数据库连接对象转义逻辑受连接的charset字符集影响不能脱离连接离线预处理。不要自己写简单的replace(,\\)手写替换会漏掉反斜杠、空字节等存在绕过注入风险。❌不推荐手写替换# 不要这么写存在安全隐患bad_titletitle.replace(,\\)区分数据类型只对字符串调用escape_string()数字、时间等不需要转义也不要套单引号。转义后的字符串拼接时外层必须手动加单引号忘记加引号直接语法报错。四、调试拿到完整渲染后的SQL用于日志打印有时候我们想看完整的最终SQL用于日志排查但又不想自己手动拼接可以使用cursor.mogrify()。mogrify只会生成SQL字符串用于打印业务执行依然使用参数化execute不要把mogrify输出再丢给execute执行。importpymysql connpymysql.connect(host127.0.0.1,userroot,passwordxxx,databasetest,charsetutf8mb4)cursorconn.cursor()titleSMS groups advanced technology upgradesqlSELECT id FROM information WHERE title %sparams(title,)# 打印完整可调试SQLfull_sqlcursor.mogrify(sql,params)print(完整SQL,full_sql)# 正常参数化执行cursor.execute(sql,params)五、常见踩坑清单坑1给占位符加上单引号# ❌错误sqlSELECT * FROM information WHERE title%scursor.execute(sql,(title,))会把%s当成字面量字符串不会执行参数替换。坑2混用拼接和参数化一部分参数f-string拼接一部分使用%s占位符极易漏转义造成漏洞。坑3离线预处理字符串没有数据库连接自己写replace做转义遇到特殊编码、反斜杠会出问题。坑4忘记设置charsetutf8mb4如果连接使用utf8无法存储emoji同时转义行为会异常。六、ORM框架下如何处理特殊字符Django ORM / SQLAlchemy 底层全部封装参数化查询正常写filter不需要关心单引号转义。Django ORM示例# ORM内部自动参数化不需要手动处理单引号objInformation.objects.filter(titleSMS groups advanced technology upgrade).first()只有当你在ORM中手写原生SQL的时候依然要遵守参数化规则不要f-string拼接。Django原生SQLfromdjango.dbimportconnection sqlSELECT id FROM information WHERE title %swithconnection.cursor()ascur:cur.execute(sql,(title,))七、总结问题根源手动f‑string拼接SQL字符串内部的单引号、反斜杠破坏SQL语法。pymysql不会处理已经拼接进SQL文本的变量。✅生产首选参数化查询cursor.execute(sql, params)自动处理特殊字符同时防御SQL注入。调试/特殊场景需要拼接使用conn.escape_string()拼接时手动增加外层单引号禁止手写replace简单替换。打印完整SQL日志使用cursor.mogrify()只用于日志不要拿来执行。不要脱离数据库连接做离线转义字符集会影响转义结果。很多同学把精力放在如何把特殊字符替换干净实际上最好的办法就是从根源避免字符串拼接SQL。延伸阅读很多线上SQL注入漏洞根源都不是复杂攻击仅仅是开发者图方便使用f-string拼接SQL遇到带单引号的业务数据直接破防。特殊字符报错只是表象背后隐藏的是安全风险。记住一条铁律业务变量永远不要直接拼接到SQL字符串本体中。