当前位置: 首页 > news >正文

从跑团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,如果遇到检定指令就触发解析,否则保留原文。
  • statsCounter统计成功和失败的次数,方便后续做内容回顾。

这段代码中,正则表达式中使用了\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)是否写成了rangechoice
中文输出乱码文件编码不是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。

http://www.cnnetsun.cn/news/4337893.html

相关文章:

  • Shieldprompt:零依赖的LLM安全测试,快速定位提示注入风险
  • STUSB4500 No Power故障排查:USB-C受电板无输出电压解决指南
  • 逆变器H桥维修:空载正常带载保护?先查高压三极管是否选错
  • C++校招笔试备战指南:从核心知识点到算法模板的全面拆解
  • STM32N6摄像头适配:DCMI采集与NPU输入实战解析
  • STM32WB上电不运行?从复位时序到选项字节的完整排查指南
  • ZTools:开源首字母搜索启动器,打造可扩展的本地工作流
  • 基于YOLOv5的车辆违停识别告警系统:Python毕设项目解析
  • 用拓扑数据分析挖掘量化因子:从持久同调到Python实战
  • STM32U5G9固件与Option Bytes打包合并为单个Hex的产线烧录实践
  • 基于STM32F446的双通道SiPM符合测量与峰值检测系统
  • STM32N6570-DK调试报错:Target is not responding排查与解决
  • STM32N6570 AI工程调试失败:PSRAM初始化顺序导致Target is not responding
  • VMware虚拟机创建、VMtools安装与系统镜像下载校验全攻略
  • DeepSeek API计费调整:从token成本结构到调用优化与报错排查
  • 工业视觉检测系统从需求分析到现场调试的完整实战指南
  • 基于CNN+LSTM的网络流量检测系统设计与实现
  • AI创业如何通过概念验证获得投资?从demo到验证的关键路径
  • STM32MP235启动失败排查:从硬件到软件完整指南
  • STM32H5嵌入式硬件故障排查:从VCC-GND短路到电化学迁移根因分析
  • 构建分析器刷新按钮第二次点击失效的排查与修复
  • Ubuntu下VScode STM32CubeIDE调试STM32看不到开发板?排查与解决
  • 锂离子电池一阶RC等效电路模型与Simulink热管理仿真分析
  • Python处理FT-ICR MS数据:从瞬态信号到分子式归属的完整流程
  • Android校招笔试:从Handler到性能优化,面试官到底在考什么?
  • 免费降ai网站能处理整篇论文吗?按免费额度、AI降重和查重结果选择?
  • Claude Code 成本控制:六个实用技巧减少 Token 消耗
  • SPC560P50L3 FlexPWM频率配置全解析:从时钟链路到实际调参
  • 映客算法笔试复盘:从KMP到卡尔曼滤波的硬核考点
  • 文献综述还在“人肉搬运”?毕夏AI正在把学术梳理变成“对话”