微型可变字符串分割器
使用格式掩码标记化和访问字符串内容
引言
解释字符串并提取信息一直以来都是必需的,并且主要与编写复杂代码和逻辑相关。
许多事情都可以通过分词器来处理,但这些分词器对于不同的字符串变体或因情况而异的分隔符来说,留下的余地很小。
正则表达式允许更复杂的处理,但创建、调试它们并在几个月后理解它们却很麻烦。
本文展示了一个小型类,它提供了一种“自然”的方式来处理复杂的提取模式,并使用一个非常小的类。
注意:本文及源代码已更新,以处理第一个版本存在的问题。
背景
演示其必要性的最好方法是提供两个简单的示例。
示例 1
我们需要处理来自日志文件的字符串,如下所示:
------------------snip------------------
Process: Tsk Mgr.EXE - Start Date: 2008-20-01 Duration: 00:01:54 - Description: Task Manager
Process: EXPLORER.EXE - Start Date: 2008-20-01 Duration: 00:01:54 - Description: Explorer
End-of-day
Process: Tsk Mgr.EXE - Start Date: 2008-21-01 Duration: 00:00:12 - Description: Task Manager
...
------------------snap------------------
关注的区域是斜体部分:进程名称、开始日期和持续时间。我们需要提取它们。
日志文件行看起来相似,第一个问题是有些行是我们不需要的,比如“End-of-day”或空行。
我们遇到的第二个问题是,我们无法真正处理标准分词器,因为我们没有分隔字符。空格不能使用,因为“Tsk Mgr.exe”的进程名称本身就包含空格,这会导致所有后续分词的部分错位。
冒号 (:) *可以* 使用,但是,我们需要从进程名称中删除“ - Start Date”,并且我们希望将“Duration”部分作为一个整体获取。
如果我们向人类解释如何提取这些值,我们会告诉他,从“Process:”之后到“Start Date:”之前的连字符“-”之间的部分,然后从“Start Date:”之后到“Duration:”之间的部分,再从“Duration:”之后到“Description:”之前的连字符“-”之间的部分。
以“掩码”的方式书写,字符串处理格式如下:
Process: #### - Start Date: #### Duration: #### - Description: Task Manager
我们需要在我们的应用程序中解析的是“####”部分。有什么比在我们的应用程序中输入完全相同的掩码模式更自然呢?为了进一步访问有趣的部分,我们应该能够给它们命名,用周围的百分号 (%) 表示。所以我们的掩码字符串看起来会是这样的:
Process: %proc% - Start Date: %date% Duration: %duration% - Description: %desc%
理想情况下,我们会将日志文件的每一行都通过这个掩码进行处理,如果成功,我们将查询变量“proc”、“date”和“duration”——忽略“desc”。如果失败,即输入字符串与掩码格式不匹配,我们将继续处理下一行日志。
示例 2
我们需要从类似以下的字符串中提取版本信息:
<softwareA> v1.4
<softwareB> v5
<softwareC> v1.3.1 Beta 5
<softwareD> v8.4.87.405 Alpha
这些字符串的唯一共同点是版本号前面的空格和小写字母“v”。同样,我们只对斜体部分感兴趣:纯版本号,不包含后缀的“Alpha”或“Beta 5”。
在这种情况下,我们将有两种不同的掩码:
一种带有版本号后的附加文本,另一种不带,所以第一个掩码看起来会是:
%software% v%version% %postfix%
我们只有两个固定元素:
- “ v”,以及
- 版本和后缀之间的空格
另一个没有后缀的掩码:
%software% v%version%
我们将先检查带有后缀的那个,如果失败(如果不存在后缀),则检查没有后缀的那个。
(仅检查第二个掩码,它将总是成功,并且会包含“Beta”和“Alpha”这些词——这是我们不想要的)。
以这种方式处理字符串的想法使得适应许多不同的任务变得更加容易,而无需重新编程通常涉及从字符串中提取数据时所需的任何额外的分词器后逻辑。
最初,一个非常好的 MP3 标签编辑器允许通过使用占位符变量来提取例如艺术家和曲目名称信息,这些信息来自 freedb.org 上各种格式的文件名,这很快吸引了我,因为它非常易于理解。我常常希望能够访问这样的功能,但从未找到过类似的东西,所以我决定自己动手编写。
使用代码
字符串分割器包含三个类:
主类 CStringSplitter
,它将被应用程序代码使用,以及两个辅助类 CSearchStringChar
和 CSearchStringStr
,它们由其他类用来解析掩码和字符串。
字符串分割器的用法在此小段代码中有所演示。
在示例中,我们使用百分号 '%' 来表示变量名的开始和结束。
#include "StringSplitter.h"
CStringSplitter Split( _T('%'), _T('%') );;
CString strValue,
strMask = _T("Process: %proc% - Start Date: %date% Duration: %duration% - Description: %desc%");
while ( !isEndOfFile() )
{
strLogLine = getNextLine();
if ( Split.matchMask( strLogLine, strMask ) )
{
if ( Split.getValue( strValue, _T("proc") ) )
{
// do something with strValue (proc)
...
}
if ( Split.getValue( strValue, _T("date") ) )
{
// do something with strValue (date)
...
}
if ( Split.getValue( strValue, _T("duration") ) )
{
// do something with strValue (duration)
...
}
}
}
当使用默认的开闭括号字符来表示占位符时,上面示例中的掩码将是:
Process: (proc) - Start Date: (date) Duration: (duration) - Description: (desc)
CStringSplitter 类
该类提供了以下功能来处理字符串。
配置掩码中变量部分的开始和结束字符。
CStringSplitter
类必须用两个可选参数来构造,以指定变量名的开始和结束字符。这些默认是开闭括号 '(' 和 ')'。
开始和结束字符可以相同(例如,如上面的代码示例中的 '%')。
将源字符串与掩码进行匹配。
matchMask
方法检查输入行与掩码的匹配情况。如果处理成功,则返回 true
,如果失败,则返回 false
,即掩码不适合输入字符串或掩码包含语法错误。
查询占位符的值。
成功处理源字符串后,可以调用 getValue
方法来获取请求变量的内容。它的第一个参数是一个字符串引用,如果变量存在,它将包含请求变量的值。如果变量存在,返回值是 true
,如果不存在,则返回 false
。
注意:变量名是不区分大小写的!这可以在 getValue
方法中使用 strcmp
而不是 stricmp
函数来更改。
使用相同的掩码处理多行。
当有数千行需要用相同的掩码处理时,只需要预先解析一次掩码。这使得字符串匹配更快。
第一步是通过调用 setMask
方法来设置掩码,并将掩码字符串作为唯一参数(这对应于 matchMask
的第二个参数)。如果掩码有效,它将返回 true
,如果无效,则返回 false
。
可以通过调用 matchLastMask
方法并将要匹配的字符串作为唯一参数(这对应于 matchMask
的第一个参数),在循环中进行字符串匹配。如果处理成功,它将返回 true
,如果失败,则返回 false
,即掩码不适合输入字符串。
成功后,可以如上所述检索占位符的值。
下面是早期示例中经过必要修改的部分(粗体显示):
CStringSplitter Split( _T('%'), _T('%') );; CString strValue, strMask = _T("Process: %proc% - Start Date: %date% Duration: %duration% - Description: %desc%"); Split.setMask( strMask ); while ( !isEndOfFile() ) { strLogLine = getNextLine(); if ( Split.matchLastMask( strLogLine ) ) { if ( Split.getValue( strValue, _T("proc") ) ) { // do something with strValue (proc) ... } ... } }
掩码的有效性和语法。
解析器对输入错误完全容忍,但是,要获得预期的结果,需要遵守语法规则。
1. 两个变量不能紧挨着。
至少需要一个固定字符将它们分开。
使用掩码“(part1)(part2)”解析任何字符串,从逻辑上讲,不会产生任何可用结果,因为 part1 和 part2 之间没有分隔。
2. 在固定文本中使用变量的开始字符。
给定以下输入字符串:
"Humidity %89"
当使用 '%' 作为变量的开始和结束字符时,在固定文本部分中出现的任何双重出现都将被解释为该字符的一次出现。
要处理固定文本中的 '%' 符号,掩码字符串必须是:
"Humidity %%%HUM%"
请注意,有三个(3)个连续的百分号。
粗体部分属于固定文本,任何双重开始字符都被解释为单个字符——“Humidity %”。
非粗体部分表示变量名,一个 '%' 作为开始字符,“HUM”作为变量名,下一个 '%' 作为结束字符。
为了避免不便,结束字符不允许出现在变量名中,即使是双重的。
3. 结束不当。
在字符串末尾未终止变量将自动终止它。
4. 掩码验证(例如,用户输入的验证)。
要在用户输入字段中使用此类,检查掩码有效性的最简单方法是简单地调用 setMask
并传入字符串。如果它返回 true
,则可以使用该掩码(但是,不能保证结果是用户想要的——Microsoft 的 PSI-API 尚未准备好公开使用 ;-)
其他平台
该代码使用了一些 Visual Studio 2005 的特定功能,但应该可以轻松地移植到其他平台,而无需任何努力。
第一个是 MFC 的 CString
,用于内部存储变量和占位符以及返回变量内容。它可以被几乎任何其他字符串类替换,因为除了简单的赋值之外,没有使用其他功能。
字符串处理本身使用旧而经典的 C 库函数 strlen
、strcpy
、strchr
、strstr
和 stricmp
,即它们各自的 Visual Studio 的 _t
前缀版本,以通过相同的代码实现 MBCS / UNICODE 兼容性。
VS 2005 的 strcpy_s
函数可以用其他编译器对应的“不安全”的 strcpy
函数替换,而不会出现问题,因为在复制之前内存是直接分配的。
由于使用的是基本的字符处理函数而不是高级字符串类例程,因此掩码解析速度应该相当快,因为这些库在大多数编译器库中都经过了汇编优化。
历史
1.0 2008-01-21 公开发布
1.1 2008 -01-25 修复了 1.0 版本中存在的问题
- 变量开闭占位符
- 将固定文本中的双重开括号字符解释为单个字符
- 掩码语法错误时的容错处理