从跑团Log到Replay:Python+正则表达式自动解析与视频生成完整方案
跑团结束之后,最让人头疼的往往不是剧情本身,而是那一大堆散落在聊天窗口里的记录。角色说了什么、谁在什么时候投了骰子、这次检定到底过了没有,全靠记忆力去还原,效率很低。比如拿一篇名为《cult之神找了份包吃包住全年无休无薪的工作》的COC跑团replay来说,剧情本身可以很轻松诙谐,但要让读者完整看懂发生过什么,记录整理就不能只靠复制粘贴。
这篇文章不讨论剧本怎么写,而是分享一套从原始Log到Replay成品的完整技术方案。我们会用一个可运行的Python脚本解析跑团记录,自动完成掷骰、检定判定和Markdown导出,还会介绍如何生成字幕文件并用FFmpeg合成视频。无论你是跑团主持人、replay视频作者,还是想给朋友整理一份跑团记录,都可以按这套思路落地。
1. COC跑团与Replay:到底是什么
1.1 什么是TRPG和COC
TRPG全称是Tabletop Role-Playing Game,也就是桌上角色扮演游戏。玩家围在桌子旁,通过语言描述来决定角色行动,由主持人负责描述场景、扮演NPC并判定行动结果。
COC是《克苏鲁的呼唤》(Call of Cthulhu)的缩写,它是TRPG中比较有代表性的一套规则。与常见的奇幻主题不同,COC更强调调查、神秘事件和精神状态管理。游戏过程中,角色的理智值(SAN值)会随着事件发展而下降,一旦SAN值归零,角色就可能陷入疯狂。
COC的核心判定方式是百分骰,也就是D100。玩家行动时,主持人会结合角色卡上的技能值,让玩家投一次1到100的随机数。结果低于或等于技能值则成功,结果越低,判定等级越高。这个机制非常适合用程序去模拟和解析,因为判定逻辑是确定的。
1.2 什么是Replay
Replay原本指的是把跑团过程重新展示出来的记录作品。早期的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解析跑团Log
3.1 设计可解析的Log格式
在设计解析脚本之前,先规定输入示例。下面是一段原始的跑团Log:
KP:你们来到废弃的神殿,门口贴着一张招聘启事。 [投掷 侦查 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。 result:D100投掷结果。 """ 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_line=3): 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 subtitles=replay.srt output.mp4这条命令会把replay.srt渲染到input.mp4画面上,生成新的output.mp4。需要注意,FFmpeg的subtitles滤镜对字体依赖较高,如果字幕中的中文显示为方块,需要指定中文字体路径。例如:
ffmpeg -i input.mp4 -vf "subtitles=replay.srt:fontsdir=/usr/share/fonts:force_style='FontName=Noto 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 = 12,20大于12,所以不是极难成功。
- 60/2 = 30,20小于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。
