前阵子公司要做一批设备资产码,大约一千多台机器,每台要生成一个内容不同、文件名又得按设备编号命名的二维码。一开始用在线生成器一张张来,效率低得离谱,复制内容、点下载、改名、存文件,一套下来十几秒就没了。后来索性用PyQt写了个桌面小工具,名字起得挺随意,叫Q-Generator Pro,一键导入名单,批量生成二维码,文件名直接按内容名称自动管好。这篇文章就把这个工具从设计到落地的完整思路、核心代码、踩坑记录都拆开讲,适合正在用PyQt做桌面工具的人,也适合那些被批量二维码需求折磨到想骂人的同学。
1. 项目背景与整体设计思路
1.1 这个工具要解决什么问题
批量生成二维码这件事,看起来很简单,但一旦数量上来,杂事就特别多。我梳理了一下实际业务场景,大致分三类:第一类是资产管理,几百上千台设备,每台都有一个唯一编号,需要把编号拼成URL或者纯文本,生成二维码后贴到设备上;第二类是会议签到或报名,需要给每个参会者生成一个带个人信息的二维码,文件名通常要求是“姓名_手机号”这种;第三类是生产批次追溯,比如车辆合格证、零部件流转单,内容里既有日期又有序列号,文件名也要能对上。
这三种场景有一个共同点:内容不是固定的,文件名也不是固定的,必须跟着每一条记录走。在线工具处理这种需求非常痛苦,你得一遍遍复制粘贴,而且很多在线工具生成的二维码分辨率不够,打印出来扫不出来。稍微正规一点的做法是用Excel插件,比如草料二维码的Excel插件,但插件能做的内容模板有限,文件名规则也不够灵活,更没法跟公司内部系统对接。所以结论很直接:需要一个本地运行的桌面小工具,导入数据、配置规则、一键批量输出。
Q-Generator Pro的定位就是解决这个“最后一公里”问题。它不需要连网,不需要数据库,一个列表、几个配置项、一个输出目录,就能把两个小时的重复劳动压缩到30秒。所谓“内容名称自定义”,说白了就是两点:二维码里装的内容可以按模板拼,输出文件名也可以按规则自动生成。这两点听起来简单,实际做起来绕了不少弯。
1.2 技术选型:PyQt、qrcode与第三方方案的取舍
技术选型这一步其实没太多纠结,但有一些取舍值得说说,给后面打算做类似工具的人做个参考。
桌面框架方面,我选的是PyQt5而不是PyQt6。PyQt6出来也有几年了,API上有不少调整,比如exec()改成exec()之后又改回去,枚举类型全部变成scoped enum,很多旧教程代码直接跑不通。PyQt5虽然官方维护进入后期,但胜在生态成熟、踩坑资料多,对一个内部工具来说稳定性比追新更重要。如果你是从零开始学,我建议也先用PyQt5把流程跑通,以后要迁移到PyQt6,记住几个改法就够了。
二维码生成库方面,主流选择有这么几个:Python的qrcode库、segno库,Java系的ZXing,C++系的qrencode。因为工具本身是Python写的,自然从qrcode和segno里选。qrcode库是用的最多、文档最全的,生成PNG、SVG都支持,还直接集成了Pillow,一个pip就装齐。segno的优点是生成的SVG体积更小、参数更精细,但资料相对少。我最后选了qrcode,理由很功利:我需要稳定、可控、遇到问题能搜到解决方案,而不是追求最极致的性能。
还有一堆现成的命令行工具,比如qrencode,但它是C程序,没法嵌进PyQt界面里做实时预览。Java的ZXing很强大,但为了一个桌面小工具去部署JRE环境,有点杀鸡用牛刀。C++的库编译链又是一堆事。所以PyQt + qrcode这个组合,不是最炫的,却是最省心的。
1.3 功能模块拆解与界面规划
动手写代码之前,我把工具拆成了四个模块:数据导入模块、规则配置模块、生成执行模块、结果反馈模块。
数据导入模块负责把Excel、CSV或者手动粘贴的数据整理成统一的记录列表。规则配置模块决定二维码内容怎么拼、文件名怎么起、用什么纠错级别和尺寸。生成执行模块是核心引擎,遍历列表、逐条调用二维码库生成图片、按规则写入文件。结果反馈模块负责进度条、日志和错误汇总,避免用户面对一个黑窗干等。
界面布局上,我把它做成三栏式:左侧是数据表格,中间是配置区,下方是操作按钮和进度区。这个顺序是照着人的操作习惯来的——先看数据,再设规则,最后点生成。不要做那种一打开就一堆选项弹窗的设计,桌面工具最怕的就是把简单事搞复杂。
界面规划里还有一个容易忽略的点:输出目录要单独一栏,而且要有“打开目录”按钮。因为批量生成完以后,用户第一反应就是去看产物,你直接帮他打开文件夹,体验会好很多。这个细节后续用起来我发现特别值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 UI交互:如何把批量流程做成顺手的工作台
PyQt的界面写起来不难,难的是把交互设计得顺手。我第一版就是粗暴地把所有控件堆在窗口里,结果用户(其实就是我自己)用起来很别扭,隔几天不用就忘了哪个按钮是干嘛的。
后来我按“三步走”重新组织了交互。第一步,导入数据:界面上有一个“导入Excel”按钮,下面是一张QTableWidget表格,导入后数据立刻显示出来,用户能直观检查。第二步,配置规则:表格下面是一排QLineEdit和一个QComboBox,分别填“内容模板”“文件名模板”“二维码纠错级别”,每个输入框带一个默认值。第三步,执行生成:右下角一个“开始生成”按钮,旁边是QProgressBar进度条,生成完如果有错误,弹日志区。
有个很实用的交互是“表格粘贴支持”。很多用户不愿意整理成Excel文件,他们的数据就在微信聊天记录里、在记事本里、在某个网页表格里。QTableWidget默认支持Ctrl+C和Ctrl+V吗?支持,但格式容易乱。我实现了一个简单的处理:粘贴时按制表符和换行符拆分,自动填入当前选中区域,这样从Excel里复制多行多列直接就能粘进来。这个功能虽然小,但直接砍掉了许多人“先存成文件再导入”的步骤。
配置区还有一个容易被低估的控件:实时预览。我放了一个QLabel,每次修改内容模板或文件名模板后,自动取列表第一行数据做一次试算,把“即将生成的二维码内容”和“文件名”即时显示出来。这个预览大概只用了十行代码,但解决了一个大痛点——规则写错不用等生成完才发现。
2.2 二维码参数调优:识别率与体积的平衡
二维码库里,QRCode类的几个参数直接决定成品的可用性,这里必须展开讲。
version参数控制二维码的版本,也就是尺寸和容量。version为1是21×21模块,每升一版增加4个模块,最大到version 40的177×177。qrcode库里可以手动指定版本,也可以传None让库自动推算。实测下来,如果内容是短网址或短编号,自动版本就够用;但如果内容是一长串带参URL或者JSON文本,自动版本会选一个刚刚够用的版本,导致模块密度特别大。模块太密,喷墨打印或者不干胶贴纸稍微糊一点,识别率就断崖式下跌。所以我后来默认让用户可以直接填死版本号,比如固定version=5,宁可图大一点,也要保证识别稳。
error_correction参数是纠错等级,四个档位L、M、Q、H分别对应约7%、15%、25%、30%的纠错能力。很多人忽略这个参数,但真实打印场景里,二维码被遮挡、被折痕压住、被高光扫到,太常见了。我的建议是:凡是准备打印到实物上的二维码,至少用M,推荐Q。H虽然纠错最强,但会明显增加模块密度,反而可能降低识别速度。这个平衡点要自己去试,批量生成前先打一两张实测。
box_size和border也不能不管。box_size是每个模块的像素数,等于是缩放倍数,默认10生成的图片比较小,打印到不干胶上会虚。我一般让用户填20以上,这样导出的是高分辨率图片,后续要缩小随时可以缩,但放大就会糊。border是静区宽度,标准要求至少4个模块的白色边距,千万别改成0,否则很多扫码枪会直接识别失败。这些参数我全部做成UI上的输入框,并在旁边标注默认值和推荐范围,因为不同打印场景确实需要不同设置。
2.3 文件名命名与内容模板的实现细节
“内容名称自定义”是标题里的重点词,实现上其实就是一个模板替换引擎,核心是用占位符替代列表里的字段。
我定义了一个约定:列表的每一列都有列名,比如“设备编号”“设备名称”“网址”。内容模板里写https://asset.example.com/detail?id={设备编号},生成时就会用每一行的设备编号去替换。文件名模板类似,比如{设备编号}_{设备名称}。这里有几个实现细节要注意。
第一,占位符解析不要用简单的replace循环,万一某个字段值里本身就包含另一个字段名,会出现意外替换。我用的方法是正则匹配花括号内的内容,再对每一个匹配项去字典里取值。第二,字段值可能为空或者None,模板替换后会出现一串undefined或者空字符串,这种二维码内容拼出来是错的。我在预处理阶段会把空值统一替换成“_”,并记录一条警告日志,用户可以回头查。第三,文件名里不能有\ / : * ? " < > |这些字符,Windows系统直接拒绝创建。所以文件名模板替换完之后,必须做一次清洗,把这些非法字符替换成下划线或去掉。
还有一个容易被忽略的点是文件名的唯一性。如果模板写得不严谨,比如只用“{设备名称}”做文件名,而列表里恰好有同名设备,后生成的会把先生成的覆盖掉。我在生成前会做一次预检,把计划生成的文件名列成一个集合,如果发现有重复,就在文件名末尾自动追加_1、_2,同时在日志里提醒用户检查模板。这个预检功能做出来以后,帮我拦下了好几次数据重复的乌龙。
3. 完整实操过程与关键代码实现
3.1 环境准备与依赖安装
做这个工具需要的依赖很少,核心就三个:PyQt5、qrcode、openpyxl。uWSGI、Django这些统统用不上。建议新开一个虚拟环境,防止搞乱系统Python。
bash复制python -m venv qgen_env
qgen_env\Scripts\activate # Windows下激活虚拟环境
pip install PyQt5 qrcode[pil] openpyxl
注意qrcode的依赖写法是qrcode[pil],这样会连带安装Pillow,生成PNG图片必须要用到。openpyxl用来读Excel文件,如果只需要读CSV,其实csv模块就能搞定,连这个依赖都可以省。但真实场景里Excel比CSV多得多,所以还是装一下。
如果还想生成SVG矢量二维码,需要额外安装qrcode[svg],不过我把输出格式默认设成PNG,因为打印场景下PNG更容易控制尺寸和渲染质量。SVG是个加分项,后期可以再加。
3.2 数据导入:Excel、CSV与表格粘贴
数据导入我写了两个入口:一个读Excel/CSV文件,一个直接解析剪贴板。
读Excel的代码不复杂,openpyxl的load_workbook配合worksheet.iter_rows就能把行数据读出来。要注意的是,Excel里某些单元格看起来是数字,实际类型是字符串,反过来也有日期类型,所以读取时最好统一转成字符串,避免后面模板替换时出类型问题。
python复制from openpyxl import load_workbook
def read_excel(path):
wb = load_workbook(path, data_only=True)
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
if not rows:
return [], []
headers = [str(h).strip() for h in rows[0]]
data = []
for row in rows[1:]:
item = {}
for idx, header in enumerate(headers):
val = row[idx] if idx < len(row) else None
item[header] = "" if val is None else str(val).strip()
data.append(item)
return headers, data
data_only=True这个参数非常重要。如果Excel里有公式单元格,不加这个参数读出来的是公式表达式,加了才能读到公式计算后的缓存值。我在一开始就踩了这个坑,批量生成出来的二维码内容全是公式,扫出来一堆“=设备编号&”之类的文本,血压直接拉满。
剪贴板粘贴的处理思路更简单:拿到QApplication.clipboard().text(),按\n拆行,按\t拆列,然后组装成同样的结构化数据。直接粘到当前QTableWidget里,用户随时能改。
3.3 核心生成逻辑代码与逐段说明
生成模块是整个工具的核心,也是我重构次数最多的模块。第一版直接在UI线程里跑for循环,一千条数据一生成,窗口直接变成“未响应”,体验惨不忍睹。后来改成后台线程加信号刷新进度条,才算真正能用。
先看最核心的单条生成函数:
python复制import qrcode
from qrcode.constants import ERROR_CORRECT_M, ERROR_CORRECT_Q
def generate_one(content, file_path, version=None, error_correction=ERROR_CORRECT_M, box_size=10, border=4):
qr = qrcode.QRCode(
version=version,
error_correction=error_correction,
box_size=box_size,
border=border,
)
qr.add_data(content)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(file_path)
return file_path
这套参数组合是我反复调过的。version传None表示让库自动选择,但如果前面的建议里固定了版本号,就把数字传进来。fit=True表示当version为None时自动选择合适的版本,这个必须开着。make_image里的fill_color和back_color,正常情况下保持黑黑白就行,别去做透明底或彩色底,识别率会受损。
然后是批量生成逻辑。为了不让界面卡死,我用QThread写了一个工作线程,里面遍历所有数据,每生成一条就发一个进度信号。这里不贴全部代码,重点说几个关键点。
python复制class BatchWorker(QThread):
progress = pyqtSignal(int, int, str) # 当前进度, 总数, 当前文件名
finished_ok = pyqtSignal(int, int) # 成功数, 失败数
def __init__(self, tasks, output_dir, params):
super().__init__()
self.tasks = tasks
self.output_dir = output_dir
self.params = params
self._cancelled = False
def run(self):
ok_count = 0
fail_count = 0
total = len(self.tasks)
for idx, task in enumerate(self.tasks):
if self._cancelled:
break
try:
content = render_template(self.params["content_template"], task)
filename = render_template(self.params["filename_template"], task)
filename = sanitize_filename(filename)
file_path = os.path.join(self.output_dir, filename + ".png")
generate_one(content, file_path, **self.params["qr_params"])
ok_count += 1
except Exception as e:
fail_count += 1
log_error(task, str(e))
self.progress.emit(idx + 1, total, task.get("名称", ""))
self.finished_ok.emit(ok_count, fail_count)
这里有个设计细节:把“内容模板”和“文件名模板”都通过render_template函数处理,这个函数就是刚才说的占位符替换器。这样内容用的是什么字段、文件名用的是什么字段,完全由用户配置决定,程序本身不用写死。generate_one里的参数通过params字典透传,UI上调整的版本、纠错级别、尺寸都能实时生效。
线程里对取消信号的支持也很重要。一千多条数据,有时候生成到一半发现规则写错了,没有取消按钮就得等全部跑完,相当折磨。我在worker里放了一个_cancelled标志位,UI上点击“停止”按钮就置为True,循环体每次检查,一旦置位立刻break,已经生成的文件保留,已完成的进度显示在日志里。这个机制等于给工具加了一条撤退路线,实际用下来非常有安全感。
3.4 打包成独立exe的注意事项
工具写好以后,大概率要发给同事用,不可能让每个人装Python环境,所以打包成exe是必须的。我用的是PyInstaller,命令行很简单:
bash复制pyinstaller -F -w --name QGeneratorPro main.py --hidden-import qrcode
-F是打包成单文件,-w是去掉控制台窗口,--hidden-import是为了防止PyInstaller漏掉qrcode这个动态导入的模块。如果用到openpyxl,一般不用额外指定,但建议打包完以后先在自己的Windows机器上跑一遍,生成几个二维码,确认所有功能正常再发出去。
打包还有一个坑:Win10、Win11的系统DPI缩放可能导致界面字体发虚。在主程序入口加上一句QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)能在PyQt5下明显改善。这个属性必须在创建QApplication之前设置,否则不生效。
4. 常见问题与排查技巧实录
4.1 二维码识别率低怎么排查
二维码生成出来以后扫不出来,是反馈最多的问题。我总结了三个最可能的原因。
第一个是版本和内容长度不匹配。如果内容里塞了很长的URL,自动版本会推高模块密度,导致打印后糊成一片。遇到这种情况,我给的建议是固定一个较高的version,比如6或8,强行放大模块尺寸,同时提高box_size,生成大图,打印时缩小到合适尺寸。这样虽然文件大一点,但识别率非常稳。
第二个是静区不够。border低于4或者图边缘被裁切,扫码枪会找不到定位角。检查方法很简单:把生成的图片在白色背景上放大到200%,看看二维码周围是否有一圈明显的白边。没有就改border=4重新生成。
第三个是深色背景或彩色背景。有些设计师喜欢把二维码做进深色海报,结果背景色一深,定位角被淹没,识别率直接归零。工业级工具别玩花活,就用白底黑码。如果实在要好看,可以保留白色浮雕底板,确保背景是纯色且与黑色模块有足够对比度。
4.2 中文编码与文件名非法字符问题
Windows下处理中文文件名和中文内容,很容易出乱码。这里有几个实操经验。
数据源是Excel时,openpyxl读到中文没问题,但一旦从CSV读取,就要注意编码。CSV文件经常是GBK或者UTF-8带BOM,如果用默认编码读,会直接变成乱码。我的处理办法是:先尝试UTF-8-sig读取,失败就换GBK读取,保证拿到的是正确的中文文本。
文件名非法字符清洗函数是必须的。replace单个字符会有遗漏,我用了一段正则:
python复制import re
def sanitize_filename(name):
name = re.sub(r'[\\/:*?"<>|\r\n]', "_", name)
name = name.strip(". ")
return name or "unnamed"
这个函数会把反斜杠、正斜杠、冒号、星号、问号、双引号、尖括号、竖线、回车换行全部替换成下划线,再去掉首尾的点和空格,避免文件名以点开头被Windows隐藏。最后如果清洗完是空字符串,给个默认名unnamed,防止保存报错。
还有一个坑:文件名的长度。微软文件系统单段文件名最长255个字符,如果模板里接了太多字段,生成到一半才报错太晚了。我在预检阶段就把超过200字符的文件名列出来,直接提示用户缩短模板,不用等生成完才发现。
4.3 界面卡顿与后台线程处理
PyQt界面卡死是最常见的桌面工具问题。QThread是标准解法,但有一个容易踩的暗坑:线程里不能直接操作UI控件。比如在线程里调用某个QLabel.setText,表面上可能不报错,但高频率更新时很大概率直接崩溃。正确的做法是线程只发信号,槽函数在UI主线程里执行更新。
信号连接还有一个细节:如果生成速度飞快,进度信号的发射频率可能非常高,导致UI线程来不及刷新,界面看起来还是卡。解决方法是控制进度更新频率,比如每处理10条才发射一次信号,或者用queue合并信号。我实测下来,每处理5到10条更新一次进度条,界面又流畅又不会显得迟钝。
如果数据量特别大,比如几万条,单线程遍历生成依然很慢。可以考虑用QThreadPool和QRunnable做多线程,但要小心磁盘IO竞争。SSD并发写没太大问题,机械硬盘并发写反而变慢。对绝大部分内部需求,单线程已经足够,别为了优化而优化。
4.4 大批量生成的性能优化
当批量数量到了几千甚至上万,有几个问题会浮出水面。
内存占用问题是最先出现的。如果把所有生成的QImage或PIL Image都放在列表里最后统一保存,几千张图的内存占用会非常可怕。我的做法是生成一张、保存一张,释放一张,整个流程的内存峰值只取决于单张图片大小,而不是总量。文件名有重复覆盖风险的问题,也提前在预检阶段处理掉。
另一个性能瓶颈是qrcode库本身的编码过程。对于纯数字内容,qrcode会自动采用数字模式,编码最快;对于URL或中文,则用字节模式或UTF-8模式,速度慢一些。这些模式是自动选择的,用户干预不了。如果确实需要极速生成,可以考虑把纯数字内容先转成shortened URL,但那样复杂度就高了,一般场景用不上。
我最后的优化是加了一个“跳过已存在文件”的选项。如果输出目录里已经存在同名文件,默认跳过不覆盖。这样二次生成只补缺失的,几百条的差量补充几秒钟就完成,不用全部重扫。
关于这个工具,我最后想说的
做Q-Generator Pro最大的收获,不是我写出了一个多高级的桌面程序,而是想明白了一件事:批量工具的本质,是把“重复劳动”抽象成“规则+数据”。数据是Excel里的那几列,规则是内容模板和文件名模板,工具要做的就是把这两者按用户看得懂的方式组合起来。这个思路后来还被我用到了其他场景,比如批量生成Word文档、批量重命名文件,都是一套逻辑。
最后再分享一个小技巧:你可以在配置区加一个“保存方案”的按钮,把当前的内容模板、文件名模板、二维码参数序列化成JSON文件。下次接到类似需求,直接“导入方案”,参数一键恢复,省去重新配置的时间。我就是靠这个功能,把平时零零散散的临时需求变成了几个固定的工作流,整个部门都说这个工具真能治各种格式混乱的问题。
