使用 Regex++ 将正则表达式添加到您的应用程序






4.67/5 (17投票s)
一篇教程,演示如何使用来自 boost.org 的 Regex++ 为您的项目添加正则表达式。
引言
什么是正则表达式?简而言之,正则表达式提供了一种将原始数据转换为可用数据的简单方法。在《精通正则表达式》(O'Reilly & Associates)的序言中,Jeffrey Friedl 写道:
“正则表达式在如此多样的应用程序中出现是有充分理由的:它们极其强大。从低层次来看,正则表达式描述了一段文本。您可以使用它来验证用户的输入,或者筛选大量数据。从更高层次来看,正则表达式允许您掌控数据。控制它。让它为您工作。精通正则表达式就是精通您的数据。”
您可能不知道,Microsoft Visual Studio 文本搜索工具中就包含正则表达式。它提供了一种非常强大的方法来搜索代码(或任何文本文件)中复杂的模式。如果您以前从未使用过正则表达式,这里有一些关于它的网络链接,可以帮助您入门。
- 正则表达式简介 - Uwe Keim on CP
- http://www.robotwisdom.com/net/regexres.html - 各种链接
- http://etext.lib.virginia.edu/helpsheets/regex.html - 简短教程
- 从 Bookpool 购买 Jeffrey E. F. Friedl 的《精通正则表达式》
- O'Reilly 网站上《精通正则表达式》的示例章节
入门
正则表达式虽然看起来难以学习,但却是程序员工具箱中最强大的工具之一,然而许多程序员却从未利用它们。您当然可以编写自己的文本解析器来完成工作,但那样做需要更多时间,更容易出错,而且远没有那么有趣(恕我直言)。
Regex++ 是一个可从 https://boost.ac.cn 获取的正则表达式库。Boost 提供免费的经过同行评审的可移植 C++ 源代码库。访问该网站了解更多信息。出于我们的目的,我们只关注 Regex++,但您可能会发现他们的许多库都很有用。Regex++ 最初作者的网站是 http://ourworld.compuserve.com/homepages/John_Maddock/
安装 Regex++
注意: 以下说明仅在您安装了 Visual Studio 6 或 7 时有效。
要安装 Regex++,请完成以下步骤(详细说明也可在 Regex++ 下载本身中找到)
- 从原始作者网站下载 Regex++。这样您将只获得 regex 库,而不是整个 boost 库。
- 解压到目录 C:\Regex++(在“解压到:”字段中键入路径 C:\Regex++,如下图所示)
- 打开命令提示符
- 将目录更改为 C:\Regex++\libs\regex\build。在此目录中,您将找到几个 make 文件。您感兴趣的是 vc6.mak。
- 为了使用 Visual Studio 的环境设置,您必须运行批处理文件 vcvars32.bat。它应该在您的路径中,所以您不需要指定它的完整路径。只需在命令提示符窗口中键入 vcvars32.bat。
- 类型
nmake -fvc6.mak
构建需要一点时间。 - 类型
nmake -fvc6.mak install
(将库和 DLL 安装到适当的位置) - 类型
nmake -fvc6.mak clean
(这个可能会出现一些错误。我遇到了,但如果需要,您可以手动删除中间文件)
现在您的库已构建并就位,可以使用了。我上面包含的项目旨在演示如何简单地解析 HTML。您现在需要做的就是打开项目并确保项目设置指向适当的 regex++ 库和包含目录。但首先进行简短讨论
注意: 要将 Regex++ 库添加到您的项目,请选择 Project | Settings...。在随后的对话框中,选择 C/C++ 选项卡。在 Category 下拉列表中,选择 Preprocessor。在 Additional include directories: 编辑框中输入 C:\Regex++。现在选择 Link 选项卡。在 Category 下拉列表中,选择 Input。在 Additional library path: 编辑框中输入 C:\Regex++。
解析 HTML
HTML 解析器并非新事物。既然轮子已经发明了,就没有理由(至少我能想到)有人需要编写自己的解析器了。话虽如此,我们将要使用的示例正是这样做的——解析 HTML。我这样做是因为解析 HTML 提供了一个很好的教学示例。具体来说,它解析 HTML 文档中的表单元素。这是一项相当复杂的任务,但是,使用正则表达式可以使其变得简单。我们希望我们的解析器足够通用,能够解析任何给定输入字段中键值对。例如,在 HTML 中
<input type="text" name="address" size=30 maxlength = "100">
我们希望只提供键名(例如 type、name、size 等),并让正则表达式返回该键对应的L值(例如 text、address、30 等)。请注意,有些值带引号,有些不带。有些使用空白字符,有些不使用。这些都是我们必须在正则表达式中考虑的因素。我们还必须考虑每个参数的不同顺序。例如,这<input type="text" name="address" size=30 maxlength = "100">
与此相同<input name="address" type="text" maxlength="100" size="30">
在示例应用程序中,我从 HTML 输入文件构建了一个单一字符串(我们将整个文件读入 CString 变量)。虽然这可能会在非常大的文件上引起问题,但出于我们的目的,我们假设文件相当小。我们需要整个字符串才能跨行匹配——但稍后会详细介绍。
ParseFile 方法
在 ParseFile 方法中,我们
- 传入要解析的 HTML 文件的文件名(必须包含
- 将整个文件读入字符串
- 创建一个正则表达式对象 (RegEx)
- 对文件字符串调用 Grep 以查找我们想要的模式,并将找到的匹配项放入 STL 向量中
- 遍历放入向量中的每个项目
- 调用 GetActualType(),它创建另一个正则表达式以获取我们找到的类型(例如 INPUT、TEXTAREA、SELECT)
- 调用 GetValue() 并传入键(例如 type、name 等)
- 生成并打印出包含我们获取的值的字符串
注意: 本文中的代码片段包含使用转义字符的正则表达式。因为这些是 C/C++ 字符串,所以这些转义字符必须转义两次。也就是说,正则表达式空白转义字符 (\s) 实际上会是这样:\\s
。而引号会是这样:\\\"
-- 第一个转义反斜杠,第二个转义引号。
BOOL CRegexTestDlg::ParseFile(CString filename) { if (filename.IsEmpty()) { return FALSE; } CString finalstring; this->m_mainEdit.SetWindowText(""); CStdioFile htmlfile; CString inString = ""; CString wholeFileString = ""; std::string wholeFileStr = ""; // Read entire file into a string. try{ if (htmlfile.Open(filename, CFile::modeRead | CFile::typeText, NULL)) { while (htmlfile.ReadString(inString)) { wholeFileString += inString; } htmlfile.Close(); } } catch (CFileException e) { MessageBox("The file " + filename + " could not be opened for reading", "File Open Failed", MB_ICONHAND|MB_ICONSTOP|MB_ICONERROR ); return FALSE; } // Need to convert string to a STL string for use in RegEx wholeFileStr = wholeFileString.GetBuffer(10); // Create our regular expression object // TRUE means that we want a match to be case-insensitive boost::RegEx expr("(<\s*(textarea|input|select)\\s*[^>]+>[^<>]*(</(select|textarea)>)?)", TRUE); // Create a vector to hold all matches std::vector<std::string> v; // Pass the vector and the STL string that hold the file contents // to the RegEx.Grep method. expr.Grep(v, wholeFileStr); // Create char array to hold actual type (e.g. input, select, textarea). char actualType[100]; // vector v is now full of all matches. We iterate through them. for(int i = 0; i < v.size(); i++) { // Get the object at the current index and typecast to string std::string line = (std::string)v[i]; // Get a pointer to the beginning of the character arrray const char *buf = line.c_str(); // Create some temporary storage variables char name[100]; char typeName[100]; // Build output string. finalstring += "input, textarea, select?: "; GetActualType(buf, actualType); finalstring += actualType; finalstring += " -- "; GetValue("name", buf, name); finalstring += "name: "; finalstring += name; finalstring += " -- "; finalstring += "input type is: "; // If it's an input, get the type of input // (e.g. text, password, checkbox, radio, etc.) if(_stricmp("input", actualType) == 0) { GetValue("type", buf, typeName); finalstring += typeName; } // Otherwise, it doesn't apply. else { finalstring += "N/A"; } finalstring += "\r\n"; } // Populate text field with items this->m_mainEdit.SetWindowText(finalstring); return TRUE; }
在此方法中,请特别注意这些行
// Create our regular expression object boost::RegEx expr("(<\\s*(textarea|input|select)\\s*[^>]+>[^<>]*(</(select|textarea)>)?)", TRUE); // Create a vector to hold all matches std::vector<std::string> v; // Pass the vector and the STL string that hold the file contents // to the RegEx.Grep method. expr.Grep(v, wholeFileStr);
expr 对象是用模式构造的。我将模式分解如下
(<\s* // Match on an open tag "<" and zero or // more white space characters (textarea|input|select)\s+[^>]+> // 1. Match on either textarea, input, or select 1 2 3 // 2. look for one or more spaces next // 3. Match on one or more characters that // are not a ">" until we find the end ">" [^<>]* // Match on zero or more characters that are not // "<" or ">" (</(select|textarea)>)?) // Match on an end tag "</" and either a select or // a text area. The question mark means that everything // inside the quotes is optional(e.g. 0 or 1 occurrences).
注意: 在前面的描述中,转义字符没有转义两次。这是实际的正则表达式如果打印出来会是什么样子。
提醒一下,上面的正则表达式运算符表示
字符 | 描述 | 用法 |
---|---|---|
* | 匹配零个或多个前一个表达式。 | "\s*" -- 零个或多个空白字符 |
+ | 匹配一个或多个前一个表达式 | "\s+" -- 一个或多个空白字符 |
[^] | 否定集。 | "[^<]" -- 匹配任何不是小于号 "<" 的字符。可以是要否定的字符列表(例如 [^<>/] -- 匹配任何不是小于号、大于号或正斜杠的字符) |
Grep 方法接收对上面创建的向量的引用。在 Grep 调用之后,向量将包含所有找到的匹配项。使用 Grep() 而不是 Search()(这是另一个有用的方法),将允许您跨行边界进行匹配。这对于您读入的文件很重要——特别是允许相当宽松格式的 HTML 文件。例如,这
<input type="text" name="name">
与此相同
<input type="text"
name="name">
在任何网络浏览器中。我们需要对此进行说明。如果您对大小写敏感性感到疑惑,请查看 RegEx 对象的实例化。第二个参数是一个布尔值。这表示您是否希望它不区分大小写——在示例代码中我们就是这样做的。
如果您想了解更多关于 boost Regex++ 库 API 的信息,请查看
GetActualType 方法
在 GetActualType 方法中,我们提取当前行正在处理的输入字段类型。请记住,在 ParseFile 方法中,我们确保至少有一种输入类型,因此此行几乎肯定会有一个。这是方法实现
BOOL CRegexTestDlg::GetActualType(const char *line, char *type) { // Create a pattern to look for. char* pattern = "<\\s*((input|textarea|select))\\s*"; // Create RegEx object with pattern. Should be case-insensitive RegEx exp(pattern, TRUE); // Search for the pattern. Use Search, not Grep since we have a single line. if(exp.Search(line)) { // If found, copy the text of the first expression match to the // type variable. strcpy(type, exp[1].c_str()); return TRUE; } // We didn't find anything. Just copy an empty string. strcpy(type, ""); return FALSE; }
看看模式本身
char* pattern = "<\\s*((input|textarea|select))\\s*";
这里我们说寻找一个开括号“<”和可能的空白字符。然后寻找“input”、“textarea”或“Select”。然后可能还有一些空白字符。注意 input|textarea|select
周围的两组括号。内层括号告诉我们这是一组可能的值。竖线 (|)(也称为“或”)告诉我们匹配可以包含这三个值中的任何一个。外层括号将我们找到的内容捕获到一个特殊变量中。因此,如果您通过我们的解析器运行此 HTML 代码
<input type= "text" name="email" size="20">
exp[1]
现在将包含单词“input”。如果您的行有其他用于捕获匹配部分的括号,它们将放在 exp[n] 中,其中 n 是从左到右、从外到内计算的当前括号集。
GetValue 方法
在 GetValue 方法中,我们传入一个要查找的键和一个指向要用值填充的变量的指针。
void CRegexTestDlg::GetValue(char *key, const char *str, char *val) { char* tmpStr = "\\s*=\\s*\\\"?([^\"<>]+)\\\"?"; char final[100]; // We need to build the string so we know exactly what we're looking for. strcpy(final, key); strcat(final, tmpStr); // Create the RegEx object with the pattern. RegEx exp(final); // Search for the if(exp.Search(str)) { // If found, copy what we found. strcpy(val, exp[1].c_str()); } else { // Otherwise copy a string with the no<key> where <key> is the key passed in. sprintf(val, "no%s", key); } }
看看这个表达式
char* tmpStr = "\\s*=\\s*\\\"?([^\"<>]+)\\\"?";
这是我们迄今为止最复杂的模式。首先,我们寻找一些可能的空白、一个等号和更多可能的空白。然后我们寻找一个开引号。问号表示前一个表达式出现0次或1次,因此如果 HTML 中不包含开引号,我们也会考虑到这一点。也就是说,如果行看起来像以下两种情况(注意引号),它仍然会找到匹配项
<input type="text" name="email">
<input type=text name=email>
接下来我们正在寻找除引号(")、开括号(<)或闭括号(>)以外的任何字符。这就是我们的值。请注意,这个值周围有括号,因为我们想将这个值捕获到我们的特殊变量 exp[n] 中。接下来我们正在寻找一个闭引号和一个可能的闭引号。
这就是我们对正则表达式的需求的终结。我们现在已经获得了我们正在寻找的值,并且可以在列表框中对其进行格式化和输出。如何处理这些值取决于您,但现在您拥有了准确有效地解析 HTML 所需的一切。示例代码可能需要一些调整,但总体而言它能完成任务。
运行示例
我提供的示例应用程序解析了一个包含表单的 HTML 文件。为了方便起见,我在项目中包含了一个 HTML 表单文件。文件名是 contact_form.html,它可以在项目的根目录中找到。运行应用程序时,只需单击“浏览...”按钮并选择此文件。然后单击“试试看!”
结论
虽然我们可以使用 strtok 或其他分词器来构建我们的解析器,但这些对于 HTML 来说并不完全理想,因为 HTML 可以非常自由(例如,这里有一个空格,那里有引号,但这里没有,换行等)。正则表达式非常适合这种文本解析。
Regex++ 是一个非常健壮的正则表达式库,您会在您的应用程序中发现它非常有用。查看示例项目并熟悉正则表达式语法。这将使您能够以最少的编码创建强大的文本解析器,并使您能够“掌握您的数据”。