← All posts

2026-11-05

打印前用 Python 格式化、验证和转换标签数据

打印前用 Python 格式化、验证和转换标签数据
pdf labels barcodes label merge browser based graphical layout editor google sheets addin microsoft excel addon electronic shelf labels esl barcode3 labels label editor

在打印前用 Python 格式化、验证和转换标签数据

一张价格标签打印出 35.00 而不是 3.50,这不是设计问题,而是数据问题——而且正是那种能躲过每一次视觉审查的问题,因为标签看起来没问题。

本文讨论的是大多数标签工作流都会跳过的那一层:一个字段值从“电子表格里有值”到“打印机把它印到纸上”之间发生了什么。

差点发出错误价格标签的自由职业者

设想一位独立开发者——我们叫她 Mara——接了一个小型零售客户:一家店,四十个 SKU,一份店主自己维护的电子表格。Mara 做好标签模板,把电子表格接入为数据源,打印一张测试页,第一张标签就把一包 250 克的咖啡显示为 35.00。

电子表格里写的是 3,50。小数点用了逗号。标签设计期望的是点号,直到店主把校样退回来,才有人注意到。

这一列的其余内容也好不到哪去:

  • 价格有 3.5、3,50、€ 3.50 几种写法,还有几个空单元格。
  • SKU 被 Excel 悄悄转成了数字,吃掉了条形码需要的前导零。
  • 保质期日期在四十行里出现了三种格式。
  • 供应商代码带有 SUP- 前缀,店主希望去掉它,让标签更简洁。

Mara 的第一反应很标准:写一个导出脚本清理 CSV,再把清理后的文件喂给打印工具。这办法能用——直到店主下周修改了电子表格,而导出脚本还留在 Mara 的笔记本电脑上,没有文档,也没人记得要运行它。

所以真正的问题是:如何在字段值到达打印机之前清理和塑造它们,而不必搭建并维护第二条数据管道?

在 RSJ LPSNG 中,答案是 Python Field Script API:脚本块附加到各个标签字段上,在打印前访问并修改字段值。脚本随标签一起走,而不是留在标签旁边。

需要明确范围:这不是关于设计标签。如果你想要布局那一侧——在浏览器编辑器中放置文本、条形码和图像——请参阅如何在浏览器布局编辑器中设计条形码标签。本文讨论的是流入该布局的值。

为什么字段级数据控制对开发者和最终用户都很重要

大多数标签缺陷都是披着设计外衣的数据缺陷。错误的小数、未格式化的日期、条形码缺失的前导零、被截断而放不下的产品名称——布局完全是在按指令行事。错的是输入。

字段级控制对两类截然不同的受众都很重要,原因本质相同。

对开发者而言,它消除了一整类附属基础设施。你不需要编写和维护外部预处理脚本、定时重写 CSV 文件的作业,或位于数据源和打印引擎之间的胶水代码。格式化和验证存在于标签定义中,而标签定义正是真正需要它们的产物。

对最终用户和小团队而言,它能产生一致的输出,而不需要任何人理解源数据。店主继续按她一贯的方式填写电子表格。标签决定价格如何显示。

Python Field Script API 是这两个世界之间的中间层:

  • 它在字段上运行,而不是在文件上运行。
  • 它可以在打印前读取和修改值。
  • 它位于渲染标签的同一平台内,因此“清理器”和“模板”之间不存在版本漂移。

API 参考文档位于 lpsng.rsj.de/api.html。

实用手册:分三遍进行格式化、验证和转换

把每个字段脚本看作对值进行三遍处理,按此顺序:格式化它、验证它,然后从它派生。下面的代码片段是说明性的 Python——重要的是逻辑的形态,而不是确切的辅助函数名称。

第一遍:格式化

将原始值规范化为标签设计所期望的精确形式。价格转为固定小数模式,日期转为一种显示格式,文本转为布局设计所依据的大小写形式。

import re

CURRENCY = re.compile(r"[^\d,.\-]")

def format_price(raw):
    """'€ 3,50' -> '3.50'; returns None when nothing usable is left."""
    if raw is None:
        return None
    cleaned = CURRENCY.sub("", str(raw)).strip()
    if not cleaned:
        return None
    if "," in cleaned and "." in cleaned:
        cleaned = cleaned.replace(".", "").replace(",", ".")   # 1.234,56
    elif "," in cleaned:
        cleaned = cleaned.replace(",", ".")                    # 3,50
    return f"{float(cleaned):.2f}"
from datetime import datetime

DATE_FORMATS = ("%Y-%m-%d", "%d.%m.%Y", "%d/%m/%Y")

def format_date(raw, out="%d.%m.%Y"):
    text = str(raw or "").strip()
    for fmt in DATE_FORMATS:
        try:
            return datetime.strptime(text, fmt).strftime(out)
        except ValueError:
            continue
    return None   # unparseable beats guessed
def label_case(raw, style="upper"):
    text = " ".join(str(raw or "").split())   # collapse stray whitespace
    return text.upper() if style == "upper" else text.title()

注意那些刻意的 None 返回。无法格式化的值应该在下一遍中显式失败,而不是变成一个看似合理的 0.00。

第二遍:验证

验证是防止错误价格悄悄到达打印机的关键。检查你所依赖的字段是否存在且格式正确,并让每条消息都足够具体,以便定位记录。

def validate_record(record):
    """Return a list of 'field: reason' strings. Empty list means OK."""
    errors = []

    sku = (record.get("sku") or "").strip()
    if not sku:
        errors.append("sku: missing")
    elif len(sku) < 8:
        errors.append(f"sku: shorter than 8 characters ({sku!r})")

    price = record.get("price")
    if price is None:
        errors.append("price: not parseable")
    elif not (0.01 <= price <= 9999.99):
        errors.append(f"price: out of range ({price!r})")

    if record.get("best_before") is None:
        errors.append("best_before: unparseable date")

    return errors

