排期短码数据解析与冲突检测实践:从文本拆解到服务落地
在活动排期、演出现场、直播编单和会展运营中UNK20 Day1: VII Simon Patterson这类短码条目非常常见。一眼看过去它像一句文案落到系统里它其实是一份被压缩成字符串的排期记录。如果只是把它显示在网页上字符串就够了一旦需要做多舞台冲突检测、嘉宾重复校验、日历导出、改期通知就必须把这一段文本拆成结构化的字段。这套流程会围绕UNK20 Day1: VII Simon Patterson这条输入从字符串拆解、数据模型、解析器实现、时间与冲突校验到命令行验证和排错清单完整落地一个小工具。适合正在做活动后台、演出票务、直播排期或预约系统的开发者阅读。需要注意输入材料只给了这条短码没有给出具体日期、时区和舞台主数据所以下面的模型和代码是按常见排期管理场景设计的。落在自己项目里时字段命名、时区策略、主数据接入方式都要根据自己的业务调整。1. 先拆开这条短码它不是一个字符串是一条结构化记录1.1 从UNK20 Day1: VII Simon Patterson可以提取哪些字段把字符串按通用排期规则切开可以得到至少五类信息。UNK20是活动代号可以理解为某场活动或某个项目目录的缩写Day1是日期档位表示第几天不一定是绝对日期冒号左侧是元信息右侧是内容VII是舞台或场次编号写成罗马数字 7Simon Patterson是演出方或嘉宾名称。字段示例值字段含义解析建议event_codeUNK20活动或项目代号统一转大写避免大小写混用day_codeDay1第几天保留原始形式同时映射 day_datestage_codeVII舞台或轮次编号统一转大写保留原文用于展示stage_seq7归一化后的场次序号罗马数字转整数供排序和比较使用artists[Simon Patterson]演出方或嘉宾用逗号拆多人按主数据映射到艺人 ID这里的stage_seq很关键。VII在界面里可以继续展示为“VII”但排序、去重、冲突检测时如果直接用字符串会出现X排在V前面这类问题。所以数据结构里要同时保留展示字段stage_code和归一化字段stage_seq。1.2 不结构化的代价是什么如果只是在静态页面里展示这一行不结构化完全没有问题。但只要出现下面两种情况字符串方案就会出问题。第一种是多场次排序。VII是罗马数字如果按字典序排序X会排在V前面而不是按编号 10、5 排不同写法7、VII、07也会让前端排序不稳定。第二种是冲突检测。要判断两个演出是否在同一舞台同一时间重叠需要把VII解析成 7再和同一舞台编号的 slot 做时间交集。如果系统里存的还是原始字符串每次判断都要重新解析逻辑分散在各处迟早会漏。所以第一步不是写一堆 if else而是先把短码转成稳定的结构化条目。这会成为后面所有功能的公共基础。生产环境里的排期条目不会这么干净常见的变体还有全角冒号UNK20 Day1 VII Simon Patterson多个空格UNK20 Day1 : VII Simon Patterson艺人多人UNK20 Day1: VII Simon Patterson, Aria舞台名带中文UNK20 Day1: VII 主舞台 Simon Patterson这些变体都要求解析器在进入核心逻辑之前先做归一化。为了演示清晰下面以基础格式为准但实现里已经加入容错处理。2. 环境准备与数据模型先用最小依赖把基础打牢2.1 运行环境为了便于复现这里不引入第三方框架只用 Python 标准库。这样在一台刚装好 Python 的机器上就能跑通不需要先安装 Pandas、Django 或 FastAPI。需要 Python 3.10 或以上版本因为代码中要使用zoneinfo.ZoneInfo处理时区这是 Python 3.9 引入的标准库能力。如果项目还在 Python 3.8可以改用pytz但当前示例统一使用zoneinfo。先确认本机环境python --version预期输出类似Python 3.10.12下面是后续会用到的核心模块模块用途为什么需要re文本归一化和罗马数字校验排期文本存在全角、空格、非法字符等情况dataclasses定义领域对象比直接存字典更清晰字段命名固定zoneinfo时区转换避免使用固定偏移正确处理夏令时sqlite3保存解析结果演示完整的落库和幂等写入json输出结构化结果方便和前端、接口、CI 工具对接typing提供类型标注让函数签名更明确减少调用时误用学习环境下只跑解析和校验不需要 SQLite生产环境建议从一开始就落库因为排期数据要支撑后续的日历导出、票务库存和变更审计。2.2 定义解析结果的数据结构解析结果用dataclass保存比直接存字典更安全。字段命名固定类型清晰IDE 补全和后续重构都会方便很多。from dataclasses import dataclass from datetime import datetime from typing import List, Optional dataclass class ScheduleEntry: raw: str event_code: str day_code: str stage_code: str stage_seq: int artists: List[str] start: Optional[datetime] None end: Optional[datetime] None其中raw保存原始输入便于排查问题时不丢失来源。stage_seq是从罗马数字归一化后的整数。start和end是可选的因为原始标题没有时间只有后续编排时才会填入。2.3 SQLite 表结构设计如果要把解析后的排期数据落库建议拆成三张表活动日期表、舞台表、排期槽位表。不要把舞台编号和日期直接堆在排期表里否则改一个舞台名要更新很多行而且容易误改其他活动的数据。CREATE TABLE IF NOT EXISTS event_days ( id INTEGER PRIMARY KEY, event_code TEXT NOT NULL, day_code TEXT NOT NULL, day_date TEXT, timezone TEXT NOT NULL DEFAULT Asia/Shanghai, UNIQUE (event_code, day_code) ); CREATE TABLE IF NOT EXISTS stages ( id INTEGER PRIMARY KEY, event_code TEXT NOT NULL, stage_code TEXT NOT NULL, stage_name TEXT NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0, UNIQUE (event_code, stage_code) ); CREATE TABLE IF NOT EXISTS schedule_slots ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_code TEXT NOT NULL, day_code TEXT NOT NULL, day_date TEXT, stage_code TEXT NOT NULL, stage_seq INTEGER, artists TEXT NOT NULL, start_time TEXT, end_time TEXT, timezone TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, UNIQUE (event_code, day_code, stage_code, stage_seq, artists, start_time) );day_date是绝对日期需要由人工或排期系统从Day1映射出来。timezone单独存一列避免直接用08:00这种固定偏移。唯一键用于幂等重复写入同一场次同一位艺人同一个开始时间不应该重复插入。3. 解析器实现从一行文本到结构化对象3.1 先做文本归一化再走核心解析解析器第一步是归一化。这里处理三种最常见的脏数据全角冒号、全角空格、多个空格。import re def normalize(raw: str) - str: text raw.strip() text text.replace(, :) text text.replace(\u3000, ) text re.sub(r\s, , text) return text全角冒号在中文输入法下很容易出现在排期文本里全角空格 U3000 在一些从表格复制过来的文本中常见多个空格会导致split()结果不稳定先合并成单个空格。3.2 解析入口和罗马数字归一化解析入口按“先拆冒号再拆元信息”的顺序处理。先拆冒号是为了保护艺人名因为Simon Patterson中间有空格如果先从空格拆无法判断哪些段属于艺人。def parse_entry(raw: str) - ScheduleEntry: text normalize(raw) if : not in text: raise ValueError(missing separator : in %r % raw) meta, artists_part text.split(:, 1) meta_parts meta.split() if len(meta_parts) ! 3: raise ValueError(meta part must contain EVENT_CODE DAY_CODE STAGE_CODE) event_code meta_parts[0].upper() day_code meta_parts[1] stage_token meta_parts[2] artists [item.strip() for item in artists_part.split(,) if item.strip()] if not artists: raise ValueError(artists cannot be empty) stage_seq None if is_valid_roman(stage_token): stage_seq roman_to_int(stage_token) return ScheduleEntry( rawraw, event_codeevent_code, day_codeday_code, stage_codestage_token.upper(), stage_seqstage_seq, artistsartists, )为什么要做罗马数字校验因为VII是合法罗马数字但IIV不是。如果只写一个简单的字符相加IIV会被错误地解析成 5。这里先用正则严格校验再转换。def is_valid_roman(value: str) - bool: pattern r^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$ return re.match(pattern, value.strip(), re.IGNORECASE) is not None def roman_to_int(value: str) - int: mapping {I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000} total 0 prev 0 for char in value.upper(): if char not in mapping: raise ValueError(invalid roman numeral: %s % value) num mapping[char] total num if prev num: total - prev * 2 prev num return total以VII为例V 加 5I 加 1I 加 1结果为 7。以IV为例I 加 1V 加 5 时发现前一个 I 小于 V需要把之前多加的 1 扣掉两次最终得到 4。3.3 运行最小示例将上面的函数保存为parser.py后可以这样验证python -c from parser import parse_entry; print(parse_entry(UNK20 Day1: VII Simon Patterson))预期输出ScheduleEntry(rawUNK20 Day1: VII Simon Patterson, event_codeUNK20, day_codeDay1, stage_codeVII, stage_seq7, artists[Simon Patterson], startNone, endNone)到这里一条短码已经被拆成可以参与排序、比较和入库的结构化对象。后面所有校验逻辑都基于这个对象不再重复解析字符串。4. 时间和冲突校验真正让排期可靠的部分4.1 时区策略先统一到 UTC再参与比较排期系统最容易踩的坑是时区。Day1这类相对日期需要在外部配置里映射成绝对日期例如2025-06-21而开始和结束时间在写入时应当先带时区转成 UTC再参与重叠判断。from zoneinfo import ZoneInfo def to_utc(dt, timezone_name: str): if dt.tzinfo is None: tz ZoneInfo(timezone_name) dt dt.replace(tzinfotz) return dt.astimezone(ZoneInfo(UTC))为什么不用固定偏移08:00因为如果活动跨越夏令时变化固定偏移可能前后不统一IANA 时区如Asia/Shanghai、Europe/London会自动处理这类规则变化。中国目前不实行夏令时但这个习惯能让系统在接入海外活动时不重改。4.2 核心冲突检测排期冲突通常分两类。第一类是同一舞台时间重叠意味着同一个舞台同时安排了多个演出第二类是同一艺人在多个舞台时间重叠意味着艺人无法同时出现在两处。def overlaps(a_start, a_end, b_start, b_end) - bool: return a_start b_end and b_start a_end def detect_stage_conflicts(slots): conflicts [] for i in range(len(slots)): for j in range(i 1, len(slots)): a, b slots[i], slots[j] if a.stage_code b.stage_code and overlaps(a.start, a.end, b.start, b.end): conflicts.append((a, b, stage)) return conflicts def detect_artist_overlaps(slots): conflicts [] for i in range(len(slots)): for j in range(i 1, len(slots)): a, b slots[i], slots[j] if not overlaps(a.start, a.end, b.start, b.end): continue shared set(a.artists) set(b.artists) if shared: conflicts.append((a, b, sorted(shared))) return conflicts这里使用两层循环数据量小时完全够用。如果排期 slot 达到数万条可以改成先按stage_code分组或者按开始时间排序后只扫描相邻区间复杂度会从 O(n^2) 降到 O(n log n)。4.3 跨天和空字段处理如果演出跨天比如 23:00 到次日 01:00直接用小时比较会出错。解决办法是保存完整datetime跨天体现在日期上而不是只存时分。如果原始数据只给了23:00-01:00需要先结合day_date构造完整start_at和end_at再在结束时间上补一天。对于start或end为空的条目冲突检测应该明确跳过而不是默认取 0否则会产生大量假冲突。校验规则通过条件失败示例处理方式开始时间小于结束时间start endstart21:30end20:00抛出校验异常或自动纠正同一舞台不重叠同一 stage_code 下时间不相交20:00-21:00 与 20:30-21:30生成冲突报告同一艺人不重叠同一艺人只出现在一个未结束 slot同一艺人在两个舞台同时段生成艺人冲突报告跨天判断结束日期超过开始日期时视为合法23:00 到次日 01:00用完整 datetime 比较空时间字段start 和 end 都非空才参与重叠判断start 或 end 为空跳过并标记未排期5. 做成命令行工具并验证输出5.1 一个可复用的 CLI把解析器和冲突检测合到scheduler_tool.py提供两个子命令parse和check。这样既能在学习阶段快速验证也能在生产环境的 CI 流程里使用。python scheduler_tool.py parse UNK20 Day1: VII Simon Patterson python scheduler_tool.py check schedule.jsonschedule.json是待检测的排期数据示例[ { raw: UNK20 Day1: VII Simon Patterson, event_code: UNK20, day_code: Day1, day_date: 2025-06-21, stage_code: VII, stage_seq: 7, artists: [Simon Patterson], start: 2025-06-21T20:00:00, end: 2025-06-21T21:30:00, timezone: Asia/Shanghai } ]命令行实现可以这样组织import json import sys from datetime import datetime from zoneinfo import ZoneInfo def load_slots(path): with open(path, r, encodingutf-8) as f: items json.load(f) slots [] for item in items: tz item.get(timezone, Asia/Shanghai) tzinfo ZoneInfo(tz) slots.append(ScheduleEntry( rawitem.get(raw, ), event_codeitem[event_code], day_codeitem[day_code], stage_codeitem[stage_code], stage_seqitem.get(stage_seq), artistsitem[artists], startdatetime.fromisoformat(item[start]).replace(tzinfotzinfo), enddatetime.fromisoformat(item[end]).replace(tzinfotzinfo), )) return slots def main(): args sys.argv[1:] if not args: print(usage: python scheduler_tool.py parse TEXT | check FILE) return 1 action args[0] if action parse: entry parse_entry(args[1]) print(entry) elif action check: slots load_slots(args[1]) stage_conflicts detect_stage_conflicts(slots) artist_conflicts detect_artist_overlaps(slots) print(stage_conflicts:, len(stage_conflicts)) for a, b, _ in stage_conflicts: print(a.artists, vs, b.artists, on, a.stage_code) print(artist_conflicts:, len(artist_conflicts)) for a, b, shared in artist_conflicts: print(artist, shared, overlaps, a.artists, b.artists) else: print(unknown action:, action) return 1 return 0 if __name__ __main__: raise SystemExit(main())5.2 验证几个典型场景正常场景下运行check预期输出stage_conflicts: 0 artist_conflicts: 0人为构造一个冲突数据例如同一舞台有两条时间重叠的记录预期输出stage_conflicts: 1 [Simon Patterson] vs [Aria] on VII artist_conflicts: 0这说明工具能识别出舞台时间重叠而且输出里包含了艺人名和舞台编号方便定位。5.3 如何集成到项目里除了命令行还可以直接把函数嵌入业务代码。比如在排期修改接口里调用detect_stage_conflicts发现问题后由接口返回错误码或者做成一个独立校验服务在发布前调用。def validate_schedule(slots): conflicts [] conflicts.extend(detect_stage_conflicts(slots)) conflicts.extend(detect_artist_overlaps(slots)) if conflicts: raise ValueError(schedule has %d conflicts % len(conflicts))如果接入 CI可以写一条流水线任务每次排期配置变更后运行python scheduler_tool.py check schedule.json一旦返回非 0 状态码流水线失败发布被阻断。这样比人工检查可靠很多。6. 常见问题排查从现象一路查到根因6.1 解析不到艺人现象是parse_entry抛错artists cannot be empty或者打印出的artists列表为空。可能原因有两个输入文本冒号右侧是空的或者冒号本身是全角字符归一化没有覆盖到。检查方式text UNK20 Day1: VII Simon Patterson print(text.encode(unicode_escape)) print(len(text.split(:, 1)))处理建议是先调用normalize()再把文本转成repr看是否有不可见字符。预防办法是在解析入口强制归一化并且对输入保留原始文本用于审计。6.2 罗马数字识别失败现象是stage_seq为None或者roman_to_int抛错。VII正常但IIV这类非法组合会被正则拦截。如果项目里舞台编号确实存在IIV这种命名说明它不是罗马数字需要走另一套编号规则而不是试图修改罗马数字转换算法。检查方式print(is_valid_roman(VII)) print(is_valid_roman(IIV))处理建议是先用正则判断是否为合法罗马数字合法的转整数不合法的保留字符串并记录告警由运营人员确认编号规则。6.3 时区转换后时间错乱现象是两条明明不相邻的记录在检测里重叠或者本地 20:00 转 UTC 后变成了当天 12:00但另一条记录没有带时区。典型原因是部分数据没有tzinfo导致astimezone时使用了本机默认时区另一部分数据带的是08:00固定偏移在对比时出现偏差。检查方式dt datetime.fromisoformat(2025-06-21T20:00:00) print(dt.tzinfo) print(to_utc(dt, Asia/Shanghai))处理建议是所有写入数据统一先转 UTC数据库里也存 UTC 时间展示时再按用户时区转换。不要让业务逻辑去判断本地时区。6.4 冲突检测漏检现象是肉眼能看到两个时间重叠但程序报告冲突为 0。常见原因是start/end被存成字符串比较时变成了字典序或者是detect_stage_conflicts里比较的是a.stage_code但一个字段是VII另一个字段是07没有经过主数据归一化。检查方式print(type(a.start), type(b.start)) print(a.stage_code, b.stage_code, a.stage_code b.stage_code) print(overlaps(a.start, a.end, b.start, b.end))处理建议是比较前把所有timezone转换为同一时区所有stage_code统一走舞台主数据映射不要直接拿原始字符串比较。6.5 全角冒号或特殊空格导致解析失败现象是浏览器里看着格式正确但解析时报missing separator :。这通常是因为文本里包含的是全角或者从 Excel 复制出来后带了 U3000 全角空格。检查方式for ch in text: print(hex(ord(ch)), ch)处理建议是在normalize()里统一替换为:替换\u3000为空格再用re.sub(r\s, , text)合并多余空白。这个步骤必须在解析之前完成。7. 生产环境落地建议与扩展方向7.1 从演示脚本到生产系统至少补上这些能力演示脚本更关注让逻辑跑通生产系统还要考虑配置、权限、日志、幂等、回滚和监控。第一是配置外置。day_date映射、时区、舞台别名都不应该写死在代码里。建议放到数据库或配置中心启动时加载到内存变更时通过监听器刷新缓存。不要在高频路径里反复读取配置。第二是主数据收敛。stage_code和artists都应有主数据表。解析器只负责把原始文本转成候选字段最终是否合法要与主数据核对。第三是日志与审计。每条排期记录的创建、修改、冲突告警都要留下操作日志。至少记录操作人、操作时间、变更前后内容否则出了问题很难定位是谁改了什么。第四是幂等写入。重复执行解析任务
上一篇/下一篇内容由系统自动关联
返回资讯列表 →