1. 二维码到底是怎么被“画”出来的很多人第一次听到“用Python制作二维码”脑子里浮现的画面是打开一个绘图软件一个格子一个格子地填色。其实完全不是这么回事。二维码本质上是一套编码规则它把一段文本、一个网址、一串数字按照特定算法转换成黑白矩阵。Python做的事情是调用现成的编码库把这段逻辑跑完再输出成图片文件。我最早接触这个需求是帮一个做线下活动的朋友批量生成带参数的二维码。当时他手里有三百多个不同的链接需要每个链接对应一张二维码图片还要在图片下方标注编号。如果手动去某个网站一个个生成再一个个下载重命名一整天都干不完。用Python写个脚本十几行代码三分钟跑完。从那以后我就意识到二维码生成这件事看起来简单但真正把它用起来里面有不少门道。这篇文章适合谁看如果你是完全没碰过Python的新手我会把环境搭建、库的安装、代码逐行解释都写清楚你照着抄就能跑。如果你已经会写Python但没做过二维码相关的项目我会重点讲参数调优、批量处理、容错设计这些实战中才会遇到的问题。如果你是想把二维码集成到自己项目里的开发者我也会提到一些进阶用法比如嵌入Logo、生成矢量图、控制文件体积。核心关键词就一个Python二维码生成。围绕它我会把整个链路拆开从库的选型到代码实现从参数含义到踩坑记录全部摊开来讲。你不需要任何图像处理的基础只要会装Python就能跟着做下来。2. 为什么选Python来做二维码而不是在线工具2.1 在线工具的局限在哪里市面上生成二维码的在线工具一抓一大把输入文字点一下就能下载。但只要你稍微有点批量需求或者对二维码的样式有要求在线工具立刻就不够用了。我总结下来在线工具主要有三个硬伤。第一是批量处理能力几乎为零。你没法一次上传一个表格让它自动生成几百张图。有些工具号称支持批量但要么限制数量要么需要付费要么生成的文件名是一串乱码后期整理成本极高。第二是参数不可控。二维码有纠错等级、版本号、边距、像素密度这些参数。在线工具通常只给你一个“尺寸”选项其他全用默认值。但实际场景中纠错等级直接决定了二维码被遮挡或磨损后还能不能扫出来。比如你要把二维码印在容易磨损的包装上就需要调高纠错等级在线工具往往不给你这个选项。第三是无法集成到自动化流程里。假设你有一个系统每次生成订单时都要附带一个二维码你不可能让系统去调用某个网页工具。你需要的是在代码层面直接生成图片文件或者直接返回图片的二进制数据。这件事只有编程库能做到。2.2 Python在这件事上的独特优势Python做二维码生成最大的优势是库的成熟度极高。有一个叫qrcode的库基本上是这个领域的标准工具。它的接口设计非常直观几行代码就能出图。同时它又足够灵活底层参数几乎全部开放你想调什么都能调。另一个优势是和其他Python生态的无缝衔接。比如你用Pandas读取Excel里的链接列表用Pillow在二维码上叠加Logo用Flask把生成结果直接返回给前端。这些库之间的配合非常顺滑不需要做任何格式转换的额外工作。还有一个容易被忽略的点可复现性。在线工具生成的二维码你今天生成和明天生成可能因为工具更新而略有差异。但用Python脚本只要库的版本锁定任何时候跑出来的结果都是一模一样的。这对于需要版本管理的项目来说非常重要。2.3 常见库的对比与选型建议Python里能做二维码的库不止一个我实际用过的有三个qrcode、segno、pyqrcode。它们各有侧重选哪个取决于你的具体需求。库名安装方式核心特点适合场景qrcodepip install qrcode生态最成熟文档全支持PIL输出通用场景需要嵌入Logo时首选segnopip install segno纯Python实现无额外依赖支持SVG需要矢量图或环境受限无法装PILpyqrcodepip install pyqrcode轻量API简单快速生成简单二维码不需要复杂定制我个人的建议是如果你不确定选哪个直接用qrcode。它的社区最大遇到问题最容易搜到答案。而且它默认依赖Pillow而Pillow几乎是Python图像处理的标配装了不亏。segno的优势在于它不依赖任何第三方图像库纯Python就能跑如果你在一个受限环境里比如某些服务器不允许装PILsegno是很好的备选。注意qrcode库在生成图片时底层依赖Pillow。如果你只装了qrcode没装Pillow生成图片时会报错。所以安装时最好一起装上。3. 环境搭建与第一个二维码3.1 Python环境的准备如果你电脑上还没有Python先去官网下载安装包。版本选3.8以上就行我用的是3.10实测3.7到3.12都没问题。安装时记得勾选“Add Python to PATH”这个选项能省掉后面配置环境变量的麻烦。装完之后打开命令行输入python --version如果能看到版本号说明安装成功。如果提示找不到命令大概率是PATH没配好重新安装一次并勾选那个选项即可。接下来是安装库。我建议养成用虚拟环境的习惯虽然这一步对新手来说有点多余但能避免不同项目之间的库版本冲突。创建虚拟环境的命令是python -m venv qr_envWindows下激活qr_env\Scripts\activateMac或Linux下激活source qr_env/bin/activate激活之后命令行前面会出现(qr_env)的标识。然后安装我们需要的库pip install qrcode[pil]这个命令里的[pil]是告诉pip同时安装Pillow依赖。如果你只写pip install qrcodePillow不会被自动装上后面生成图片时会报错。3.2 五行代码生成第一张二维码环境准备好之后打开你常用的代码编辑器新建一个first_qr.py文件写入以下代码import qrcode img qrcode.make(https://example.com) img.save(my_first_qr.png)运行这个脚本当前目录下就会多出一个my_first_qr.png文件。用手机扫一下应该能跳转到对应的网址。这五行代码里qrcode.make()是最上层的封装它自动帮你选好了版本、纠错等级、边距等参数。对于简单需求这样用完全没问题。但如果你想控制二维码的细节就需要用更底层的QRCode类。3.3 理解二维码的基本参数在深入代码之前有必要先搞清楚几个核心参数的含义。这些参数决定了二维码长什么样、能不能被扫出来、适合什么场景。版本号version二维码有1到40个版本版本越高能存储的数据越多但图像也越复杂。版本1是21x21的矩阵版本40是177x177。qrcode库可以自动根据数据长度选择最小版本你也可以手动指定。纠错等级error_correction分为L、M、Q、H四个级别分别对应约7%、15%、25%、30%的数据冗余。级别越高二维码被遮挡或损坏后越容易恢复但同样数据量需要的版本也越高。日常使用选M就够了如果二维码要印在容易磨损的地方选Q或H。边距border二维码四周的空白区域单位是格子数。标准规定最少是4qrcode库默认也是4。边距太小会导致扫描器无法识别边界太大则浪费空间。像素密度box_size每个格子占多少像素。默认是10意味着一个版本1的二维码21x21格子生成的图片是210x210像素。如果你需要更高清的图把这个值调大即可。实操心得很多人生成的二维码扫不出来八成是因为边距设得太小或者纠错等级太低导致图像被压缩后失真。如果你要把二维码放在网页上建议边距至少设为4纠错等级至少设为M。4. 从基础到进阶二维码生成的完整实操4.1 用QRCode类精细控制输出qrcode.make()虽然方便但没法控制细节。真正做项目时我都是用QRCode类来构建。下面是一个完整的示例包含了所有常用参数的设置import qrcode qr qrcode.QRCode( version1, error_correctionqrcode.constants.ERROR_CORRECT_H, box_size10, border4, ) qr.add_data(https://example.com/some/long/path) qr.make(fitTrue) img qr.make_image(fill_colorblack, back_colorwhite) img.save(custom_qr.png)这里有几个关键点需要解释。version1表示强制使用版本1但如果数据长度超过版本1的容量make(fitTrue)会自动升级到合适的版本。如果你把fit设为False数据超长时就会直接报错。我一般建议保持fitTrue让库自己决定。fill_color和back_color控制前景色和背景色。默认是黑底白字但你可以改成任何颜色。不过要注意前景色和背景色的对比度必须足够高否则扫描器识别不了。深蓝配白色、黑色配浅黄都是不错的选择。千万别用红色配绿色对比度太低扫不出来。4.2 批量生成二维码的完整脚本批量生成是实际项目中最常见的需求。假设你有一个CSV文件里面有两列一列是编号一列是链接。你需要为每个链接生成一张二维码文件名用编号命名。先准备一个测试用的CSV文件links.csvid,url 001,https://example.com/page/1 002,https://example.com/page/2 003,https://example.com/page/3然后写脚本import csv import os import qrcode output_dir qr_output os.makedirs(output_dir, exist_okTrue) with open(links.csv, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: qr qrcode.QRCode( versionNone, error_correctionqrcode.constants.ERROR_CORRECT_M, box_size10, border4, ) qr.add_data(row[url]) qr.make(fitTrue) img qr.make_image(fill_colorblack, back_colorwhite) filename os.path.join(output_dir, f{row[id]}.png) img.save(filename) print(f已生成: {filename})这个脚本跑完qr_output目录下就会出现三张以编号命名的二维码图片。versionNone配合fitTrue让库自动选择最小可用版本避免浪费空间。注意事项批量生成时一定要加异常处理。如果某一行数据为空或者格式不对脚本会直接崩溃前面的成果也白费了。建议在循环里加try...except把出错的编号记录下来最后统一处理。4.3 在二维码中间嵌入Logo带Logo的二维码看起来更专业也更适合品牌场景。实现思路是先生成二维码图片再用Pillow打开Logo图片调整大小后粘贴到二维码正中间。import qrcode from PIL import Image qr qrcode.QRCode( versionNone, error_correctionqrcode.constants.ERROR_CORRECT_H, box_size10, border4, ) qr.add_data(https://example.com) qr.make(fitTrue) qr_img qr.make_image(fill_colorblack, back_colorwhite).convert(RGB) logo Image.open(logo.png) qr_width, qr_height qr_img.size logo_size qr_width // 5 logo logo.resize((logo_size, logo_size), Image.LANCZOS) pos ((qr_width - logo_size) // 2, (qr_height - logo_size) // 2) qr_img.paste(logo, pos) qr_img.save(qr_with_logo.png)这里有几个细节值得注意。纠错等级必须设为H因为Logo会遮挡一部分二维码区域只有最高纠错等级才能保证被遮挡后仍然可扫。Logo的尺寸不要超过二维码宽度的五分之一超过这个比例遮挡面积太大扫描成功率会明显下降。Image.LANCZOS是重采样算法能让缩放后的Logo边缘更平滑。如果你用的是较新版本的Pillow这个常量可能改名为Image.Resampling.LANCZOS两个都试一下哪个不报错用哪个。4.4 生成矢量图格式的二维码位图二维码在放大后会模糊如果你需要印刷或者在大屏幕上展示矢量图是更好的选择。qrcode库本身支持SVG输出但需要额外安装qrcode[pil]之外的依赖。更简单的方式是用segno库import segno qr segno.make(https://example.com, errorh) qr.save(qr_vector.svg, scale10)segno生成的SVG是纯矢量路径无论放大多少倍都清晰。scale参数控制每个格子的尺寸单位是毫米。如果你需要PDF格式segno也能直接输出qr.save(qr_vector.pdf, scale10)实操心得SVG格式的二维码在网页上使用时可以直接用img标签引用也可以用CSS控制大小非常灵活。但要注意有些老旧的扫描器对SVG渲染支持不好如果目标用户群体使用的设备比较杂还是用PNG更稳妥。5. 参数调优与常见问题排查5.1 二维码扫不出来的五大原因这是我踩过最多次的坑。二维码生成出来看着没问题但手机就是扫不出来。排查下来原因基本集中在以下五个方面。问题现象可能原因解决方法扫描器无反应边距太小将border设为4或更大扫描后跳转错误数据编码问题检查输入文本是否包含特殊字符部分设备能扫部分不能对比度不足提高前景色与背景色的对比度放大后模糊无法识别位图分辨率太低增大box_size或改用SVG带Logo后扫不出来遮挡面积过大缩小Logo或提高纠错等级边距这个问题特别隐蔽。标准规定二维码四周至少要有4个格子的空白但有些在线工具为了美观会把边距设得很小。用Python生成时border参数默认就是4但如果你手动改成了1或2扫描成功率会大幅下降。数据编码问题也值得展开说。二维码支持多种编码模式包括数字、字母数字、字节和汉字。qrcode库会自动选择最合适的模式但如果你输入的文本里混了emoji或者特殊符号可能会触发编码异常。遇到这种情况可以显式指定编码qr.add_data(包含特殊字符的文本.encode(utf-8))5.2 控制生成文件体积的技巧批量生成几千张二维码时文件体积会成为一个问题。一张默认参数的PNG二维码大约2到5KB一万张就是20到50MB。如果还要嵌入Logo体积会更大。控制体积最直接的方法是降低box_size。默认的10意味着每个格子10像素版本1的二维码就是210x210。如果你只是用于屏幕展示box_size设为4或5就够了文件体积能缩小到原来的四分之一。另一个方法是使用调色板模式。Pillow默认保存PNG时用的是RGB模式每个像素占3个字节。二维码只有黑白两色用模式“1”1位像素或“P”模式调色板能大幅压缩体积img qr.make_image(fill_colorblack, back_colorwhite) img img.convert(1) img.save(small_qr.png, optimizeTrue)optimizeTrue会让Pillow尝试用最优的压缩参数保存进一步减小体积。实测下来一张版本3的二维码用这种方式保存体积可以压到1KB以内。5.3 中文内容的处理要点二维码本身是二进制编码理论上可以存储任何数据。但中文内容有一个容易忽略的坑编码方式。如果你直接把中文字符串传给add_data()qrcode库会默认用UTF-8编码这本身没问题。但有些老旧的扫描器可能不支持UTF-8解码扫出来会是乱码。解决方法是显式指定编码模式。qrcode库支持通过QRCode类的add_data方法传入字节串并配合optimize参数控制编码选择qr.add_data(中文内容.encode(utf-8)) qr.make(fitTrue)如果你需要兼容性最大化可以考虑把中文内容先转成URL编码或者用一个短链接服务中转一下。不过现在主流手机的原生相机都能正确识别UTF-8编码的中文二维码这个问题在实际使用中已经很少遇到了。注意如果你的二维码内容是一个网址并且网址里包含中文参数一定要先做URL编码。否则生成的二维码虽然能扫出来但跳转时会出错。6. 把二维码集成到实际项目中的几种思路6.1 用Flask做一个在线生成接口如果你需要给团队提供一个二维码生成服务用Flask写一个简单的接口是最快的方式。核心思路是接收前端传来的文本参数在内存中生成二维码图片直接返回二进制数据不落盘。from flask import Flask, request, send_file import qrcode from io import BytesIO app Flask(__name__) app.route(/qr) def generate_qr(): text request.args.get(text, https://example.com) qr qrcode.QRCode( versionNone, error_correctionqrcode.constants.ERROR_CORRECT_M, box_size10, border4, ) qr.add_data(text) qr.make(fitTrue) img qr.make_image(fill_colorblack, back_colorwhite) buf BytesIO() img.save(buf, formatPNG) buf.seek(0) return send_file(buf, mimetypeimage/png)这个接口部署之后访问/qr?text你的内容就能直接得到一张二维码图片。前端可以直接用img src/qr?text...的方式引用非常方便。BytesIO是内存中的二进制流用它来中转图片数据避免了频繁读写磁盘。对于并发量不高的内部工具来说这种方式的性能完全够用。6.2 在二维码下方添加文字标注有时候光有二维码不够还需要在图片下方标注编号或说明文字。Pillow可以帮我们实现这个需求from PIL import Image, ImageDraw, ImageFont import qrcode qr qrcode.QRCode(box_size10, border4) qr.add_data(https://example.com) qr.make(fitTrue) qr_img qr.make_image(fill_colorblack, back_colorwhite).convert(RGB) qr_width, qr_height qr_img.size label_height 40 canvas Image.new(RGB, (qr_width, qr_height label_height), white) canvas.paste(qr_img, (0, 0)) draw ImageDraw.Draw(canvas) font ImageFont.truetype(arial.ttf, 20) text 编号: 001 bbox draw.textbbox((0, 0), text, fontfont) text_width bbox[2] - bbox[0] text_x (qr_width - text_width) // 2 draw.text((text_x, qr_height 5), text, fillblack, fontfont) canvas.save(qr_with_label.png)这里用ImageFont.truetype加载系统字体。Windows下arial.ttf通常在C:\Windows\Fonts目录下Mac下可以用/System/Library/Fonts/Supplemental/Arial.ttf。如果找不到字体文件可以用ImageFont.load_default()加载默认字体但默认字体不支持中文只能显示英文和数字。textbbox方法用于计算文字的边界框这样才能做到水平居中。不同版本的Pillow这个方法的返回值可能略有差异如果报错可以改用draw.textsize()不过这个方法在新版本中已被弃用。6.3 动态二维码与静态二维码的区别严格来说用Python生成的二维码都是静态二维码因为数据直接编码在图像里一旦生成就无法更改。市面上所谓的“动态二维码”其实是一个短链接指向一个可配置的跳转页面二维码本身存储的是这个短链接。如果你需要动态二维码的效果可以用Python生成一个指向你自己服务器的短链接然后在服务器端做重定向。这样你只需要在数据库里修改目标地址二维码本身不用重新生成。这个方案在营销场景中很常见比如一个二维码印在海报上但跳转的目标页面可以随时更换。实现思路是用Flask或FastAPI写一个重定向接口路径参数作为唯一标识查询数据库得到真实目标地址然后返回302重定向。二维码里存储的就是这个带标识的短链接。实操心得动态二维码的短链接域名一定要稳定如果域名过期或者服务器下线所有已印刷的二维码都会失效。建议至少注册一个长期域名并设置好续费提醒。7. 一些容易被忽略的细节和我的个人经验7.1 二维码的“容错”不是万能的很多人以为把纠错等级调到H二维码就可以随便遮挡。实际上纠错等级H的30%冗余是指码字级别的冗余不是面积级别的。而且Logo遮挡的是中心区域这个区域包含的是格式信息和部分数据遮挡过多仍然会导致无法解码。我实测下来的经验是纠错等级H的二维码中心区域被遮挡面积不超过15%时扫描成功率仍然很高。超过20%就开始不稳定超过25%基本扫不出来。所以Logo尺寸控制在二维码宽度的五分之一以内是比较安全的。7.2 颜色搭配的隐形陷阱二维码扫描器的工作原理是识别深色和浅色区域的对比。所以前景色必须比背景色深这是铁律。但很多人不知道的是扫描器对颜色的敏感度不同。红色和绿色的对比度在人眼看来很明显但在很多扫描器的灰度转换算法中这两种颜色的灰度值非常接近导致识别失败。安全的颜色组合是深蓝配白、黑色配浅黄、深绿配白。避免使用红色、橙色作为前景色也避免用浅灰作为背景色。如果你不确定某个配色是否安全可以用手机拍一张灰度照片看看如果前景和背景在灰度模式下仍然有明显差异那就是安全的。7.3 批量生成时的性能优化当你要生成上万张二维码时性能会成为瓶颈。我做过一个测试用默认参数生成一万张版本3的二维码单线程跑了大约4分钟。优化之后可以压缩到1分钟以内。优化的核心思路是复用QRCode对象。QRCode类的实例化有一定开销如果你在循环里每次都新建一个实例这部分开销会累积。更好的做法是创建一个实例每次循环时调用clear()方法重置数据qr qrcode.QRCode(box_size10, border4) for url in url_list: qr.clear() qr.add_data(url) qr.make(fitTrue) img qr.make_image() img.save(f{hash(url)}.png)另一个优化点是减少磁盘IO。如果不需要保留中间文件可以直接把图片数据写入内存或数据库。如果必须落盘使用SSD而不是机械硬盘速度差异非常明显。7.4 二维码的“版本”不是越高越好版本号越高二维码的矩阵越大同样物理尺寸下每个格子就越小。如果打印出来的二维码物理尺寸固定版本40的二维码每个格子可能只有0.1毫米普通打印机根本印不清楚手机也扫不出来。正确的做法是根据物理尺寸反推最大可用版本。假设你要印一个3cm x 3cm的二维码打印机的精度是300dpi那么3cm大约对应354像素。如果版本是1057x57格子每个格子约6像素完全够用。如果版本是40177x177格子每个格子只有2像素打印出来会糊成一团。所以生成二维码时不要盲目追求高版本而是要根据最终的使用场景来选择合适的版本和box_size组合。7.5 测试环节不能省二维码生成完之后一定要用至少两台不同品牌的手机测试扫描。我遇到过好几次这样的情况用某品牌手机扫没问题换另一个品牌就扫不出来。原因可能是扫描算法对边距或对比度的要求不同。测试时还要注意扫描距离和角度。有些二维码在正对时能扫出来稍微倾斜就不行。这通常是因为边距不够或者纠错等级太低。如果目标用户会在各种角度下扫描建议把纠错等级提高到Q或H边距设为4以上。最后再分享一个小技巧如果你不确定生成的二维码是否合规可以用qrcode库自带的print_ascii()方法在终端里打印出来看看。虽然终端里的显示比例和实际图片不同但至少能确认数据编码是否正确以及二维码的结构是否完整。这个方法在调试阶段非常实用不需要每次都生成图片文件。