ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

纯Python实现条形码生成:Code 128与EAN-13编码原理与工程实践

2026/8/18 3:38:12 拓冰建站 浏览量
纯Python实现条形码生成:Code 128与EAN-13编码原理与工程实践 1. 为什么需要纯Python生成条形码在数据处理、库存管理、商品流通的各个环节条形码Barcode早已是无处不在的“数字身份证”。无论是超市收银台“嘀”的一声还是快递员手持终端扫描包裹背后都是条形码技术在默默工作。对于开发者而言集成条形码功能的需求也相当普遍可能是为内部管理系统生成资产标签可能是为电商平台动态创建商品码也可能是为线下活动打印入场券。过去这类需求往往依赖于第三方库比如著名的python-barcode它功能强大但底层依赖Pillow这样的图像处理库和reportlab这样的PDF生成库。这本身没问题但在某些极端场景下依赖会成为障碍。例如在Serverless函数如AWS Lambda环境中为了控制部署包大小和冷启动时间需要极力精简依赖又或者在一些高度受限的嵌入式或边缘计算设备上安装完整的图像处理库栈可能非常困难甚至不可能。这时“纯Python”方案的价值就凸显出来了。它意味着不依赖任何需要编译的C扩展库如Pillow中处理图像的核心部分仅使用Python标准库。这样的代码移植性极强几乎可以在任何能运行Python解释器的环境中工作部署简单到只需复制一个.py文件。当然凡事都有代价纯Python方案通常在性能上无法与基于C的库媲美且功能上可能只覆盖最核心、最通用的码制。但对于许多轻量级、批处理量不大或环境特殊的应用来说它提供了一个极其优雅和可靠的备选方案。本文将深入探讨如何使用纯Python生成几种最常用的条形码特别是Code 128和EAN-13。我们会从编码原理讲起一步步实现从数据到黑白条空图像的转换并最终输出为PPM格式的图片文件。你会发现剥离了复杂的依赖之后条形码生成的本质逻辑是如此清晰和有趣。2. 理解条形码不止是黑白条纹在动手写代码之前我们必须先理解条形码到底是什么。很多人以为它只是一组宽窄不一的线条实际上它是一个精密的编码系统包含了数据、校验和以及特定的起始/终止标志。2.1 常见码制与选择条形码家族庞大不同码制适用于不同场景EAN-13 (European Article Number)最常见的商品条形码全球通用。固定13位数字前2-3位是国家代码接着是厂商代码、商品代码最后一位是校验码。它的编码规则相对固定。Code 128非常高效的高密度码制支持全部128个ASCII字符故名Code 128。它拥有三个不同的字符集A、B、C可以在一维码中编码数字、字母甚至控制字符广泛应用于物流、仓储管理。它的编码灵活性使其成为我们实现的重点。Code 39较早期的一种码制编码简单但密度较低。只能编码数字、大写字母及少数几个符号如-, ., $, /, , %, 空格通常需要首尾的“*”号作为起止符。QR Code这是二维码属于矩阵式二维条码信息容量和纠错能力远超一维码。纯Python实现二维码逻辑更为复杂涉及纠错编码、掩模等本文暂不涉及。对于纯Python实现Code 128是一个理想的起点。因为它应用广泛编码规则公开且结构化实现过程能很好地揭示条形码生成的通用原理。EAN-13作为另一种经典码制其编码方式左侧奇偶性编码也颇具代表性我们可以一并实现作为对比。2.2 条形码的结构解剖一个完整的条形码图像不仅仅是编码后的条空序列。以Code 128为例它的物理结构如下左侧空白区 (Left Quiet Zone)一段空白区域用于扫描设备定位起始点。起始符 (Start Character)标识码制类型A, B, C集。数据符 (Data Characters)实际编码的数据每个字符由11个“模块”宽的条空组合表示。校验符 (Check Character)根据一个特定公式通常是模103运算计算出的校验码用于确保数据读取的准确性。终止符 (Stop Character)一个特殊的条空模式标识条形码结束其后通常还跟着一个终止“条”。右侧空白区 (Right Quiet Zone)与左侧对称的空白区域。注意这里的“模块”是条形码宽度的基本单位。一个“条”或一个“空”的宽度可以是1个、2个、3个或4个模块宽。编码表定义了每个字符对应的由6个“条”或“空”组成的序列它们的宽度之和总是11个模块。理解了这个结构我们的任务就清晰了给定一个字符串我们需要选择合适的字符集将其中的每个字符映射为特定的条空序列计算校验码加上起始符和终止符最后将所有模块转换为像素点绘制成图像。3. 构建核心Code 128编码器让我们从最复杂的Code 128开始。纯Python实现的核心是构建一个完整的编码映射表并实现编码逻辑。3.1 定义编码表Code 128的编码表是公开的。每个可打印字符或功能字符对应一个数值0-105和一个由3个“条”和3个“空”共6个元素组成的序列每个元素的宽度是1到4个模块。 我们需要在代码中定义一个这样的字典。为了节省篇幅这里展示关键部分# Code 128编码映射表 (字符值 - [条1宽, 空1宽, 条2宽, 空2宽, 条3宽, 空3宽]) CODE128_CHARSET { # 字符集B的映射 (常用支持数字、大写字母、小写字母及常用符号) : [2, 1, 2, 2, 2, 2], # 值 0 !: [2, 2, 2, 1, 2, 2], # 值 1 : [2, 2, 2, 2, 2, 1], # 值 2 #: [1, 2, 1, 2, 2, 3], # 值 3 // ... 此处省略大量中间映射 ... 0: [1, 2, 2, 2, 1, 3], # 值 16 1: [1, 2, 2, 3, 1, 2], # 值 17 // ... 更多数字和字母 ... A: [2, 2, 1, 2, 2, 2], # 值 33 B: [2, 2, 2, 2, 1, 2], // ... 以此类推 // 功能字符如起始符、终止符 103: [2, 1, 2, 3, 2, 1], # START_B (值103) 106: [2, 2, 3, 1, 1, 2], # STOP 图案 (值106 注意STOP后还有一个2模块宽的“条”) }实际上完整的映射表有107项。我们还需要一个反向字典用于根据字符查找其对应的数值这在计算校验和时会用到。3.2 编码逻辑与校验和计算编码一个字符串的基本步骤如下选择起始符根据输入字符串的特征选择字符集A、B或C。为简化我们默认使用字符集BSTART_B值103因为它覆盖了最常见的字符。转换数据将输入字符串的每个字符通过反向字典转换为其对应的数值存入一个列表。计算校验码校验和的初始值是起始符的值。遍历数据数值列表将每个数值乘以它在序列中的位置起始符是位置1第一个数据是位置2以此类推然后累加。将累加和除以103取余数。这个余数就是校验码的数值。根据这个数值在编码表中找到对应的条空序列。构建完整序列将起始符的条空序列、所有数据字符的条空序列、校验字符的条空序列、终止符的条空序列按顺序拼接起来。记住终止符序列后要额外添加一个宽度为2的“条”。def encode_code128(data): 将字符串编码为Code 128的条空模块宽度序列 if not data: return [] # 1. 选择START_B (值103) start_pattern CODE128_CHARSET[103] encoded_sequence start_pattern[:] # 复制起始符序列 # 用于校验和计算的数值列表 value_sequence [103] # 起始符值 # 2. 编码每个字符 for char in data: if char not in CHAR_TO_VALUE: raise ValueError(f字符 {char} 不在Code 128字符集B中) char_value CHAR_TO_VALUE[char] char_pattern CODE128_CHARSET[char] encoded_sequence.extend(char_pattern) value_sequence.append(char_value) # 3. 计算校验码 checksum value_sequence[0] # 从起始符开始 for i, value in enumerate(value_sequence[1:], start1): # 从第一个数据字符开始 checksum value * (i 1) # 位置是 i1因为起始符是位置1 checksum_value checksum % 103 checksum_pattern CODE128_CHARSET[checksum_value] encoded_sequence.extend(checksum_pattern) # 4. 添加终止符 stop_pattern CODE128_CHARSET[106] encoded_sequence.extend(stop_pattern) # 终止符后的额外“条” encoded_sequence.append(2) return encoded_sequence这个函数返回一个列表其中元素交替代表“条”和“空”的模块宽度。列表长度是偶数因为总是以“条”开始以“条”结束。3.3 从模块序列到图像像素得到模块宽度序列后我们需要将其渲染为图像。我们将输出为PPM (Portable Pixmap)格式这是一种纯文本的、无需任何依赖即可读写的图像格式。def modules_to_ppm(module_sequence, height100, bar_width2, quiet_zone10): 将条空模块序列转换为PPM格式的字符串。 :param module_sequence: 交替的条/空宽度列表 :param height: 条形码图像高度(像素) :param bar_width: 每个模块的像素宽度 :param quiet_zone: 左右空白区的模块宽度 :return: PPM文件内容字符串 # 计算图像总宽度 total_modules sum(module_sequence) 2 * quiet_zone img_width total_modules * bar_width # PPM头 (P3表示ASCII格式的RGB255表示最大颜色值) ppm_header fP3\n{img_width} {height}\n255\n # 构建一行像素数据 (黑白二值0为黑255为白) # 我们先生成一行然后复制height次。 row_pixels [] # 左侧空白区 (白色) row_pixels.extend([255, 255, 255] * (quiet_zone * bar_width)) # 绘制条和空 is_bar True # 序列以“条”开始 for width in module_sequence: color [0, 0, 0] if is_bar else [255, 255, 255] # 黑或白 # 将当前颜色重复 width * bar_width 次 row_pixels.extend(color * (width * bar_width)) is_bar not is_bar # 切换条/空 # 右侧空白区 (白色) row_pixels.extend([255, 255, 255] * (quiet_zone * bar_width)) # 将一行像素数据重复height行并格式化为字符串 all_pixels row_pixels * height # 将像素值列表转换为空格分隔的字符串每行不超过70字符是PPM的惯例 pixel_data .join(map(str, all_pixels)) # 简单的换行处理非必须但使文件更易读 from textwrap import wrap pixel_lines wrap(pixel_data, width70) pixel_data_formatted \n.join(pixel_lines) return ppm_header pixel_data_formatted现在我们可以将两者结合生成一个完整的Code 128条形码def generate_code128_barcode(data, filenamebarcode.ppm, **kwargs): 生成Code 128条形码并保存为PPM文件 try: modules encode_code128(data) ppm_data modules_to_ppm(modules, **kwargs) with open(filename, w) as f: f.write(ppm_data) print(f条形码已生成并保存至: {filename}) print(f提示您可以使用图像查看器如IrfanView或在线转换工具将PPM转换为PNG/JPEG。) except Exception as e: print(f生成条形码时出错: {e}) # 使用示例 generate_code128_barcode(HELLO123, height80, bar_width3)4. 实现经典EAN-13编码器EAN-13的编码逻辑与Code 128不同。它固定13位数字编码分为左侧6位和右侧6位外加一位校验码。左侧数字的编码有“奇”和“偶”两种模式具体模式由第一位数字前置码决定。右侧数字统一采用“偶”编码。此外左右侧由中间的分隔符隔开。4.1 EAN-13编码表与结构EAN-13使用7个模块表示一个数字2个条、2个空共4个元素宽度组合固定。左侧奇偶编码表不同。# EAN-13 编码表 (数字 - 左侧奇/偶编码右侧编码) # 编码用7位二进制字符串表示1代表条黑0代表空白 EAN13_ENCODING { L_odd: { # 左侧奇编码 0: 0001101, 1: 0011001, 2: 0010011, 3: 0111101, 4: 0100011, 5: 0110001, 6: 0101111, 7: 0111011, 8: 0110111, 9: 0001011 }, L_even: { # 左侧偶编码 0: 0100111, 1: 0110011, 2: 0011011, 3: 0100001, 4: 0011101, 5: 0111001, 6: 0000101, 7: 0010001, 8: 0001001, 9: 0010111 }, R: { # 右侧编码 (全是偶编码的补码) 0: 1110010, 1: 1100110, 2: 1101100, 3: 1000010, 4: 1011100, 5: 1001110, 6: 1010000, 7: 1000100, 8: 1001000, 9: 1110100 } } # 前置码决定左侧6位数字的奇偶模式 PARITY_PATTERNS { 0: LLLLLL, 1: LLGLGG, 2: LLGGLG, 3: LLGGGL, 4: LGLLGG, 5: LGGLLG, 6: LGGGLL, 7: LGLGLG, 8: LGLGGL, 9: LGGLGL }4.2 EAN-13编码与生成函数编码过程如下验证输入确保是13位数字字符串。计算校验码如果输入12位则自动计算第13位EAN-13使用模10加权算法。确定左侧奇偶模式根据第一位数字前置码查找PARITY_PATTERNS。编码左侧数据根据奇偶模式从L_odd或L_even表中查找第2到第7位数字的编码。编码右侧数据所有右侧数字第8到第13位包括校验码都从R表中查找编码。添加起止符和分隔符起始符101中间分隔符01010终止符101。def calculate_ean13_checksum(first_12_digits): 计算EAN-13的前12位数字的校验码 total 0 for i, digit in enumerate(first_12_digits): d int(digit) # 奇数位从1开始计数乘3偶数位乘1 total d * (3 if (i % 2 0) else 1) checksum (10 - (total % 10)) % 10 return str(checksum) def encode_ean13(data): 将13位数字字符串编码为EAN-13的二进制模块序列字符串形式1为条 if len(data) 12: data data calculate_ean13_checksum(data) if len(data) ! 13 or not data.isdigit(): raise ValueError(EAN-13编码需要13位数字字符串或12位数字自动计算校验码) first_char data[0] left_data data[1:7] # 第2-7位 right_data data[7:] # 第8-13位含校验码 # 获取左侧奇偶模式 parity_key PARITY_PATTERNS.get(first_char) if not parity_key: raise ValueError(f无效的前置码: {first_char}) # 编码左侧 left_bits for i, digit in enumerate(left_data): parity parity_key[i] # L 或 G (G 对应 L_even) encoding_table EAN13_ENCODING[L_even] if parity G else EAN13_ENCODING[L_odd] left_bits encoding_table[digit] # 编码右侧 right_bits for digit in right_data: right_bits EAN13_ENCODING[R][digit] # 组合起始符(101) 左侧编码 分隔符(01010) 右侧编码 终止符(101) full_bitstring 101 left_bits 01010 right_bits 101 return full_bitstring def generate_ean13_barcode(data, filenameean13_barcode.ppm, height100, bar_width2, quiet_zone10): 生成EAN-13条形码并保存为PPM文件 try: bitstring encode_ean13(data) # 得到类似101010...101的字符串 # 将二进制字符串转换为模块宽度列表1-黑条0-白空 # 这里需要将连续的相同比特合并为宽度。EAN-13每个模块宽度是1。 # 为了复用 modules_to_ppm 函数我们生成一个宽度为1的序列。 module_sequence [] current_bit bitstring[0] count 1 for bit in bitstring[1:]: if bit current_bit: count 1 else: module_sequence.append(count) current_bit bit count 1 module_sequence.append(count) # 添加最后一段 ppm_data modules_to_ppm(module_sequence, heightheight, bar_widthbar_width, quiet_zonequiet_zone) with open(filename, w) as f: f.write(ppm_data) print(fEAN-13条形码已生成并保存至: {filename}) except Exception as e: print(f生成EAN-13条形码时出错: {e}) # 使用示例生成一个示例商品码中国690开头 generate_ean13_barcode(690123456789, bar_width3, height80) # 输入12位自动计算校验码 # 或直接输入13位 # generate_ean13_barcode(6901234567892, bar_width3, height80)5. 优化、调试与实战心得纯Python方案跑通了但在投入生产环境前还有一些重要的细节需要打磨。5.1 图像质量与可读性优化PPM格式虽然通用但文件体积庞大纯文本存储RGB值。在实际使用中我们通常需要PNG或JPEG格式。虽然我们不能依赖Pillow但可以借助Python标准库zlib和itertools来生成PNG格式。PNG格式本身并不复杂其核心是使用DEFLATE算法压缩的IDAT数据块。实现一个完整的PNG编码器超出了本文范围但我们可以提供一个思路将二值图像0和1按每像素1位的格式打包然后使用zlib.compress进行压缩最后按照PNG文件规范组装数据块IHDR, IDAT, IEND等。这是一个高级挑战但完全可行。另一个优化点是抗锯齿。在bar_width较大时条形码边缘是锯齿状的。纯软件实现抗锯齿如生成更高分辨率再缩放成本较高。一个更实用的建议是确保bar_width是整数最好为2或3。许多条码扫描设备对模块宽度的比例非常敏感非整数的缩放可能导致读取失败。在modules_to_ppm函数中我们通过将每个模块渲染为bar_width个相同颜色的像素来实现“放大”这保证了条空比例的绝对精确。5.2 常见问题排查与调试即使代码逻辑正确生成的条形码也可能无法被扫描器识别。以下是一个系统性的排查清单检查空白区这是最容易被忽略的错误。quiet_zone参数必须足够大。Code 128和EAN-13通常要求左右空白区至少是10倍模块宽度。我们的代码中quiet_zone参数的单位是模块在modules_to_ppm中会乘以bar_width转换为像素。确保最终图像两侧有足够的白色边缘。校验和错误这是第二常见的错误。务必反复核对校验和算法。对于Code 128确认“位置”权重计算是否正确起始符是位置1。对于EAN-13确认奇偶位权重乘3的规则。一个有效的调试方法是找几个已知的、能正确扫描的条码数据用你的代码生成然后对比生成的条空模式是否一致。可以在编码后打印出二进制序列或模块宽度序列进行比对。条空比例失真确保在渲染时每个“条”或“空”的宽度严格按照编码表给出的模块数乘以bar_width来绘制。任何四舍五入或整数除法截断都可能导致比例失调使扫描器无法解码。图像尺寸问题条形码高度不能太矮否则扫描激光线难以捕捉。通常高度至少是宽度的15%或更多。我们的height参数可以灵活调整。PPM文件查看生成的.ppm文件可以用支持该格式的查看器打开如GIMP, IrfanView。如果打不开首先检查文件头P3、宽度高度和最大值255是否格式正确像素数据是否只包含0-255的整数并以空格/换行分隔。5.3 性能考量与扩展思路纯Python实现的性能瓶颈主要在两个循环编码时的字符遍历和渲染时的像素填充。对于批量生成如一次生成数万个速度会明显慢于python-barcode。性能优化技巧使用局部变量在循环内部频繁访问的全局字典如CODE128_CHARSET可以先赋值给局部变量局部变量查找速度更快。使用列表推导和itertools在构建像素列表时可以考虑用itertools.chain和列表推导式替代显式的for循环和extend但要注意可读性。避免字符串拼接在生成PPM像素数据字符串时我们使用了 .join(map(str, all_pixels))这已经是比较高效的方式。避免在循环中使用拼接字符串。功能扩展思路支持更多码制基于同样的框架可以添加Code 39、ITF等码制。关键在于定义好它们的编码表和起止符规则。添加文本下方的人类可读字符在PPM数据中可以在条形码下方预留空白然后通过计算像素位置将原始数据字符串以点阵的形式“画”上去。这需要实现一个简单的位图字体渲染也是一个有趣的纯Python挑战。输出到其他矢量格式除了位图PPM还可以考虑输出为SVG可缩放矢量图形。SVG是纯文本的XML格式生成条形码的rect矩形元素非常简单而且无限缩放不失真非常适合打印场景。实现一个modules_to_svg函数会比生成PNG更容易。6. 封装与使用打造你的轻量级条码工具最后我们将上面的功能封装成一个易于使用的类或模块并提供命令行接口让它成为一个真正可用的工具。# barcode_pure.py import sys class PurePythonBarcode: 纯Python条形码生成器 def __init__(self): # ... 此处集成之前定义的编码表常量 ... pass def code128(self, data, **kwargs): return generate_code128_barcode(data, **kwargs) def ean13(self, data, **kwargs): return generate_ean13_barcode(data, **kwargs) # 命令行接口 if __name__ __main__: if len(sys.argv) 3: print(用法: python barcode_pure.py 类型 数据 [输出文件名]) print(类型: code128 或 ean13) print(示例: python barcode_pure.py code128 \TEST-123\ my_barcode.ppm) sys.exit(1) btype sys.argv[1].lower() data sys.argv[2] filename sys.argv[3] if len(sys.argv) 3 else f{btype}_barcode.ppm generator PurePythonBarcode() try: if btype code128: generator.code128(data, filenamefilename, height100, bar_width2) elif btype ean13: generator.ean13(data, filenamefilename, height100, bar_width2) else: print(f不支持的条形码类型: {btype}) except Exception as e: print(f错误: {e})现在你可以通过一行命令生成条形码了python barcode_pure.py code128 HelloWorld output.ppm。回顾整个实现过程从理解条码结构、实现编码逻辑、到渲染图像和优化调试我们完成了一个零依赖、可移植性极强的条形码生成方案。它可能不是性能最强的但在依赖管理严格或环境受限的场景下这份掌控感和简洁性是无价的。在实际项目中你可以根据需求在此基础之上进行裁剪或增强例如只保留你需要的码制或者将渲染部分替换为更高效的实现。希望这篇深入的探讨不仅能让你获得一个可用的工具更能让你理解那些日常扫描动作背后有趣的技术细节。