Unity自动化工具开发:C#脚本字符串提取与本地化预处理

Unity自动化工具开发:C#脚本字符串提取与本地化预处理 1. 项目概述与核心价值最近在做一个本地化相关的功能需要把项目中所有脚本里写死的、需要翻译的文本内容都提取出来。手动去翻几百个C#脚本这活儿想想就头大效率低还容易漏。作为一个Unity开发者遇到这种重复性高、规则明确的“脏活累活”第一反应就是能不能写个编辑器工具把它自动化了这就是“导出项目中所有Script里面的文本内容”这个需求的由来。简单来说这个工具的目标是一键扫描整个Unity项目的Assets文件夹或者指定的文件夹找出所有.cs脚本文件然后通过正则表达式或语法分析识别出其中像字符串字面量比如玩家生命值、Debug.Log的输出、或者是标记了特定特性的字段例如[SerializeField] private string itemName;把这些文本内容提取出来并整理成一个结构化的文件比如CSV、Excel或者JSON方便后续的翻译、校对或者单纯做个内容审计。这玩意儿特别适合需要做游戏多语言支持的团队、有大量配置文本需要管理的项目或者只是想清理一下项目中那些“散落”在各处的字符串的开发者。即使你是个Unity新手跟着走一遍这个工具的开发过程也能对Unity编辑器扩展、C#文件处理、正则表达式有更深的了解以后遇到类似需求就能自己动手丰衣足食了。2. 整体设计与思路拆解2.1 需求分析与技术选型接到“导出脚本文本”这个需求首先要拆解它到底要干什么。核心任务很明确找文件 - 读内容 - 提文本 - 存结果。但每个环节都有不少细节要考虑。找文件Unity项目的脚本基本都在Assets目录下后缀是.cs。我们肯定要递归遍历所有子目录。这里直接用System.IO命名空间下的Directory和File类就行简单可靠。有人可能会想到用AssetDatabase但它更擅长处理Unity引擎识别的资源如Prefab、Material对于纯代码文件的遍历System.IO效率更高且不依赖编辑器状态。读内容就是读取.cs文件的文本。用File.ReadAllText一次性读入内存对于绝大多数项目都够用。如果遇到超大型的单文件虽然不推荐可能需要流式读取但这种情况极少。提文本这是最核心也最复杂的部分。怎么从C#代码里准确找出我们想要的“文本内容”我总结了三种常见场景和对应策略字符串字面量代码里用双引号包裹的部分如string name “英雄”。这可以用正则表达式来匹配模式大概是”([^”\]|\.)*”。但要注意转义字符比如他说”你好” 这种嵌套或带转义的。特定方法调用比如Debug.Log(“玩家升级了”)、UI控件的text属性赋值label.text “开始游戏”;。这些也需要用正则表达式去匹配方法名和参数。标记了特定属性的字段比如我们约定所有需要本地化的字符串都放在带有[Localized]特性的公共字段里。这种情况用正则就有点力不从心了最好能进行简单的语法分析识别出字段声明和特性。考虑到通用性和开发速度正则表达式是首选。它能覆盖前两种最常见的情况。对于第三种如果项目结构规范也可以设计特定的正则模式来匹配例如查找[.*Localized.*]后面的string字段。更高级的方案是使用像Roslyn这样的编译器服务进行完整的语法分析那精度最高但复杂度也剧增对于大多数日常需求有点“杀鸡用牛刀”。我们这个工具先以正则实现为主。存结果提取出来的文本不能乱糟糟的堆着。我们需要记录至少这些信息文本内容本身、所在的脚本文件路径、在文件中的行号方便定位、以及可能的上文信息比如它所属的变量名或方法名。输出格式选择CSV最通用用Excel、Numbers或者文本编辑器都能打开也方便导入数据库或翻译管理系统。JSON也行结构更清晰但文件体积可能稍大。2.2 工具架构与模块划分基于以上分析我们可以把工具分成几个清晰的模块这样代码好维护以后加功能也方便。文件遍历模块 (FileScanner)负责递归扫描指定目录下的所有.cs文件返回文件路径列表。这里可以加一些过滤条件比如忽略Plugins、Editor等第三方或工具目录。文本提取模块 (TextExtractor)核心模块。接收一个文件路径读取内容然后使用一系列“提取器”IExtractor接口来干活。每个提取器负责一种匹配规则如字符串字面量提取器、Debug.Log提取器。这种设计符合“开闭原则”以后要新增一种提取规则比如提取Invoke方法的字符串参数只需要新增一个实现了IExtractor接口的类就行不用动老代码。数据聚合与处理模块 (DataProcessor)把所有提取器找到的文本片段收集起来。这里要做去重同一句话在多个地方出现只记一次还是每次都记录根据需求定补充上下文信息比如这个文本是赋值给哪个变量的并整理成结构化的数据对象列表。输出模块 (Exporter)负责把处理好的数据列表写入到磁盘。支持多种格式比如CSVExporter、JSONExporter。同样用接口抽象方便扩展。编辑器界面模块 (EditorWindow)提供一个友好的Unity编辑器窗口让用户可以选择扫描目录、勾选需要的提取器、选择输出格式和路径然后一键执行。这是工具的门面用户体验的关键。整个工作流就是用户在EditorWindow点按钮 -FileScanner找文件 - 对每个文件TextExtractor调用所有激活的提取器干活 -DataProcessor汇总数据 -Exporter输出文件 - 完成提示。3. 核心细节解析与实操要点3.1 正则表达式文本提取的“灵魂”正则表达式用得好提取效率翻倍用得不好bug找得你崩溃。我们来深入看看几个关键模式。匹配普通字符串字面量最基本的模式是”([^”\]|\.)*”。这个模式的意思是找一个双引号”然后匹配任意多个“不是双引号也不是反斜杠的字符”或者“一个反斜杠后面跟任意字符用于处理转义”直到遇到下一个双引号。在C#中为了在字符串里表示一个双引号需要转义写成\所以模式里要能匹配\\” 这个序列。RegexOptions可以加上RegexOptions.Compiled预编译提升性能以及RegexOptions.Multiline让^和$匹配行首行尾方便记录行号。// 示例提取简单字符串字面量 string pattern ([^\\]|\\.)*; Regex stringLiteralRegex new Regex(pattern, RegexOptions.Compiled); MatchCollection matches stringLiteralRegex.Matches(fileContent); foreach (Match match in matches) { string extractedText match.Value.Trim(); // 去掉首尾的双引号 // 处理转义字符例如将\转换为 extractedText Regex.Unescape(extractedText); // 记录到结果中... }匹配特定方法调用如Debug.Log这个稍微复杂点因为方法参数可能跨行也可能包含嵌套方法调用。一个相对稳健但非百分百准确的方法是先匹配方法名和左括号然后贪婪地匹配到下一个成对的右括号。这需要处理括号嵌套可以用一个计数器来实现但代码会复杂。对于大多数单行、参数简单的调用可以用Debug\.Log\(”([^”)]*)”)。这个模式假设Debug.Log的参数就是一个简单的字符串字面量并且调用在一行内完成。对于更复杂的情况可能需要更精细的正则或者语法分析。注意正则表达式无法完美解析一门编程语言的所有语法。对于非常复杂的代码结构比如字符串插值$”Hello {name}”正则提取可能会出错或遗漏。我们的工具定位是“辅助工具”能提取出大部分显式字符串即可。如果项目对准确性要求极高需要考虑Roslyn方案。3.2 避免误匹配注释和字符串中的字符串代码里除了我们想要的字符串还有被注释掉的代码和字符串本身包含双引号的情况这些都会干扰我们的正则匹配。过滤注释一个务实的做法是在用正则匹配目标字符串之前先把源代码中的注释移除。我们可以写一个简单的状态机来移除单行注释(//)和多行注释(/* ... */)。但要注意字符串字面量里也可能出现//或/*它们不应该被移除。一个更简单但同样不完美的方法是在匹配到字符串字面量后检查它是否出现在注释范围内。可以通过匹配所有注释块并记录它们在文件中的起止索引然后判断我们找到的字符串的索引是否落在任何注释块内。如果是就丢弃它。处理字符串中的转义双引号这是我们正则表达式模式 (”([^”\]|\.)*”) 已经考虑到的部分。\\.这个分支就是为了匹配像\、\n、\t这样的转义序列。在提取出原始字符串后记得使用Regex.Unescape或System.Text.RegularExpressions.Regex.Unescape方法注意Regex.Unescape设计用于反转义正则表达式模式中的转义对于普通C#字符串转义直接使用System.Text.RegularExpressions.Regex.Unescape可能不完全准确更安全的做法是自己处理或使用简单的替换来将\恢复为”得到真正的文本内容。实际上对于C#字符串字面量更准确的做法是使用System.CodeDom.Compiler.CodeDomProvider相关的功能来解析但这更重。我们这里采用一个简化的处理提取后将\\” 替换为”。// 简化版的去除转义 string raw match.Value.Trim(); string clean raw.Replace(\\\, \).Replace(\\n, \n).Replace(\\t, \t); // 根据需要添加其他转义3.3 性能优化与大规模项目处理当项目有成千上万个脚本文件时性能就得考虑了。主要瓶颈在文件IO和正则匹配。异步与进度反馈文件遍历和读取是IO密集型操作可以使用System.Threading.Tasks来异步执行避免编辑器卡死。更重要的是要在编辑器窗口上显示进度条(EditorUtility.DisplayProgressBar)让用户知道工具还在运行而不是假死。并行处理每个文件的提取工作是独立的非常适合并行。可以用Parallel.ForEach来并发处理多个文件大幅提升速度。但要注意线程安全提取结果需要汇聚到一个线程安全的集合中比如ConcurrentBagT。缓存与增量导出如果项目脚本变动不大可以考虑缓存上次扫描的结果文件路径和修改时间。下次导出时只扫描修改过的文件然后合并结果。这需要设计一个持久化的缓存文件来记录元信息。正则表达式编译前面提到的RegexOptions.Compiled会将正则表达式编译为IL代码首次构建后有更好的运行时性能。对于要在成千上万个文件上反复使用的正则模式这个选项很有价值。// 示例使用Parallel.ForEach进行并行提取需注意UI线程更新 ConcurrentBagTextItem allItems new ConcurrentBagTextItem(); string[] allScriptPaths Directory.GetFiles(scanPath, *.cs, SearchOption.AllDirectories); Parallel.ForEach(allScriptPaths, (scriptPath) { string content File.ReadAllText(scriptPath); ListTextItem itemsInThisFile ExtractFromContent(content, scriptPath); foreach (var item in itemsInThisFile) { allItems.Add(item); } }); // 注意在Unity编辑器主线程中更新进度条需要特殊处理可以使用主线程调度4. 实操过程与核心环节实现4.1 搭建Unity编辑器窗口首先我们创建一个标准的Editor Window作为工具的入口。using UnityEditor; using UnityEngine; using System.Collections.Generic; // ... 其他所需命名空间 public class ScriptTextExporterWindow : EditorWindow { [MenuItem(Tools/导出脚本文本内容)] public static void ShowWindow() { var window GetWindowScriptTextExporterWindow(); window.titleContent new GUIContent(脚本文本导出器); window.Show(); } private string scanPath Assets; private Listbool extractorToggles new Listbool() { true, true, false }; // 示例几个提取器的开关 private string[] extractorNames { 字符串字面量, Debug.Log, 标记[Text]字段 }; private ExportFormat exportFormat ExportFormat.CSV; private string outputPath ExportedTexts.csv; private void OnGUI() { GUILayout.Label(扫描设置, EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); scanPath EditorGUILayout.TextField(扫描目录, scanPath); if (GUILayout.Button(选择..., GUILayout.Width(60))) { string newPath EditorUtility.OpenFolderPanel(选择扫描目录, scanPath, ); if (!string.IsNullOrEmpty(newPath)) { // 将绝对路径转换为相对于项目Assets的路径 if (newPath.StartsWith(Application.dataPath)) scanPath Assets newPath.Substring(Application.dataPath.Length); else scanPath newPath; } } EditorGUILayout.EndHorizontal(); GUILayout.Space(10); GUILayout.Label(提取规则, EditorStyles.boldLabel); for (int i 0; i extractorToggles.Count; i) { extractorToggles[i] EditorGUILayout.Toggle(extractorNames[i], extractorToggles[i]); } GUILayout.Space(10); GUILayout.Label(输出设置, EditorStyles.boldLabel); exportFormat (ExportFormat)EditorGUILayout.EnumPopup(输出格式, exportFormat); outputPath EditorGUILayout.TextField(输出路径, outputPath); GUILayout.Space(20); if (GUILayout.Button(开始导出, GUILayout.Height(30))) { ExecuteExport(); } } private void ExecuteExport() { // 这里调用核心的导出逻辑 // 1. 收集激活的提取器 // 2. 调用FileScanner // 3. 遍历文件用TextExtractor处理 // 4. 用DataProcessor聚合 // 5. 用Exporter输出 // 6. 显示完成对话框 EditorUtility.DisplayDialog(完成, $文本已导出至{outputPath}, 确定); } private enum ExportFormat { CSV, JSON, Excel } }这个窗口提供了基本的配置界面选择扫描文件夹、勾选需要的提取规则、选择输出格式和路径。4.2 实现核心提取器我们以最基础的“字符串字面量提取器”为例看看一个提取器如何实现。首先定义一个提取结果的数据结构public class TextItem { public string Text { get; set; } // 提取出的纯净文本 public string SourceFile { get; set; } // 源文件路径相对Assets public int LineNumber { get; set; } // 所在行号从1开始 public string Context { get; set; } // 上下文如变量名或方法名 public string ExtractorType { get; set; } // 是哪种提取器找到的 }然后定义提取器接口public interface ITextExtractor { string Name { get; } // 提取器名称 ListTextItem Extract(string fileContent, string filePath); // 核心提取方法 }最后实现字符串字面量提取器using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; public class StringLiteralExtractor : ITextExtractor { public string Name 字符串字面量; public ListTextItem Extract(string fileContent, string filePath) { ListTextItem results new ListTextItem(); // 移除所有注释避免匹配到注释里的字符串简化处理实际可能需要更精确的注释移除 string contentWithoutComments RemoveComments(fileContent); // 匹配字符串字面量的正则表达式简化版未完全处理所有转义和跨行 // 这个正则匹配双引号内的内容允许转义双引号(\)和转义字符(如\n, \t) string pattern ((?:[^\\]|\\.)*); Regex regex new Regex(pattern, RegexOptions.Compiled); // 为了计算行号我们将内容按行分割 string[] lines fileContent.Split(new[] { \r, \n }); int currentIndex 0; MatchCollection matches regex.Matches(contentWithoutComments); foreach (Match match in matches) { // 获取匹配到的字符串包含双引号 string matchedWithQuotes match.Value; // 获取组1的内容即双引号内的部分包含转义字符 string innerText match.Groups[1].Value; // 计算这个匹配在原始内容中的行号近似 // 注意由于移除了注释索引可能对不上这里用原始内容计算更准确 // 我们直接在原始fileContent中查找这个匹配的字符串效率较低但准确 // 更好的做法是在移除注释时记录索引映射这里为了简单使用原始内容查找。 int matchIndexInOriginal fileContent.IndexOf(matchedWithQuotes, currentIndex); if (matchIndexInOriginal -1) continue; // 没找到跳过可能由于注释移除导致 int lineNumber GetLineNumberFromIndex(fileContent, matchIndexInOriginal); currentIndex matchIndexInOriginal matchedWithQuotes.Length; // 更新搜索起始位置 // 处理转义字符得到最终文本 string finalText UnescapeString(innerText); // 添加上下文信息这里简单处理可以尝试获取前一个赋值或变量名 string context TryGetContext(fileContent, matchIndexInOriginal); results.Add(new TextItem { Text finalText, SourceFile filePath, LineNumber lineNumber, Context context, ExtractorType this.Name }); } return results; } private string RemoveComments(string code) { // 简易的注释移除处理单行注释和多行注释 // 注意这个简易方法可能会误删字符串内的注释符号生产环境需要更健壮的解析器。 string blockCommentPattern /\*(.*?)\*/; string lineCommentPattern //(.*?)\r?\n; string temp Regex.Replace(code, blockCommentPattern, , RegexOptions.Singleline); temp Regex.Replace(temp, lineCommentPattern, \n); return temp; } private int GetLineNumberFromIndex(string text, int index) { // 计算给定索引在文本中是第几行从1开始 if (index 0 || index text.Length) return 1; int lineNumber 1; for (int i 0; i index; i) { if (text[i] \n) lineNumber; } return lineNumber; } private string UnescapeString(string escaped) { // 简化版的转义处理 return escaped.Replace(\\\, \) .Replace(\\n, \n) .Replace(\\t, \t) .Replace(\\r, \r) .Replace(\\\\, \\); } private string TryGetContext(string fileContent, int matchIndex) { // 尝试获取上下文向前查找最近的变量名或属性名 // 这是一个启发式方法并不总是准确 int searchStart Math.Max(0, matchIndex - 50); // 向前搜索50个字符 string snippet fileContent.Substring(searchStart, matchIndex - searchStart); // 简单查找类似 “...”; 或 (“...”) 前面的标识符 // 这里只是一个示例实际实现会更复杂 Match varMatch Regex.Match(snippet, ([a-zA-Z_][a-zA-Z0-9_]*)\s*\s*$); if (varMatch.Success) return varMatch.Groups[1].Value; return N/A; } }这个提取器已经具备了基本功能匹配字符串、处理转义、计算行号、尝试获取上下文。RemoveComments函数是一个简化实现对于复杂的、包含字符串内注释符号的代码可能会出错。在要求高的场景下需要考虑使用更完善的注释移除算法或者直接基于原始文本匹配然后通过判断匹配是否在注释区间内来过滤。4.3 集成与执行流程在编辑器窗口的ExecuteExport方法中我们将所有模块串联起来private void ExecuteExport() { // 0. 验证输入 if (string.IsNullOrEmpty(scanPath) || !Directory.Exists(scanPath)) { EditorUtility.DisplayDialog(错误, 扫描路径无效或不存在, 确定); return; } // 1. 收集激活的提取器 ListITextExtractor activeExtractors new ListITextExtractor(); if (extractorToggles[0]) activeExtractors.Add(new StringLiteralExtractor()); if (extractorToggles[1]) activeExtractors.Add(new DebugLogExtractor()); // 假设已实现 // ... 添加其他提取器 if (activeExtractors.Count 0) { EditorUtility.DisplayDialog(警告, 请至少选择一种提取规则, 确定); return; } // 2. 扫描文件 Liststring scriptFiles; try { scriptFiles FileScanner.ScanForScriptFiles(scanPath); } catch (Exception e) { EditorUtility.DisplayDialog(扫描错误, $扫描文件时出错{e.Message}, 确定); return; } if (scriptFiles.Count 0) { EditorUtility.DisplayDialog(提示, 在指定路径下未找到.cs脚本文件。, 确定); return; } // 3. 遍历并提取带进度条 ListTextItem allItems new ListTextItem(); int totalFiles scriptFiles.Count; for (int i 0; i totalFiles; i) { string filePath scriptFiles[i]; // 更新进度条 if (EditorUtility.DisplayCancelableProgressBar(导出中, $正在处理: {Path.GetFileName(filePath)}, (float)i / totalFiles)) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog(取消, 导出操作已被用户取消。, 确定); return; } try { string content File.ReadAllText(filePath); foreach (var extractor in activeExtractors) { var items extractor.Extract(content, filePath); allItems.AddRange(items); } } catch (Exception e) { Debug.LogWarning($处理文件 {filePath} 时出错: {e.Message}); // 可以选择记录到日志继续处理其他文件 } } EditorUtility.ClearProgressBar(); // 4. 数据处理如去重、排序 ListTextItem processedItems DataProcessor.Process(allItems); // 5. 导出文件 IExporter exporter ExporterFactory.CreateExporter(exportFormat); // 工厂模式创建导出器 try { exporter.Export(processedItems, outputPath); EditorUtility.DisplayDialog(完成, $成功导出 {processedItems.Count} 条文本到\n{outputPath}, 确定); // 可选在资源管理器中选择生成的文件 EditorUtility.RevealInFinder(outputPath); } catch (Exception e) { EditorUtility.DisplayDialog(导出错误, $导出文件失败{e.Message}, 确定); } }FileScanner,DataProcessor,ExporterFactory和具体的CSVExporter等实现需要根据之前的设计去补充完整。例如CSVExporter可以用StringBuilder拼接CSV行或者使用像CsvHelper这样的库需导入来生成更规范的CSV文件。5. 常见问题与排查技巧实录在实际开发和使用的过程中你肯定会遇到一些坑。下面是我踩过之后总结出来的经验希望能帮你省点时间。5.1 提取结果不准确或遗漏这是最常见的问题症状可能是该提取的文本没提出来或者提了一堆根本不是UI文本的内容比如代码里的路径字符串。问题原因1正则表达式被注释干扰排查检查提取到的文本看看是不是有很多来自被注释掉的代码行。例如// string test “废弃的文本”里的字符串被提取出来了。解决加强注释过滤逻辑。上面提供的RemoveComments方法很基础。一个更健壮的方法是先不过滤注释在提取到所有字符串匹配后根据其索引位置判断是否落在任何一个注释块//或/* */内。你需要先写一个函数来找出源代码中所有注释的范围。问题原因2正则无法处理复杂语法排查字符串插值$”玩家{playerName}你好”可能只提取出空字符串””或者提取不全。跨行字符串虽然C#中字符串字面量不能直接跨行但可以用连接也可能被拆分成多个片段。解决对于字符串插值我们的简单正则无能为力。可以考虑两种策略一是放弃提取这种复杂字符串因为它们通常包含变量不适合直接本地化二是升级到使用Roslyn进行语法树分析它能准确识别字符串插值表达式并将其内容作为一个整体处理。对于用连接的字符串可以尝试在提取后根据行号和代码模式进行合并但这很复杂。一个务实的做法是在编写需要本地化的文本时避免使用插值和跨行连接或者使用StringBuilder然后我们的工具主要服务于这种“规范”的字符串。问题原因3转义字符处理不当排查提取出来的文本里\n还是显示为两个字符\和n而不是换行符。解决确保你的UnescapeString方法覆盖了所有常见的C#转义序列\\,\,\’,\0,\a,\b,\f,\n,\r,\t,\v以及Unicode转义\uXXXX和十六进制转义\xXX。可以使用Regex.Unescape但要注意它主要用于正则表达式字面量对于C#字符串最稳妥的是自己处理或寻找专门的库。5.2 工具运行缓慢或导致Unity无响应当项目脚本非常多时同步遍历和提取会阻塞主线程让编辑器“卡死”。问题原因所有工作都在主线程完成文件IO和正则匹配都是耗时操作。解决必须加进度条使用EditorUtility.DisplayProgressBar和EditorUtility.ClearProgressBar。这不会加速但能让用户知道程序在运行不是卡死。考虑异步/多线程将文件遍历和文本提取放到后台线程。但是Unity的编辑器API如AssetDatabase和UI更新必须在主线程。我们可以使用System.Threading.Tasks.Task.Run将耗时的计算部分丢到线程池然后在主线程用EditorApplication.delayCall或Dispatcher来更新进度条和最终完成提示。注意在后台线程中不要访问任何Unity对象或AssetDatabase。性能分析用System.Diagnostics.Stopwatch给各个阶段计时找到瓶颈。通常是文件IO或某个复杂的正则表达式。对于正则确保使用了Compiled选项。对于IO如果反复读取同一批文件考虑缓存。5.3 导出文件格式错乱或内容不对比如CSV文件用Excel打开时所有内容挤在一列或者包含逗号、换行符的文本破坏了CSV结构。问题原因CSV格式中如果字段值本身包含分隔符逗号或换行符必须用双引号将整个字段括起来。如果字段内还包含双引号则需要转义为两个双引号””。解决不要自己用string.Join(“,”, ...)简单拼接。应该使用标准的CSV写入方法。可以自己实现一个稳健的封装string EscapeCsvField(string field) { if (field null) return “”; // 如果字段包含逗号、换行、双引号则需要用双引号包裹 if (field.Contains(“,”) || field.Contains(“\””) || field.Contains(“\n”) || field.Contains(“\r”)) { // 将字段内的双引号替换为两个双引号 return “\”” field.Replace(“\””, “\”\””) “\””; } return field; }或者直接引入一个轻量级的CSV库比如CsvHelper它能妥善处理所有这些边界情况。编码问题确保写入文件时使用正确的编码如UTF-8 with BOM(new UTF8Encoding(true)) 或UTF-8以保证中文等非ASCII字符正常显示。5.4 如何扩展新的提取规则随着项目发展你可能会发现新的需要提取的文本模式比如提取LocalizationManager.GetText(“key”)中的key或者提取所有[Tooltip(“说明文字”)]特性里的文字。操作步骤新建一个类实现ITextExtractor接口。在Extract方法中编写针对新模式的正则表达式或解析逻辑。在编辑器窗口的extractorNames和extractorToggles列表中添加对应的名称和默认开关状态。在ExecuteExport方法中根据extractorToggles的状态实例化并添加这个新的提取器到activeExtractors列表中。这种设计使得功能扩展非常方便符合单一职责原则每个提取器只关心一种模式。5.5 实际使用心得与建议先小范围测试第一次使用时不要直接对整个Assets目录运行。先创建一个测试文件夹放几个有代表性的脚本文件包含各种字符串、注释、复杂语法跑一下工具检查输出结果是否符合预期。调整好正则和过滤逻辑后再全量扫描。输出信息要丰富除了文本内容源文件路径和行号至关重要。当文本需要修改或翻译时你能快速定位到代码中的位置。Context上下文字段也很有帮助比如知道这个文本是playerNameLabel.text的值能帮你理解它的用途。考虑去重策略同一个字符串可能在多个地方使用。导出时是每个出现的地方都导出一条还是只导出一条唯一的这取决于你的用途。如果是用于本地化生成key-value表可能需要去重。如果是用于代码审查查看所有使用处则需要保留所有条目。可以在DataProcessor.Process中提供选项。集成到工作流这个工具可以做成CI/CD流水线的一部分。例如每晚自动运行导出所有文本与翻译平台对接或者检测是否有新的未翻译文本加入。这时就需要做成命令行模式脱离编辑器窗口运行。正则表达式调试推荐使用在线的正则表达式测试工具如 regex101.com将你的C#代码片段贴进去反复调试你的正则模式确保它能准确匹配你想要的内容并且没有误匹配。把调试好的模式复制到代码里。