资讯动态

Python3 变量写入 SQL 语句的 4 种方法:从拼接、参数化到元组传参

发布时间:2026/10/10 0:58:20 来源:尧图企业网站定制
1. 为什么把变量塞进 SQL 字符串会出事从一次登录接口的报错说起先还原一个特别常见的场景。你写了个注册接口前端把username和password传过来你想把这两条数据写进数据库。第一反应大概率是这样name alice password 123456 sql insert into userinfo values( name , password ) cursor.execute(sql)单看功能它确实能跑通数据也进去了。问题出在当name不是普通字符串的时候。假设有人在前端输入框里填了这么一段alice); drop table userinfo; --拼出来的 SQL 就变成了insert into userinfo values(alice); drop table userinfo; --,123456)分号把语句截断第二条drop table直接执行后面的内容被--注释掉。你的用户表就这么没了。这不是危言耸听SQL 注入常年排在各类安全风险榜单的前列根源就是「把用户输入当成 SQL 代码的一部分来拼接」。那正确的做法是什么核心思路只有一句话让 SQL 语句的结构和变量的值彻底分开。SQL 语句里只留占位符变量的值通过参数单独传给数据库驱动由驱动负责转义和绑定。这样无论用户输入什么数据库都只把它当成一个「值」而不是「代码」。Python3 里操作数据库主流驱动有sqlite3标准库自带、pymysql、mysql-connector-python、psycopg2等。它们对参数绑定的支持略有差异但整体套路一致。下面我会把 4 种写法从最危险到最安全依次讲清楚每种都给可直接复制的代码并用恶意输入实测注入是否被拦截。这篇文章适合谁刚学 Python 操作数据库、被「变量怎么加进 SQL」卡住的新手也适合写了几年代码但一直用字符串拼接、想系统梳理参数化写法的开发者。读完你能明确知道哪种写法该用、哪种必须扔进垃圾桶以及遇到TypeError、ProgrammingError这类报错时怎么排查。需要说明的是本文示例以sqlite3和pymysql为主因为这两个最容易在本地复现。如果你用的是其他数据库占位符符号会变比如 PostgreSQL 用%sOracle 用:name但「结构归结构、值归值」的原则完全一样。2. 动手前的准备TaoToken 接入与 sqlite3/pymysql 环境配置在写代码之前先把两件事准备好一个是数据库驱动环境另一个是如果你打算用大模型辅助生成或审查 SQL 代码可以顺手把 TaoToken 的接入配好。先说数据库环境这部分是硬性依赖。2.1 sqlite3 零配置起步sqlite3是 Python 标准库自带的不需要pip install直接 import 就能用。它把整个数据库存成一个文件特别适合本地练习和验证注入问题。建库建表import sqlite3 conn sqlite3.connect(test.db) cursor conn.cursor() cursor.execute( create table if not exists userinfo ( id integer primary key autoincrement, username text not null, password text not null ) ) conn.commit() conn.close()跑完这段当前目录下会多一个test.db文件表结构就建好了。后面所有 sqlite3 的示例都基于这张表。2.2 pymysql 连接 MySQL如果你用的是 MySQL先装驱动pip install pymysql然后建立连接。注意pymysql的占位符是%s不是?这点和 sqlite3 不一样后面会反复强调import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordyour_password, databasetestdb, charsetutf8mb4 ) cursor conn.cursor()charsetutf8mb4建议加上否则中文和 emoji 可能存进去变问号。2.3 用 TaoToken 辅助生成和审查 SQL 代码写参数化 SQL 的时候占位符符号、参数顺序、元组和字典的对应关系容易搞混。我习惯把表结构和需求丢给模型让它先出一版参数化写法再自己核对。TaoToken 的模型对话入口可以直接用模型对话https://taotoken.net/api/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite如果你要长期写数据库相关的代码甚至想让 Agent 帮你批量改老项目里的拼接 SQL可以看下 Coding PlanCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewriteAPI Key 在控制台里生成接入文档里有各语言的调用示例API Keyshttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite基础 API 地址是https://taotoken.net/api这个不带 UTM配置客户端时填这个。如果你用的是 Claude Code 这类编码工具Anthropic 兼容入口在这里ClaudeCodeAnthropichttps://taotoken.net/api/anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite环境准备好之后我们进入正题。下面 4 种方法我会按「危险程度」和「推荐程度」排开你可以对照自己的代码看看属于哪一种。3. 4 种变量写入 SQL 的写法从字符串拼接到命名参数的可复制配置这一节是全文的核心。我会把 4 种写法逐一拆开每种都给完整可运行的代码并说明它的适用场景和坑。3.1 方法一字符串拼接f-string / % / 这是最直观、也最危险的写法。三种变体# 变体 A加号拼接 sql insert into userinfo (username, password) values( name , password ) # 变体 B% 格式化 sql insert into userinfo (username, password) values(%s,%s) % (name, password) # 变体 Cf-string sql finsert into userinfo (username, password) values({name},{password})三种写法本质一样变量在 Python 层面就被塞进了 SQL 字符串等execute拿到手时SQL 结构已经被污染了。它们唯一的「优点」是写起来顺手但代价是安全性和可维护性双输。只要变量来自用户输入、外部接口、文件读取就绝对不能这么写。3.2 方法二问号占位符 元组传参sqlite3 推荐sqlite3 用?作为占位符参数以元组形式传给execute的第二个参数import sqlite3 conn sqlite3.connect(test.db) cursor conn.cursor() name alice password 123456 sql insert into userinfo (username, password) values(?, ?) cursor.execute(sql, (name, password)) conn.commit()注意(name, password)这个元组顺序必须和?一一对应。单个参数时也要写成(name,)那个逗号不能省否则 Python 会把它当成普通括号而不是元组这是新手最常踩的坑之一。查询同理sql select id, username from userinfo where username ? cursor.execute(sql, (name,)) rows cursor.fetchall() for row in rows: print(row)3.3 方法三%s 占位符 元组传参pymysql 推荐MySQL 的驱动普遍用%s注意这里虽然长得像 Python 的格式化符号但它是驱动自己的占位符不要在 Python 层面做%运算import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordyour_password, databasetestdb, charsetutf8mb4 ) cursor conn.cursor() name alice password 123456 sql insert into userinfo (username, password) values(%s, %s) cursor.execute(sql, (name, password)) conn.commit()批量插入时用executemany参数是「元组的列表」data [(bob, 111), (carol, 222), (dave, 333)] sql insert into userinfo (username, password) values(%s, %s) cursor.executemany(sql, data) conn.commit()executemany比循环单条execute快很多因为它减少了往返次数。3.4 方法四命名参数字典传参当参数多、顺序容易记混时命名参数可读性最好。sqlite3 用:namepymysql 用%(name)s# sqlite3 命名参数 sql insert into userinfo (username, password) values(:username, :password) cursor.execute(sql, {username: name, password: password}) # pymysql 命名参数 sql insert into userinfo (username, password) values(%(username)s, %(password)s) cursor.execute(sql, {username: name, password: password})命名参数的好处是顺序无关字典的 key 和占位符名字对应即可。参数一多这种写法比元组清晰得多。3.5 四种写法对照表写法占位符传参类型注入风险可读性推荐度字符串拼接无无极高差禁用问号占位符?元组无中sqlite3 首选%s 占位符%s元组无中pymysql 首选命名参数:name/%(name)s字典无高参数多时首选注意占位符符号由驱动决定不是由数据库决定。同一个 MySQL用 pymysql 是%s用 mysql-connector 也是%s但用某些 ORM 又是另一套。写之前先确认驱动文档。3.6 一个容易忽略的点表名和字段名不能参数化参数化只能用于「值」不能用于表名、字段名、order by后面的列名。下面这种写法是错的# 错误表名不能用占位符 sql select * from ? where id ? cursor.execute(sql, (userinfo, 1)) # 会报错如果表名或列名确实需要动态只能用白名单校验allowed_tables {userinfo, orders, products} table userinfo if table not in allowed_tables: raise ValueError(非法表名) sql fselect * from {table} where id %s cursor.execute(sql, (1,))这里虽然用了 f-string但table已经过白名单校验不是用户可控的任意字符串所以是安全的。这个边界要分清楚。4. 验证请求与成功结果用恶意输入实测注入是否被拦截光说安全不够得实测。这一节我用真实的恶意输入跑一遍看参数化到底拦不拦得住。4.1 构造恶意输入准备一个典型的注入 payloadevil_name alice); drop table userinfo; -- evil_password whatever4.2 用字符串拼接跑一遍危险演示import sqlite3 conn sqlite3.connect(test.db) cursor conn.cursor() evil_name alice); drop table userinfo; -- evil_password whatever sql insert into userinfo (username, password) values( evil_name , evil_password ) print(拼接后的 SQL, sql) try: cursor.execute(sql) conn.commit() except Exception as e: print(执行异常, e)打印出来的 SQL 是insert into userinfo (username, password) values(alice); drop table userinfo; --,whatever)sqlite3 的execute默认一次只执行一条语句所以这里会抛Warning: You can only execute one statement at a time。但换成某些允许堆叠查询的驱动或配置drop table就真的执行了。即便没执行成功这条 SQL 的结构已经被破坏说明拼接本身就是错的。4.3 用参数化跑同样的输入import sqlite3 conn sqlite3.connect(test.db) cursor conn.cursor() evil_name alice); drop table userinfo; -- evil_password whatever sql insert into userinfo (username, password) values(?, ?) cursor.execute(sql, (evil_name, evil_password)) conn.commit() # 查回来看看存进去的是什么 cursor.execute(select username, password from userinfo where password ?, (evil_password,)) print(cursor.fetchall())输出[(alice); drop table userinfo; --, whatever)]整段恶意字符串被当成一个普通的username值原样存进去了drop table没有执行表还在。这就是参数化的效果数据库把?位置的内容当成纯数据不做任何语法解析。4.4 再验证一次表是否还在cursor.execute(select name from sqlite_master where typetable and nameuserinfo) print(cursor.fetchall())输出[(userinfo,)]表完好。如果换成拼接写法且驱动允许堆叠查询这里就会返回空列表。4.5 pymysql 下的同样验证import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordyour_password, databasetestdb, charsetutf8mb4 ) cursor conn.cursor() evil_name alice); drop table userinfo; -- sql insert into userinfo (username, password) values(%s, %s) cursor.execute(sql, (evil_name, whatever)) conn.commit() cursor.execute(select username from userinfo where password %s, (whatever,)) print(cursor.fetchall())结果一样恶意字符串被完整存为用户名注入无效。提示验证注入时建议在本地或测试库上做别拿生产库练手。虽然参数化能拦住但拼接写法一旦跑在生产上后果不可逆。5. 本篇常见报错排查401、local proxy failed、reading choices 与 OAuth 对照写参数化 SQL 和接入模型辅助时会遇到几类典型报错。这一节按真实错误信息来对照排查。5.1 TypeError: not enough arguments for format string这个报错通常出现在 pymysql 里原因是 SQL 里有%s占位符但你没传参数或者参数数量对不上# 错误有 %s 但没传参 sql insert into userinfo (username, password) values(%s, %s) cursor.execute(sql) # TypeError解决补上元组参数cursor.execute(sql, (name, password))。还有一种情况是 SQL 里本来就有%字符比如like %abc%这时要把%转义成%%否则驱动会误以为那是占位符。5.2 sqlite3.ProgrammingError: Incorrect number of bindings suppliedsqlite3 里?的数量和元组元素数量不一致时会报这个sql insert into userinfo (username, password) values(?, ?) cursor.execute(sql, (name,)) # 只传了一个报错解决数清楚?有几个元组就放几个。单个参数记得写(name,)。5.3 pymysql.err.OperationalError: (1045, Access denied)这是连接层的认证失败和 SQL 写法无关。检查user、password、host是否正确以及该用户是否有远程连接权限。本地开发常见的是密码填错或 MySQL 用户只允许localhost登录。5.4 401 Unauthorized调用模型接口时如果你在用 TaoToken 的接口辅助生成 SQL 代码遇到 401通常是 API Key 没填、填错或者请求头格式不对。检查Authorization: Bearer 你的Key这个头是否完整。Key 在控制台生成API Keyshttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite5.5 local proxy failed / connection refused这类报错一般是本地网络配置问题比如客户端里填了代理地址但代理没启动或者 Base URL 写错。确认你填的 API 地址是https://taotoken.net/api不要多加路径或斜杠。如果客户端里之前配过代理先清掉再试。5.6 reading choices 相关报错调用对话接口时如果报解析choices失败通常是返回体不是预期的 JSON 结构可能因为请求被拦截、返回了 HTML 错误页或者模型名写错。先打印原始响应体看看到底返回了什么再对照接入文档检查请求参数接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite5.7 OAuth 相关报错如果你用的是 Claude Code 这类工具走的是 Anthropic 兼容入口遇到 OAuth 报错一般是认证方式没选对。这类工具通常支持 API Key 和 OAuth 两种模式用 TaoToken 的话选 API Key 模式Base URL 填ClaudeCodeAnthropichttps://taotoken.net/api/anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite5.8 配置三件套对照不管你用哪种客户端或工具接入时都要确认这三样齐全缺一个就连不上配置项填什么常见错误Base URLhttps://taotoken.net/api多写路径、漏写 httpsAPI Key控制台生成的 Key复制时带空格、Key 过期Model ID文档里列出的模型名名字拼错、用了不存在的模型以 Codex 的auth.json为例配置大致是这样{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: 文档里对应的模型ID }Cline 的 MCP 配置里同样要填全这三项Base URL、Key、Model ID 一个都不能少。CC Switch 切换配置时也检查这三件套是否完整。6. 把参数化写进你的代码习惯从今天起改掉拼接回到最开始那个问题变量怎么加进 SQL 语句。答案已经很清楚了——不要「加进去」而是用占位符留位置把值单独传。sqlite3 用?配元组pymysql 用%s配元组参数多了用命名参数配字典。字符串拼接无论用加号、%还是 f-string只要变量来自外部就必须改掉。我自己的习惯是写完一条 SQL先看里面有没有直接出现的变量。如果有立刻改成占位符。表名、列名这种没法参数化的走白名单校验。批量插入用executemany别循环单条执行。如果你想让模型帮你审查老项目里的 SQL 拼接或者批量生成参数化版本可以用模型对话入口把代码贴进去让它改模型对话https://taotoken.net/api/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite长期做数据库相关开发、需要 Agent 持续辅助的看 Coding PlanCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite最后留一个实操建议把你项目里所有execute调用搜一遍凡是 SQL 字符串里出现了、%、f且后面跟着变量的全部标记出来逐个改。改完用本文第 4 节的恶意输入跑一遍回归确认注入被拦住。这一步花不了多少时间但能挡掉一类很致命的问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