资讯动态

ArcGIS Pro二次开发:利用C#正则表达式高效提取属性表字符串成分

发布时间:2026/8/24 7:22:14 来源:尧图企业网站定制
1. 项目缘起一个看似简单却暗藏玄机的需求在ArcGIS Pro的二次开发工作中处理属性表数据是家常便饭。最近我遇到了一个来自业务部门的“小”需求他们有一张记录了城市地物信息的图层其中有一个名为“描述”的字段内容五花八门比如“XX路18号A座临时建筑”、“Sample-2023-001测试点”、“约120m²”。他们的需求是希望我能写个工具自动把这些混杂的字符串拆分开分别提取出其中的中文、英文、数字和特殊符号并存入不同的字段方便后续的分类统计和数据分析。乍一听这需求太简单了不就是字符串处理吗用C#的string方法截取、循环判断不就行了但当我真正开始动手面对上千条记录里各种意想不到的组合时才发现事情没那么简单。比如“120m²”里的上标“²”算数字还是符号“A-1栋”里的连字符该怎么处理全角括号和半角括号是否要区分如果只用基础的字符串方法代码会迅速膨胀成一团难以维护的if-else乱麻且效率低下。这时一个强大的工具浮现在脑海——正则表达式。它就像一把精密的手术刀能够用一套简洁的规则模式精准地“切割”出我们需要的任何字符片段。对于这个需求正则表达式几乎是唯一优雅且高效的解决方案。本文将基于ArcGIS Pro Add-in开发详细分享如何利用C#的正则表达式实现从复杂字符串中精准提取四类字符中文、英文、数字、特殊符号的完整流程、核心代码、避坑经验以及一个可直接复用的工具实现。2. 正则表达式理解这把“字符串手术刀”在深入代码之前我们必须先理解正则表达式的核心思想。它不是魔法而是一套用于描述字符串匹配模式的语法规则。你可以把它想象成一种超级通配符但比“*”和“?”要强大和精确无数倍。2.1 核心概念与语法速览对于我们的提取任务只需要掌握几个最核心的元字符和字符类字符类[a-zA-Z]匹配任何一个英文字母不区分大小写。[0-9]匹配任何一个数字。[一-龥]或[\u4e00-\u9fa5]匹配任何一个中文字符。这是Unicode编码范围涵盖了绝大多数常用汉字。[!#$%^*()\-_\[\]{}|;:,.?/\\~]这是一个示例匹配一系列特殊符号。注意像[、]、-这样的字符在正则表达式中有特殊含义需要用反斜杠进行转义。量词*匹配前面的子表达式零次或多次。匹配前面的子表达式一次或多次至少一个。?匹配前面的子表达式零次或一次。{n}匹配确定的 n 次。{n,}至少匹配 n 次。{n,m}匹配至少 n 次至多 m 次。分组与捕获使用圆括号()可以将多个字符组合成一个子表达式并且Regex.Matches方法可以捕获每个括号内匹配到的内容。匹配模式默认情况下正则表达式是贪婪匹配即尽可能多地匹配字符。例如用.*去匹配“abc123def”它会匹配整个字符串。在量词后面加上?就变成了懒惰匹配或非贪婪匹配即尽可能少地匹配字符。例如用.*?去匹配“abc123def”它可能只匹配到“a”取决于后续模式。2.2 设计我们的提取模式我们的目标是将一个字符串按字符类别“切分”。有两种思路思路A匹配目标分别用四个正则表达式去匹配我们想要的四类字符。思路B匹配分隔符用一个复杂的正则表达式匹配所有非目标字符即分隔符然后进行分割。对于提取任务思路A更直观、更灵活也更容易理解和调试。因此我们为每类字符定义模式中文[\u4e00-\u9fa5]。匹配一个或多个中文字符。英文[a-zA-Z]。匹配一个或多个英文字母。数字\d或[0-9]。匹配一个或多个数字。注意\d也匹配全角数字如“”如果不需要需明确使用[0-9]。特殊符号[^\u4e00-\u9fa5a-zA-Z0-9\s]。这是一个“取反”的思路。[^...]表示匹配不在括号内的任何字符。这里我们排除了中文、英文、数字和空白字符\s剩下的就是特殊符号。这个模式能捕获绝大多数标点、数学符号、单位符号等。注意关于“特殊符号”的定义是模糊的。空格、换行、制表符等空白字符\s通常不被视为有意义的“特殊符号”所以我们将其排除。如果你的业务场景需要包含空格可以移除\s。3. 在ArcGIS Pro Add-in中实现提取工具理解了原理我们开始动手编码。我们将创建一个按钮工具点击后遍历所选图层的要素处理指定字段并将结果写入四个新字段。3.1 开发环境与项目准备首先确保你已安装Visual Studio 2019 或更高版本。ArcGIS Pro SDK for .NET版本需与你的ArcGIS Pro匹配。在VS中创建新的“ArcGIS Pro Module”项目选择“ArcGIS Pro Add-in”模板。3.2 核心代码实现StringExtractorTool.cs我们将工具逻辑写在一个继承自MapTool的类中。以下是核心方法的详细拆解。using ArcGIS.Core.CIM; using ArcGIS.Core.Data; using ArcGIS.Core.Geometry; using ArcGIS.Desktop.Core; using ArcGIS.Desktop.Editing; using ArcGIS.Desktop.Framework; using ArcGIS.Desktop.Framework.Contracts; using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Mapping; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Windows; namespace YourAddinNamespace { internal class StringExtractorTool : MapTool { public StringExtractorTool() { // 设置工具为“单击”工具而非“拉框”工具 IsSketchTool false; SketchType SketchGeometryType.Point; SketchOutputMode SketchOutputMode.Map; } protected override Task OnToolActivateAsync(bool active) { return base.OnToolActivateAsync(active); } protected override async Taskbool OnSketchCompleteAsync(Geometry geometry) { // 1. 获取当前活动地图和图层这里假设只处理第一个选中的图层 var mapView MapView.Active; if (mapView null) return false; var selectedLayers mapView.GetSelectedLayers(); if (selectedLayers.Count 0) { MessageBox.Show(请先在地图内容窗格中选择一个要素图层。); return false; } var targetLayer selectedLayers.First() as FeatureLayer; if (targetLayer null) { MessageBox.Show(所选图层不是要素图层。); return false; } // 2. 弹出对话框让用户选择源字段和目标字段名 // 这里简化处理假设我们有一个自定义对话框 ExtractFieldDialog // 该对话框返回sourceFieldName, chineseFieldName, englishFieldName, numberFieldName, symbolFieldName var dialog new ExtractFieldDialog(targetLayer); if (dialog.ShowDialog() ! true) // 用户点击取消 { return false; } var (sourceFieldName, chineseFieldName, englishFieldName, numberFieldName, symbolFieldName) dialog.GetFieldNames(); // 3. 在异步任务中执行耗时的数据操作 return await QueuedTask.Run(() { try { // 打开要素图层的工作空间 using (var table targetLayer.GetTable()) using (var rowCursor table.Search(null, false)) { // 检查字段是否存在若不存在则添加 var fieldNames new Liststring { chineseFieldName, englishFieldName, numberFieldName, symbolFieldName }; var dataStore table.GetDatastore(); var schema table.GetDefinition(); var fields schema.GetFields(); foreach (var newFieldName in fieldNames) { if (fields.Any(f f.Name.Equals(newFieldName, StringComparison.OrdinalIgnoreCase))) { // 字段已存在可以跳过或提示用户 // 这里选择跳过你也可以选择先清空字段内容 continue; } // 添加新字段字符串类型长度可设大一些如255 var fieldDescription FieldDescription.CreateStringField(newFieldName, 255); table.AddField(fieldDescription); } // 重新获取字段定义确保新字段生效在同一个编辑操作内通常需要 // 更稳妥的做法是重新打开游标或使用EditOperation的Callback // 此处为简化我们假设字段已添加成功直接进入下一阶段的编辑操作 } // 使用编辑操作EditOperation来批量修改属性支持撤销/重做 var editOperation new EditOperation(); editOperation.Name 提取字符串成分; editOperation.SelectModifiedFeatures false; editOperation.SelectNewFeatures false; editOperation.Callback(context { // 在Callback内部重新获取表和游标确保上下文正确 using (var table targetLayer.GetTable()) using (var rowCursor table.Search(null, false)) { var sourceFieldIndex rowCursor.FindField(sourceFieldName); var chineseFieldIndex rowCursor.FindField(chineseFieldName); var englishFieldIndex rowCursor.FindField(englishFieldName); var numberFieldIndex rowCursor.FindField(numberFieldName); var symbolFieldIndex rowCursor.FindField(symbolFieldName); // 预编译正则表达式提升性能尤其在大数据量时 Regex regexChinese new Regex([\u4e00-\u9fa5], RegexOptions.Compiled); Regex regexEnglish new Regex([a-zA-Z], RegexOptions.Compiled); Regex regexNumber new Regex(\d, RegexOptions.Compiled); Regex regexSymbol new Regex([^\u4e00-\u9fa5a-zA-Z0-9\s], RegexOptions.Compiled); while (rowCursor.MoveNext()) { using (var row rowCursor.Current) { var sourceValue row[sourceFieldIndex] as string; if (string.IsNullOrEmpty(sourceValue)) { // 源字段为空则目标字段也置空 row[chineseFieldIndex] null; row[englishFieldIndex] null; row[numberFieldIndex] null; row[symbolFieldIndex] null; } else { // 提取中文连接所有匹配到的中文片段 var chineseMatches regexChinese.Matches(sourceValue); row[chineseFieldIndex] string.Join(, chineseMatches.CastMatch().Select(m m.Value)); // 提取英文连接所有匹配到的英文片段 var englishMatches regexEnglish.Matches(sourceValue); row[englishFieldIndex] string.Join(, englishMatches.CastMatch().Select(m m.Value)); // 提取数字连接所有匹配到的数字片段 var numberMatches regexNumber.Matches(sourceValue); row[numberFieldIndex] string.Join(, numberMatches.CastMatch().Select(m m.Value)); // 提取特殊符号连接所有匹配到的符号片段 var symbolMatches regexSymbol.Matches(sourceValue); row[symbolFieldIndex] string.Join(, symbolMatches.CastMatch().Select(m m.Value)); } // 保存修改 row.Store(); } } } }, targetLayer.GetTable()); // 传入表作为回调状态 // 执行编辑操作 bool editResult editOperation.Execute(); if (!editResult) { MessageBox.Show($操作执行失败: {editOperation.ErrorMessage}); return false; } MessageBox.Show(字符串成分提取完成); return true; } catch (Exception ex) { MessageBox.Show($处理过程中发生错误{ex.Message}); return false; } }); } } }3.3 关键代码段解析与避坑指南QueuedTask.Run的运用所有涉及访问地理数据库Geodatabase核心对象如Table、RowCursor的操作必须放在QueuedTask.Run或await QueuedTask.Run中执行。这是ArcGIS Pro SDK的强制要求因为GIS操作必须在MCT多线程公寓线程上运行否则会引发线程访问异常。EditOperation.Callback的妙用直接使用RowCursor修改数据并调用row.Store()是有效的但这样操作不会被ArcGIS Pro的编辑引擎记录因此无法撤销。使用EditOperation及其Callback方法可以将我们的修改逻辑包装成一个可撤销、可重做的原子操作这是专业Add-in开发的最佳实践。正则表达式的预编译RegexOptions.Compiled选项会将正则表达式编译为独立的程序集首次匹配时开销较大但后续匹配速度会显著提升。在对大量数据进行循环处理时使用此选项能带来明显的性能改善。如果只处理少量数据可以不使用。字段存在性检查与动态添加代码中演示了如何检查字段是否存在以及如何通过Table.AddField()动态添加字段。这是一个非常实用的功能让工具更具通用性。注意添加字段的操作本身也需要在QueuedTask中执行。空值处理务必检查源字段值是否为null或空字符串。如果直接对null调用Regex.Matches会引发异常。我们的处理方式是当源值为空时将所有目标字段也设为null。4. 处理边界情况与进阶优化上面的基础代码已经能解决80%的问题但在实际生产中总会遇到一些边界情况和特殊需求。4.1 字符类别的重叠与优先级我们的四个正则表达式是独立匹配的这意味着像“120m²”这样的字符串regexNumber会匹配到“120”。regexEnglish会匹配到“m”。regexSymbol会匹配到“²”。这符合我们的预期。但有时业务规则可能不同例如要求将“120m²”整体视为一个“带单位的数字”而不是拆开。这时你需要重新设计正则表达式或者定义更复杂的优先级逻辑。例如可以先匹配“数字单位符号”的组合模式。4.2 全角与半角字符我们的英文模式[a-zA-Z]只匹配半角英文字母。如果数据中包含全角字母如“”它不会被匹配到英文类而可能被归入特殊符号因为其Unicode编码不在a-zA-Z范围内。如果需要包含全角字母模式可以修改为[a-zA-Z--]。数字和符号也存在同样的问题。关键在于明确业务需求定义清晰的字符集范围。4.3 性能优化对于超大型数据集当处理数十万甚至上百万的要素时性能成为关键。使用QueryFilter如果不需要处理所有要素在获取游标时使用QueryFilter来限制范围。批量处理与进度反馈可以在EditOperation.Callback内部每处理1000或10000条记录后使用ArcGIS.Desktop.Framework.Dialogs.ProgressDialog向用户反馈进度避免界面“假死”。考虑使用Geoprocessing工具对于极其庞大的数据或者需要将逻辑分享给不熟悉Add-in的用户可以考虑将核心算法封装成一个地理处理GP工具。GP框架本身对大数据有更好的后台处理和并行优化潜力。你可以创建一个执行Python脚本或.NET后台处理的GP工具。4.4 扩展提取结果的进一步处理提取出的四类字符串可能还需要进一步清洗或分析。中文分词提取出的中文可能是一个连续的字符串如“北京市海淀区”。如果需要更细粒度的信息省、市、区可能需要集成中文分词库如Jieba.NET。数字格式化提取出的数字字符串“00120”可能需要转换为整数120或保留格式。符号分类特殊符号可能包含多种类型如标点。、数学符号-、货币符号¥$等。你可以用更细化的正则表达式对symbolField的值进行二次分类。5. 一个完整的工具对话框示例前面代码中提到的ExtractFieldDialog是一个WPF窗口让用户交互式地选择字段。其核心XAML和代码逻辑如下ExtractFieldDialog.xaml(简化版)Window x:ClassYourAddinNamespace.ExtractFieldDialog ... Grid StackPanel TextBlock Text选择源字段:/ ComboBox x:NamecboSourceField DisplayMemberPathName/ TextBlock Text中文结果字段名:/ TextBox x:NametxtChineseField TextChinese/ TextBlock Text英文结果字段名:/ TextBox x:NametxtEnglishField TextEnglish/ TextBlock Text数字结果字段名:/ TextBox x:NametxtNumberField TextNumber/ TextBlock Text符号结果字段名:/ TextBox x:NametxtSymbolField TextSymbol/ StackPanel OrientationHorizontal HorizontalAlignmentRight Button x:NamebtnOk Content确定 ClickBtnOk_Click IsDefaultTrue/ Button x:NamebtnCancel Content取消 ClickBtnCancel_Click IsCancelTrue/ /StackPanel /StackPanel /Grid /WindowExtractFieldDialog.xaml.csusing ArcGIS.Core.Data; using ArcGIS.Desktop.Mapping; using System.Collections.Generic; using System.Linq; using System.Windows; namespace YourAddinNamespace { public partial class ExtractFieldDialog : Window { private FeatureLayer _layer; public string SourceFieldName { get; private set; } public string ChineseFieldName { get; private set; } public string EnglishFieldName { get; private set; } public string NumberFieldName { get; private set; } public string SymbolFieldName { get; private set; } public ExtractFieldDialog(FeatureLayer layer) { InitializeComponent(); _layer layer; Loaded ExtractFieldDialog_Loaded; } private void ExtractFieldDialog_Loaded(object sender, RoutedEventArgs e) { // 在UI线程上异步获取字段列表 QueuedTask.Run(() { var fieldList new Liststring(); using (var table _layer.GetTable()) { var def table.GetDefinition(); foreach (var field in def.GetFields()) { if (field.FieldType FieldType.String) // 通常只处理字符串字段 { fieldList.Add(field.Name); } } } // 将字段列表传回UI线程更新ComboBox Application.Current.Dispatcher.Invoke(() { cboSourceField.ItemsSource fieldList; if (fieldList.Count 0) cboSourceField.SelectedIndex 0; }); }); } private void BtnOk_Click(object sender, RoutedEventArgs e) { if (cboSourceField.SelectedItem null) { MessageBox.Show(请选择源字段。); return; } SourceFieldName cboSourceField.SelectedItem as string; ChineseFieldName txtChineseField.Text.Trim(); EnglishFieldName txtEnglishField.Text.Trim(); NumberFieldName txtNumberField.Text.Trim(); SymbolFieldName txtSymbolField.Text.Trim(); // 简单验证字段名是否有效不能为空且不能与现有字段重名这里省略详细检查 if (string.IsNullOrEmpty(ChineseFieldName) || ...) { MessageBox.Show(结果字段名不能为空。); return; } this.DialogResult true; this.Close(); } private void BtnCancel_Click(object sender, RoutedEventArgs e) { this.DialogResult false; this.Close(); } public (string, string, string, string, string) GetFieldNames() { return (SourceFieldName, ChineseFieldName, EnglishFieldName, NumberFieldName, SymbolFieldName); } } }这个对话框提供了基本的交互你可以根据需要增加更多功能比如字段名查重、预览效果等。6. 实测案例与常见问题排查让我们用一个具体的图层来测试这个工具。假设有一个“设施点”图层其“备注”字段包含以下几条记录北京市海淀区中关村大街27号临时Sample-Point-2023-001约120m²造价~50万ABC Co., Ltd.(空值)运行工具后新字段将被填充如下源字段 (备注)中文字段英文字段数字字段符号字段北京市海淀区中关村大街27号临时北京市海淀区中关村大街号临时27Sample-Point-2023-001SamplePoint2023001-约120m²造价~50万约造价万m12050²~ABC Co., Ltd.ABCCoLtd..(空值)nullnullnullnull结果分析记录1中文提取正确数字“27”被提取括号被识别为符号。注意“大街27号”中的“号”是中文所以数字和中文被分开了。记录2连字符“-”被识别为符号英文和数字被成功分离。记录3这是一个混合案例。“m²”的上标“²”被识别为符号因为它不是[0-9]。“”和“~”也被识别为符号。这是符合我们定义的规则的。记录4句点“.”和逗号“”被识别为符号。注意“Co., Ltd.”中的空格被我们的符号正则排除了因为包含了\s。记录5空值被正确处理。常见问题与排查工具点击后无反应检查是否在内容列表Contents中选中了要素图层工具代码的第一部分会检查这个。检查是否在OnSketchCompleteAsync方法中使用了await QueuedTask.Run所有GIS对象操作必须在其中。检查Visual Studio的输出窗口是否有异常信息ArcGIS Pro Add-in的异常有时不会直接弹出。字段添加失败原因字段名已存在、字段名非法如以数字开头、包含特殊字符、或没有对该数据集的写权限。排查在table.AddField前后加入try-catch捕获GeodatabaseException并查看具体信息。在对话框中增加字段名合法性校验。提取结果不符合预期调试正则表达式这是最常见的问题。强烈建议在编写正则时使用在线的正则表达式测试工具如 regex101.com预先验证你的模式。将样本数据粘贴进去看匹配高亮是否正确。检查字符编码确保你的正则表达式模式字符串中的Unicode范围书写正确。\u4e00-\u9fa5是常见的中文范围但可能不包含所有罕见汉字或标点。理解贪婪匹配我们的模式用的是一次或多次它会匹配连续的同类型字符。例如“abc123def”中的英文会被匹配为“abcdef”一个整体而不是“abc”和“def”两个。这通常是我们想要的。如果你需要分开情况会复杂很多可能需要对字符串进行完全分词这超出了本文范围。处理速度慢优化确保使用了RegexOptions.Compiled。优化如果图层有空间索引或属性索引且你只处理部分数据务必使用QueryFilter。简化如果不需要撤销功能可以考虑直接在QueuedTask中使用RowCursor和RowBuffer进行批量更新而不通过EditOperation但这会牺牲用户体验。这个工具的开发过程让我再次体会到正则表达式在处理复杂文本时的强大与便捷。它初看复杂但一旦掌握核心语法就能以极简的代码解决许多繁琐的文本解析问题。在GIS数据处理中非空间属性信息的清洗、规整是提升数据质量的关键一步希望这个详细的实现过程能为你带来启发。在实际项目中你可能需要根据具体的数据特点微调正则表达式模式但整体的框架和思路是通用的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价