从跑团Log到Replay:Python+正则表达式自动解析与视频生成完整方案

从跑团Log到Replay:Python+正则表达式自动解析与视频生成完整方案 跑团结束之后最让人头疼的往往不是剧情本身而是那一大堆散落在聊天窗口里的记录。角色说了什么、谁在什么时候投了骰子、这次检定到底过了没有全靠记忆力去还原效率很低。比如拿一篇名为《cult之神找了份包吃包住全年无休无薪的工作》的COC跑团replay来说剧情本身可以很轻松诙谐但要让读者完整看懂发生过什么记录整理就不能只靠复制粘贴。这篇文章不讨论剧本怎么写而是分享一套从原始Log到Replay成品的完整技术方案。我们会用一个可运行的Python脚本解析跑团记录自动完成掷骰、检定判定和Markdown导出还会介绍如何生成字幕文件并用FFmpeg合成视频。无论你是跑团主持人、replay视频作者还是想给朋友整理一份跑团记录都可以按这套思路落地。1. COC跑团与Replay到底是什么1.1 什么是TRPG和COCTRPG全称是Tabletop Role-Playing Game也就是桌上角色扮演游戏。玩家围在桌子旁通过语言描述来决定角色行动由主持人负责描述场景、扮演NPC并判定行动结果。COC是《克苏鲁的呼唤》Call of Cthulhu的缩写它是TRPG中比较有代表性的一套规则。与常见的奇幻主题不同COC更强调调查、神秘事件和精神状态管理。游戏过程中角色的理智值SAN值会随着事件发展而下降一旦SAN值归零角色就可能陷入疯狂。COC的核心判定方式是百分骰也就是D100。玩家行动时主持人会结合角色卡上的技能值让玩家投一次1到100的随机数。结果低于或等于技能值则成功结果越低判定等级越高。这个机制非常适合用程序去模拟和解析因为判定逻辑是确定的。1.2 什么是ReplayReplay原本指的是把跑团过程重新展示出来的记录作品。早期的Replay以文字为主把游戏中的对话、动作和掷骰结果按照时间顺序整理成一篇可读的文稿。后来随着视频制作工具普及越来越多的Replay采用视频形式配合立绘、表情、音效和字幕让观众像看动画一样了解跑团过程。文字Replay是技术门槛最低的形式。它不需要剪辑软件也不需要配音只要把对话和骰子结果整理清楚即可。但文字Replay也有自己的问题如果格式不统一读者很难区分“剧情描述”和“角色对话”更难看懂检定结果意味着什么。视频Replay的观看体验更好但制作成本明显更高。它需要把文字稿件转成字幕再与画面、音频合成。这个过程如果纯手工操作一期半小时的视频可能要花掉好几个晚上。把重复性的文字处理交给脚本是提升效率最直接的方式。1.3 为什么需要技术手段辅助跑团记录的核心问题有两个数据乱和结构散。数据乱指的是原始Log中混杂了多种信息包括主持人的描述、玩家的吐槽、骰子机器人的输出、角色对话和临时规则讨论。如果把这些内容全部平铺出来读者很难抓到重点。结构散指的是Replay需要有明确的节奏比如“场景开始 - 调查 - 检定 - 结果 - 下一场景”。原始Log没有这个结构所有消息都是按时间顺序排列的需要二次加工。技术手段可以解决两件事用正则表达式和脚本自动识别Log中的掷骰指令生成检定结果。用统一的模板把内容输出为Markdown或字幕文件减少排版重复劳动。这样人工只需要关注剧情润色和逻辑调整不用再逐行计算检定结果。2. 制作Replay的整体流程与工具准备2.1 从Log到Replay的五大阶段无论使用什么工具制作Replay的整体流程都可以拆成五个阶段。第一阶段是原始Log收集。跑团过程中使用骰子机器人或玩家手动记录把每一次检定、每一段关键对话保存下来。这个阶段最重要的是完整宁多勿缺。第二阶段是数据清洗。把Log中的无效信息删除比如群聊中的表情刷屏、重复消息、无关讨论。同时统一说话人名称和格式方便后续解析。第三阶段是检定解析。这是技术最核心的部分。脚本识别Log中的掷骰指令调用随机数生成器模拟D100根据COC规则返回成功等级并把结果替换回原文。第四阶段是格式输出。按Replay的展示需要输出为Markdown文档或SRT字幕文件。这个阶段决定了成品是适合在博客发布还是适合进入视频剪辑流程。第五阶段是发布或合成。文本Replay可以直接发布到博客或社区视频Replay则需要用剪辑工具或FFmpeg把字幕和素材合成。2.2 环境与工具说明本文示例以Python作为主要开发语言因为它的正则表达式库和文本处理能力都足够简洁。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。你只需要准备以下工具工具用途Python 3编写解析脚本文本编辑器编辑与调试代码FFmpeg将字幕合成到视频中Markdown编辑器预览最终Replay文稿FFmpeg并不是必须的如果只做文字Replay完全可以跳过视频合成的环节。本文会把它作为进阶方案介绍。2.3 自定义Log格式建议要让脚本能够准确识别掷骰指令最好在记录跑团时使用一套统一格式。这套格式不需要很复杂关键是稳定。推荐掷骰指令写法如下[投掷 侦查 40] [检定 心理学 50] [SAN 60]用英文方括号包围内部以空格分隔。这样正则表达式匹配起来非常方便不会和中文对话中的括号冲突。为什么用英文方括号而不是中文全角括号因为英文括号在正则表达式中更容易处理也避免中文输入法自动匹配成不完整字符的问题。如果你已经使用中文括号记录也不影响思路只要在脚本中将两种括号都纳入匹配范围即可。3. 用Python解析跑团Log3.1 设计可解析的Log格式在设计解析脚本之前先规定输入示例。下面是一段原始的跑团LogKP你们来到废弃的神殿门口贴着一张招聘启事。 [投掷 侦查 40] 调查员我怀疑这个cult之神只是想过安稳日子。 [心理学 50] KP神殿里没有陷阱但气氛有点诡异。这里可以看到几个关键点普通文本行直接保留并输出。[投掷 侦查 40]表示一次侦查检定技能值为40。[心理学 50]表示一次心理学检定技能值为50。实际项目中还可以增加更多指令比如[灵感 65]、[伤害 1d6]等。本文先以技能检定为例。3.2 掷骰指令解析与检定判定COC 7th规则中的成功等级通常分为四种结果小于等于技能值的五分之一为极难成功。结果小于等于技能值的二分之一为困难成功。结果小于等于技能值为成功。结果大于技能值为失败。如果结果是96到100且大于技能值则为大失败。不同版本的规则对大失败区间定义略有差异实际使用时需要根据你们使用的规则书确认。本文采用这套通用规则并在代码中预设了大失败判断条件。我们使用Python的random.randint(1, 100)来模拟D100投掷结果。这里需要注意random.randint包含首尾值正好对应百分骰的1到100。3.3 核心代码实现下面提供一个完整可运行的解析脚本。建议将文件保存为coc_parser.py。# -*- coding: utf-8 -*- COC跑团Log解析器 功能识别Log中的掷骰指令生成检定结果并输出Markdown格式Replay文稿。 import re import random from collections import Counter # 匹配 [投掷 侦查 40] 或 [检定 心理学 50] ROLL_PATTERN re.compile( r\[(?:投掷|检定)\s([\u4e00-\u9fa5A-Za-z0-9])\s(\d{1,3})\s*\] ) # 匹配 [SAN 60] SAN_PATTERN re.compile(r\[SAN\s(\d{1,3})\s*\]) def roll_d100(): 模拟投掷一个百分骰返回1到100之间的整数。 return random.randint(1, 100) def judge_coc(skill_value: int, result: int) - str: 根据COC 7th常见规则判断成功等级。 参数说明 skill_value角色技能值例如侦查40。 resultD100投掷结果。 if result skill_value // 5: return 极难成功 if result skill_value // 2: return 困难成功 if result skill_value: return 成功 if result 96 and result skill_value: return 大失败 return 失败 def parse_skill_roll(line: str): 返回匹配到的技能检定信息。 如果匹配失败返回None。 match ROLL_PATTERN.search(line) if not match: return None skill_name match.group(1) skill_value int(match.group(2)) result roll_d100() verdict judge_coc(skill_value, result) return { skill_name: skill_name, skill_value: skill_value, result: result, verdict: verdict, } def parse_san_check(line: str): 解析SAN值检定指令。 返回当前SAN值以及检定结果规则与普通技能一致。 match SAN_PATTERN.search(line) if not match: return None san_value int(match.group(1)) result roll_d100() verdict judge_coc(san_value, result) return { san_value: san_value, result: result, verdict: verdict, } def parse_log(lines) - tuple: 逐个处理Log行返回输出文本列表和统计信息。 output_lines [] stats Counter() for raw_line in lines: line raw_line.strip() if not line: continue skill_info parse_skill_roll(line) if skill_info: stats[skill_info[verdict]] 1 output_lines.append( f- {line}\n f 检定结果{skill_info[skill_name]} f{skill_info[result]}/{skill_info[skill_value]} f- **{skill_info[verdict]}** ) continue san_info parse_san_check(line) if san_info: stats[san_info[verdict]] 1 output_lines.append( f- {line}\n f SAN检定{san_info[result]}/{san_info[san_value]} f- **{san_info[verdict]}** ) continue output_lines.append(line) return output_lines, stats def main(): log [ KP你们来到废弃的神殿门口贴着一张招聘启事。, [投掷 侦查 40], 调查员我怀疑这个cult之神只是想过安稳日子。, [检定 心理学 50], [SAN 60], KP神殿里没有陷阱但气氛有点诡异。, ] output_lines, stats parse_log(log) print( Replay文稿 ) print(\n.join(output_lines)) print(\n 检定统计 ) for key, value in stats.items(): print(f{key}: {value}) if __name__ __main__: main()代码的核心思路很清晰ROLL_PATTERN负责匹配掷骰指令。judge_coc负责根据规则判断结果。parse_log负责逐行扫描Log如果遇到检定指令就触发解析否则保留原文。stats用Counter统计成功和失败的次数方便后续做内容回顾。这段代码中正则表达式中使用了\u4e00-\u9fa5来匹配中文字符所以技能名称可以是中文例如“侦查”“心理学”。如果你需要匹配英文技能名正则中已经加入了A-Za-z0-9可以直接兼容。3.4 运行与验证将脚本保存后在命令行中执行python coc_parser.py因为掷骰是随机数每次运行结果会不同。以下是某次运行的可能输出 Replay文稿 KP你们来到废弃的神殿门口贴着一张招聘启事。 - [投掷 侦查 40] 检定结果侦查 37/40 - **成功** 调查员我怀疑这个cult之神只是想过安稳日子。 - [检定 心理学 50] 检定结果心理学 72/50 - **失败** - [SAN 60] SAN检定33/60 - **成功** KP神殿里没有陷阱但气氛有点诡异。 检定统计 成功: 2 失败: 1从结果可以看出脚本已经自动完成了掷骰、判定和文案替换。生成后的文本可以直接复制到Markdown编辑器中进行二次润色。这里需要特别说明脚本中的随机数是模拟掷骰如果你希望跑团结果可复现可以在脚本中加入随机种子或把这些逻辑替换为读取骰子机器人已经产出的结果。实际使用中很多人会选择直接在聊天记录里让骰子机器人掷骰然后让脚本只负责解析和美化而不是自己生成随机数。两种方式各有优势自行决定即可。4. 从Replay文本到展示页面4.1 Markdown输出解析脚本输出的文本已经可以粘贴到CSDN或任何支持Markdown的平台上。为了让成品更像一篇正式Replay建议在脚本前后增加标题和分隔线。比如在main()函数中增加如下输出# 跑团记录cult之神找了份包吃包住全年无休无薪的工作 **出场角色**KP、调查员A --- ## 第一幕神殿招聘 KP你们来到废弃的神殿门口贴着一张招聘启事。这种方式不需要额外改代码只需要在脚本输出内容前拼接一段固定的Replay头信息即可。对于稳定更新的系列Replay建议把这部分做成一个单独的函数方便后续统一修改。4.2 带时间轴的SRT字幕生成如果想把Replay做成视频需要把文本内容转成字幕文件。SRT是最常用的字幕格式结构如下1 00:00:01,000 -- 00:00:04,000 KP你们来到废弃的神殿门口贴着一张招聘启事。 2 00:00:04,500 -- 00:00:08,000 调查员我怀疑这个cult之神只是想过安稳日子。生成字幕的Python代码并不复杂。只要给每条文本设定一个开始时间和持续时间就可以了。常见做法是对话长度决定持续时间或者统一设定每条3秒。下面是一个简单的SRT生成示例def generate_srt(lines, seconds_per_line3): srt_lines [] start_time 1.0 for idx, text in enumerate(lines, 1): start_ts seconds_to_srt_time(start_time) end_time start_time seconds_per_line end_ts seconds_to_srt_time(end_time) srt_lines.append(f{idx}\n{start_ts} -- {end_ts}\n{text}\n) start_time end_time 0.5 return \n.join(srt_lines) def seconds_to_srt_time(seconds): ms int((seconds - int(seconds)) * 1000) total_seconds int(seconds) hours total_seconds // 3600 minutes (total_seconds % 3600) // 60 secs total_seconds % 60 return f{hours:02}:{minutes:02}:{secs:02},{ms:03}这个示例的核心思路是把每条文本按固定时长切分时间轴实际项目中建议根据文本长度动态计算时长避免字幕停留过短或过长。4.3 视频合成思路FFmpeg有了SRT字幕文件后可以用FFmpeg将字幕合成到视频素材中。常见的合成命令如下ffmpeg -i input.mp4 -vf subtitlesreplay.srt output.mp4这条命令会把replay.srt渲染到input.mp4画面上生成新的output.mp4。需要注意FFmpeg的subtitles滤镜对字体依赖较高如果字幕中的中文显示为方块需要指定中文字体路径。例如ffmpeg -i input.mp4 -vf subtitlesreplay.srt:fontsdir/usr/share/fonts:force_styleFontNameNoto Sans CJK SC output.mp4关于中文字体的具体名称不同系统环境差异较大使用时先用fc-list查看系统可用中文字体再替换FontName参数即可。如果你使用的是剪映等图形化剪辑软件可以跳过FFmpeg直接把导出的SRT文件导入剪映字幕面板。这样能利用可视化界面做更精细的样式调整。5. 常见问题与排查思路5.1 常见解析问题在使用这套方案时最容易遇到下面几种问题问题现象常见原因解决思路正则匹配不到掷骰指令使用了中文括号或指令格式不统一统一采用英文方括号如[投掷 侦查 40]检定结果全部是成功或全部失败随机数生成范围写入错误检查random.randint(1, 100)是否写成了range或choice中文输出乱码文件编码不是UTF-8所有源文件和Log文件都使用UTF-8编码保存输出文本缺少换行Markdown要求空行才能换段需要分隔的文本之间保留空行大失败判定和预期不一致不同规则版本对大失败区间定义不同查看你们使用的规则书调整judge_coc中的边界值5.2 规则判定问题COC规则有一个比较容易混淆的地方极难成功、困难成功和成功之间是包含关系。如果一个D100结果是20技能值是60那么60/5 1220大于12所以不是极难成功。60/2 3020小于30所以是困难成功。20小于60所以同时满足成功条件。在judge_coc函数中我们按顺序先判断极难成功再判断困难成功最后判断成功这样逻辑是合理的。如果你把判断顺序反过来就会把困难成功误判成普通成功。另一个常见问题是早期COC规则中大失败通常只会在结果大于技能值且为96到100时触发。如果你的规则书以95为分界线可以在代码中直接调整数字。不要迷信网上的某一种写法一定要以你们实际使用的规则为准。5.3 运行脚本时的Python环境问题如果运行脚本时提示ModuleNotFoundError大概率是环境变量或Python版本问题。本文示例只用到了Python标准库不需要安装额外依赖。确保运行命令是python coc_parser.py而不是在其它语言环境中执行。如果你电脑上同时安装了Python 2和Python 3注意使用python3 coc_parser.py避免进入Python 2环境导致语法错误。6. 最佳实践与工程建议6.1 Log记录规范建议脚本解析只对格式统一的Log有效。为了让整个工作流更稳定建议跑团开始前先约定记录规范。首先所有掷骰指令统一用英文方括号包裹例如[投掷 侦查 40]。其次不建议把剧情描述和掷骰指令写在同一行否则解析后需要手动调整。最后角色说话时统一使用“角色名”前缀这样后续可以通过脚本自动统计每个角色的台词数量甚至生成角色时间线。如果使用骰子机器人输出结果脚本可以调整为直接匹配机器人的输出格式。思路是一致的只需要修改正则表达式和解析逻辑即可。6.2 数据处理与备份跑团记录是多人协作的产物很容易因为群聊记录被清理而丢失。建议每次跑团结束后立即将原始Log导出并保存到本地仓库。推荐使用Git管理跑团记录和脚本。每期Replay对应一个目录目录内包含replay-01/ ├── raw/ │ └── log-raw.md ├── script/ │ └── coc_parser.py ├── output/ │ └── replay.md └── assets/ └── images/这样做的好处是版本可控哪一期内容被误改都能找回。多人跑团时可以让专人负责Log整理避免每个人都维护一份不一致的记录。6.3 异常处理与可维护性当前示例中的代码比较简洁但在实际项目中需要增加异常处理。比如当技能值被误写成负数或超过100时脚本应该给出明确提示而不是直接计算出一个错误的结果。推荐在parse_skill_roll中增加范围校验def parse_skill_roll(line: str): match ROLL_PATTERN.search(line) if not match: return None skill_value int(match.group(2)) if not 0 skill_value 100: raise ValueError(f技能值必须在1到100之间当前值{skill_value}) # 其余逻辑保持不变另外脚本需要预留日志功能。解析大量Log时最好把每一条解析结果记录到日志文件方便在输出异常时回溯。6.4 安全边界与合规提示跑团记录中经常包含玩家的一些即兴发言发布前需要明确获得所有参与者的同意。不要直接把包含真实姓名、联系方式或隐私信息的聊天记录原样发布。如果Replay要公开发布建议使用玩家网名或角色名替代真实姓名。删除与剧情无关的私人聊天内容。避免将现实中的敏感事件影射到剧本中。对可能引起争议的内容做软化处理。这部分不是技术问题但非常重要。技术方案可以解决格式不能让工具替你解决所有内容判断。6.5 性能优化方向如果一次跑团的Log非常大比如几百上千行当前脚本的逐行处理方式已经足够快。如果未来要处理上万行Log可以考虑以下优化用re.compile提前编译正则表达式而不是在循环中调用re.match时隐式编译。将解析结果缓存到SQLite或JSON文件避免每次重新生成。使用multiprocessing并行处理多个文件。本文的示例脚本只适用于单文件处理若需要批量处理建议把main()函数改为接收文件列表的版本。7. 总结与下一步学习方向通过这篇文章我们从零完成了一套COC跑团Log解析工具实现了识别掷骰指令、模拟D100检定、输出Markdown文稿和SRT字幕的功能。核心逻辑并不复杂难点在于统一Log格式和保证判定规则正确。接下来你可以从以下几个方向继续深入扩展更多的掷骰指令例如[伤害 1d6]、[灵感 65]让工具覆盖更多跑团场景。把解析脚本封装成一个带Web界面的小工具让不熟悉命令行的人也能使用。结合语音合成和立绘素材尝试制作自动化程度更高的视频Replay。把跑团数据保存为结构化JSON为后续的角色成长分析、剧情分支统计做数据基础。对于实际项目我建议先小范围试用这套工作流整理一期Replay后再逐步完善格式。工具不需要一开始就做得很庞大能把常用环节自动化就已经能节省大量时间。跑团记录的最终目的不是追求工具的复杂度而是让故事被更完整地保存下来。希望这篇教程能帮你把下一段冒险整理成一份像样的Replay。