像 price: not parseable ('0,00') 这样的消息会告诉操作员该看哪个字段,以及是哪个值导致的。像 invalid data 这样的消息只会让他们打电话给你。

price: not parseable ('0,00')
sku: shorter than 8 characters ('4711')
best_before: unparseable date (row: Espresso 250g)

第三遍:转换

只有在值被格式化并验证之后,才应该从中派生显示值——组合产品代码、去掉供应商前缀、为窄字段取短名称。

def product_code(record, prefix="SUP-"):
    supplier = (record.get("supplier_code") or "").strip()
    if supplier.startswith(prefix):
        supplier = supplier[len(prefix):]
    sku = (record.get("sku") or "").strip()
    return f"{supplier}-{sku}".strip("-")

脚本在工作流中的位置

在 RSJ LPSNG 中,你把脚本块附加到布局中的标签字段上。平台将其作为正常打印或渲染作业的一部分运行——同一次运行也处理 PDF 或 PNG 输出,以及通过 Web 服务接口直接打印。没有需要启动的附属进程,也没有需要推理的顺序问题。

这对交接很重要。格式化规则是标签定义的一部分,因此它随模板移动,而不是留在编写者的机器上。

困难方式与管理方式

困难方式是 Mara 差点选择的那条:一个单独的脚本,在 CSV 或 Excel 文件被喂给打印工具之前重写它们。它有自己的部署故事、自己的故障模式,以及自己那个安静的假设——每个人都记得在打印前运行它。

管理方式把 Python 脚本块放在 RSJ LPSNG 中的字段本身上。格式化和验证随标签一起走。同一个标签可以渲染为 PDF 或 PNG,或通过 Web 服务接口发送到直接打印,而字段逻辑在每种情况下都相同,因为它属于标签,而不是外部步骤。

第一个字段脚本的检查清单

  • 从一个字段和一条规则开始。 格式化价格,渲染一个示例 PDF,并检查它。然后再处理下一个字段。
  • 让失败可定位。 每条验证消息都应指明字段并显示有问题的值。
  • 用故意混乱的输入测试。 空单元格、多余空白、混合日期格式、负价格、0.00 的价格、长度错误的 SKU。
  • 决定是转换还是查找。 Excel 文件既可以作为数据文件,也可以作为查找文件,因此供应商名称可能属于参考表,而不是在脚本中派生。数据源一侧请参阅如何从 Excel 打印带条形码的标签。
  • 先渲染再运行。 三条混乱记录的 PDF 校样只需一分钟;重印四十张错误的货架标签要花一上午。

应避免的事项

不要把业务规则埋在布局里。 将格式化和验证保留在字段脚本中,将设计决策保留在布局编辑器中。混合它们意味着设计调整可能悄悄改变价格格式。

不要悄悄把坏数据强制转换成看似合理的值。 一张自信地打印错误价格的标签,比一个大声失败的任务更糟。返回 None,让验证来捕获它。

不要在多个字段中重复相同的转换。 如果六个字段需要相同的价格格式,一个共享方案胜过六个复制粘贴、逐渐分叉的块。同一标签上字段之间的不一致是最常见的错误来源之一。

不要猜测有歧义的输入。 03/04/2026 要么是 3 月 4 日,要么是 4 月 3 日。在源头固定格式;猜测的脚本对某些人来说一定是错的。

不要把脚本当作测试的替代品。 渲染示例标签并查看它们。字段脚本减少意外数量,但它们并不消除检查的需要。

常见问题

Python 脚本块在 RSJ LPSNG 中在哪里运行?

Python 脚本块附加到标签字段上,在打印前访问和修改字段值,作为平台中标签定义的一部分。

我需要构建单独的数据清理管道吗?

不需要。字段脚本在标签内部处理格式化、验证和转换,因此你不需要一个附属脚本来在数据文件到达打印引擎之前重写它们。

我还能以编程方式提交作业吗?

可以。Web 服务接口让你提交打印作业并将单个标签渲染为 PDF 或 PNG,而 OAuth2 模型涵盖了用单一注册 URL 连接外部系统。

相同的字段逻辑适用于 PDF、PNG 和直接打印吗?

PDF/PNG 渲染和直接打印是 Web 服务接口的文档化输出路径,而字段逻辑属于这些路径所使用的标签定义。电子货架标签是一条单独的轨道,有自己的供应商中立 JSON 接口(ESLSEND)——请参阅 ESL 接口文档,而不是假设打印路径原样适用。

非开发者可以使用包含字段脚本的标签吗?

可以。一旦脚本附加到字段上,打印标签就不需要编写或阅读 Python。运行作业的人选择数据并打印;格式化和验证作为标签的一部分发生。

这会取代布局编辑器吗?

不会。布局编辑器处理设计;Python 字段脚本处理流入每个字段的数据。它们是同一标签的互补部分。

结论

Mara 的修复不是更好的导出脚本,而是把规则移到值实际被使用的地方:一个价格字段,把小数逗号格式化为 3.50,拒绝打印空单元格,并在输出时去掉供应商前缀。店主保留她的电子表格。标签不再出错。

Next Generation Label Printing System 正是围绕这种分工构建的——在布局编辑器中设计,在字段脚本中控制数据,当你想要以编程方式驱动作业时使用 Web 服务接口。从一个字段、一条规则和一个渲染的 PDF 开始。然后交接出去。


相关文章

EU label: AI-generated content