65.9K
CodeProject 正在变化。 阅读更多。
Home

释放 Microsoft Office 文档中正则表达式的全部威力

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.54/5 (11投票s)

2008年6月13日

CPOL

6分钟阅读

viewsIcon

49300

downloadIcon

1089

第一部分:一种使用 .NET 和 Microsoft Office 主互操作程序集在 Microsoft Office 文档中执行强大搜索的方法

必备组件

为了运行示例应用程序,必须安装 Microsoft .NET Framework 2.0 或更高版本。此外,必须安装 Microsoft Office 2003 或更高版本以及 Microsoft Office 2003 主互操作程序集 (PIA) 可再分发包。如果完整安装 Microsoft Office 2003,则会安装这些 PIA,或者您可以从 Microsoft 免费获取它们。

有关如何在 .NET 程序中安装和使用主互操作程序集的更多信息,请参阅 此链接

我想强调的是,运行或修改此程序不需要 Office 的 Visual Tools。

引言

正则表达式是文本处理的强大工具。可以使用复杂的表达式来查找各种文本模式。正则表达式引擎集成在许多文本编辑器中。大多数正则表达式示例显示了如何处理 ASCII 或 Unicode 文本。除了处理前面提到的标准文本格式的编辑器外,还有数百万(甚至可能是数十亿)个文档以 Microsoft 的多种 Office 格式编码,例如 WORD 格式 (doc)、富文本格式 (RTF) 和 Excel (XLS)。虽然可以通过智能标签在 Microsoft Office 文档中使用正则表达式执行搜索,但对于许多文档处理目的而言,其实现非常繁琐。在本文中,我将通过使用 Microsoft .NET Framework,介绍一种将正则表达式的强大功能应用于 Microsoft Word 文档的简单方法。该方法利用 System.Text.RegularExpressions 命名空间和 Microsoft Word 互操作程序集来实现此解决方案。此外,通过使用动态加载程序集,可以验证每个正则表达式匹配,以确保匹配的正确性。例如,编写一个表示 2007 年 2 月 7 日的数值日期(例如 02/07/2007)的正则表达式很容易。但是,要在正则表达式中包含对无效日期(如 04/31/2002 或 02/30/2007)的检查,在没有执行此类检查的代码的情况下非常困难。

在未来的文章中,我计划介绍如何通过使用 MSOFFICE 互操作程序集和 .NET 技术,使用正则表达式执行复杂的文本搜索和替换算法。我还将把这些技术应用于其他 MSOFFICE 文档,例如 EXCEL。

背景

对 Microsoft 应用程序的正则表达式支持最早出现在 Word 97 中。其实现非常繁琐,因为使用的语法与正则表达式标准差异很大。Microsoft 认识到其实现中的不足,并将其作为智能标签库 2.0 的一部分重新引入了正则表达式,该库首次随 Microsoft Office 2003 提供。智能标签(其中正则表达式操作是其中一小部分)代表了一种通用的、集成的用户呈现文档数据的方式。然而,由于其不直观、复杂的方式,Microsoft 自己在其 MSDN 网站上承认,一项调查显示开发人员尚未采取必要措施来开发它们或使用 Microsoft .NET Framework 来实现。请参阅这篇 MSDN 文章了解更多信息:通过创建托管代码中的智能标签来实现 Office 2003 的潜力。本文的重点是设计一种简单而强大的方法来使用正则表达式(以及验证代码)。

Using the Code

启动时,程序会读取 XML 文件Searches.XML。此文件包含所有内置正则表达式搜索的信息。此 XML 文件中包含对 URL、IP 地址、美国日期、欧洲日期、美国电话号码和电子邮件地址的搜索。用户可以向此文件添加任意数量的搜索选项。通过勾选所需的搜索,可以激活每个搜索选项。

每个搜索组在 XML 文件中包含以下信息:

  • Search Regex – 用于搜索的正则表达式
  • Identifier – 出现在复选框列表中的搜索标题
  • FindColor – 用于突出显示文档中找到的文本的颜色
  • Action – 使用的操作(此版本仅支持查找)
  • PlugInName – 与搜索关联的程序集的名称。如果没有关联的程序集,“None”将被使用。
  • PlugInFunction – 在其插件程序集中为找到的此搜索块调用的函数
  • Description – 显示在复选框列表中的描述文本

查找文本

MSWordRegExDemo 包含通过 Microsoft Word 互操作程序集使用自动化操作 Microsoft Word 或 RTF 文档的方法。所有这些方法都包含在 DocumentEngine 类中。在此应用程序中使用的两个主要的 Microsoft Word 对象是

Word.Application app;
Word.Document theDoc;

为了打开文档,我们执行以下调用,该调用由 GUI 中的文件打开事件触发

// Opens a Microsoft WORD or RTF document
public void OpenDocument(string documentName)
{
    object optional = Missing.Value;
    object visible = true;
    object fileName = documentName;
    if (app == null)
        app = new Word.Application();

    app.Visible = true;

    try
    {
        // have Word open the document
        theDoc = app.Documents.Open(ref fileName, ref optional,
            ref optional, ref optional, ref optional, ref optional, ref optional,
            ref optional, ref optional, ref optional, ref optional, ref visible,
            ref optional, ref optional, ref optional, ref optional);

        paraCount = theDoc.Paragraphs.Count;
    }
    catch(Exception ex)
    {
        MessageBox.Show(ex.Message + ": Error opening document");
    }
}

第一步是将 Word 文档的文本转换为文本。一旦我们在文本域中获得了文档,我们就可以在文本上执行正则表达式搜索,看看是否有任何匹配。见下文

// convert the text in the Microsoft Office document into a .NET string
docText = docEngine.GetRng(currentParaNum).Text;

如果发生一个或多个匹配,我们将匹配的文本传递给 Microsoft Word.Find 函数。在搜索文本时,我们需要选择一个文本范围导入到文本中。我选择了段落范围说明符。这意味着我们将逐段遍历文档,在每个段落上执行我们的搜索。对于短文档,我们可以选择整个文档范围。如果我们想迭代脚注,Word 提供了一个脚注范围。为了获得每个段落的范围,使用以下函数

// returns the range of text in paragraph number
// nParagraphNumber
public Word.Range GetRng(int nParagraphNumber)
{
    try
    {
        return theDoc.Paragraphs[nParagraphNumber].Range;
    }
    catch (System.Runtime.InteropServices.COMException ex)
    {
        MessageBox.Show(ex.Message + "\nParagraph Number:
        " + nParagraphNumber.ToString() + " does not exist.");
        return null;
    }
}

执行文本“查找”的主要函数是 RegularExpressionFind

// perform a search based on regular expressions
public void RegularExpressionFind(int paraNum, string docText,
       SearchStruct selSearchStruct, out List<hitinfo /> hits)
{
    HitInfo hitInfo = new HitInfo();
    hits = new List<hitinfo />();
    System.Text.RegularExpressions.Regex r;
    Word.WdColor color = GetSearchColor(selSearchStruct.TextColor);

    r = new Regex(selSearchStruct.RegExpression);
    MatchCollection matches = r.Matches(docText);

    // no matches go on to next paragraph
    if (matches.Count == 0)
        return;

    // check if we have a validation assembly
    try
    {
        if (!LoadSearchAssembly(selSearchStruct.PlugInName,
                                selSearchStruct.PlugInFunction))
            return;
    }
    catch (Exception ex)
    {
        throw ex;
    }

    int index = 0;

    // this is the start point in the Microsoft Office document
    int startSearchPos = GetRng(paraNum).Start;

    foreach (Match match in matches)
    {
        // Perform validation check
        if (hasValidationAssembly)
        {
            Object[] objList = new Object[1];
            objList[0] = (Object)match;
            if (!Convert.ToBoolean(validationMethod.Invoke
                (assemblyInstance, objList)))
                continue;
        }
        index = docText.IndexOf(match.Value, index);

        // we assume the URL extends until first white space
        string matchStr = docText.Substring(index, match.Value.Length);
        index += matchStr.Length - 1;

        // find the pattern in the Word document
        FindTextInDoc(OperationMode.DotNetRegExMode, paraNum,
        matchStr, color, startSearchPos, out  startSearchPos,
        out hitInfo.StartDocPosition);

        // add match to our hit list
        hitInfo.Text = match.Value;
        hits.Add(hitInfo);
   }
}

首先,我们通过使用 Regex .NET 函数在导入的段落中搜索正则表达式。

r = new Regex(selSearchStruct.RegExpression);
MatchCollection matches = r.Matches(docText);

// no matches go on to next paragraph
if (matches.Count == 0)
    return;

如果存在匹配,我们将加载搜索程序集(如果尚未加载),并对匹配执行其他验证。

try
{
  if (!LoadSearchAssembly(selSearchStruct.PlugInName,
            selSearchStruct.PlugInFunction))
            return;
}

以下方法动态加载正则表达式的验证程序集(如果存在)。如果程序集先前已加载,LoadFrom 方法将返回它。

// loads the search assembly and the desired plug-in function
 public bool LoadSearchAssembly(string plugginName, string plugInFunction)
 {
     try
     {
        // if there is no validation assembly, leave
        if (plugginName.ToLower() == "none")
        {
            hasValidationAssembly = false;
            return true;
        }
        hasValidationAssembly = true;

        // Use the file name to load the assembly into the current
        // application domain.
        string plugginPath = Path.GetDirectoryName
        (Application.ExecutablePath) + @"\Plugins\" + plugginName;
        if (!File.Exists(plugginPath))
            throw new Exception("Cannot find path to assembly: " +
                                plugginName);

        Assembly a = Assembly.LoadFrom(plugginPath);
        // Get the type to use.
        Type[] types = a.GetTypes();

        // Get the method to call.
        validationMethod = types[0].GetMethod(plugInFunction);
        // Create an instance.
        assemblyInstance = Activator.CreateInstance(types[0]);

        return true;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
        return false;
    }
}

以下是验证数值日期的程序集

// SaelSoft -- NumericalDateValidatorClass.cs
// Purpose -- Validates dates in the form of:
// US Date Format:      (month) mm/ (day) dd/ (year) yyyy  or
// European Date Format: (day) dd/ (month) mm/ (year) yyyy)
// 2008 David Saelman

namespace SaelSoft.RegExPlugIn.NumericalDateValidator
{
    public class NumericalDateValidatorClass
    {
        int month = 0;
        int day = 0;
        int year = 0;
        public bool ValidateUSDate(Match matchResult)
        {
            if (matchResult.Groups.Count < 3)
                return false;
            int nResult = 0;

            if (int.TryParse(matchResult.Groups[1].ToString(), out nResult))
                month = nResult;
            else
                return false;
            if (int.TryParse(matchResult.Groups[2].ToString(), out nResult))
                day = nResult;
            else
                return false;

            if (int.TryParse(matchResult.Groups[3].ToString(), out nResult))
                year = nResult;
            else
                return false;

            return CommonDateValidation();
        }

        public bool ValidateEuropeanDate(Match matchResult)
        {
            if (matchResult.Groups.Count < 3)
                return false;
            int nResult = 0;

            if (int.TryParse(matchResult.Groups[1].ToString(), out nResult))
                month = nResult;
            else
                return false;
            if (int.TryParse(matchResult.Groups[2].ToString(), out nResult))
                day = nResult;
            else
                return false;

            if (int.TryParse(matchResult.Groups[3].ToString(), out nResult))
                year = nResult;
            else
                return false;

            return CommonDateValidation();
        }

        private bool CommonDateValidation()
        {
            // verify that all 30 day months do not contain 31 days e.g. 4/31/2007
            if (day == 31 && (month == 4 || month == 6 || month == 9 || month == 11))
            {
                return false; // 31st of a month with 30 days
            }
            // February, a special case cannot contain 30 or more days
            else if (day >= 30 && month == 2)
            {
                return false; //  checFebruary 30th or 31st
            }
            // check for February 29 outside a leap year
            else if (month == 2 && day == 29 && !(year % 4 == 0
                                && (year % 100 != 0 || year % 400 == 0)))
            {
                return false;
            }
            else
            {
                return true; // Valid date
            }
        }
    }

最后,如果我们有一个真正的匹配,我们就通过调用 DocumentEngine 函数 FindTextInDoc 来在 Word 文档中搜索匹配的 string

internal bool FindTextInDoc(OperationMode opMode, int currentParaNum,
         string textToFind, Word.WdColor color, int start, out int end,
         out int textStartPoint)
{
    string strFind = textToFind;
    textStartPoint = 0;

    // get the range of the current paragraph
    Word.Range rngDoc = GetRng(currentParaNum);

    // make sure we are not past the end of the range
    if (start >= rngDoc.End)
    {
        end = 0;
        return false;
    }
    rngDoc.Start = start;

    // setup Microsoft Word Find based upon
    // Regular Expression Match
    rngDoc.Find.ClearFormatting();
    rngDoc.Find.Forward = true;
    rngDoc.Find.Text = textToFind;

    // make search case sensitive
    object caseSensitive = "1";
    object missingValue = Type.Missing;

    // wild cards
    object matchWildCards = Type.Missing;

    // this is for a future version
    if (opMode == OperationMode.Word97Mode)
        matchWildCards = "1";

    // find the text in the word document
    rngDoc.Find.Execute(ref missingValue, ref caseSensitive,
        ref missingValue, ref missingValue, ref missingValue,
        ref missingValue, ref missingValue, ref missingValue,
        ref missingValue, ref missingValue, ref missingValue,
        ref missingValue, ref missingValue, ref missingValue,
        ref missingValue);

    // select text if true
    if (hilightText)
        rngDoc.Select();

    end = rngDoc.End + 1;
    textStartPoint = rngDoc.Start;

    // we found the text
    if (rngDoc.Find.Found)
    {
        rngDoc.Font.Color = color;
        // the range endpoint will change if we modified the text
        return true;
    }
    return false;
}

关注点

DocumentEngine 类利用 Microsoft Office 事件来检测用户关闭应用程序加载的 Microsoft Word 文档的情况。当调用 Quit 事件时,应用程序和文档对象被设置为 NULL。当用户打开新文档时,它们会被重新初始化。

public DocumentEngine()
{
  app = new Word.Application();
  // the following line will not compile if the Microsoft
  ((Word.ApplicationEvents4_Event)app).Quit += new Microsoft.Office.
  Interop.Word.ApplicationEvents4_QuitEventHandler(App_Quit);
}

// notification that application was quit by user
private void App_Quit()
{
   app = null;
   theDoc = null;
}

此项目可以作为 Microsoft Word 和 RTF 文档的复杂文档处理应用程序的第一步。基本上,使用正则表达式对 ASCII 或 UNICODE 文件可以完成的所有操作,现在也可以轻松地对 *.doc*.rtf 文件完成。在我的下一篇文章中,我将展示如何通过动态程序集,使用正则表达式执行复杂的格式设置。

有关 Microsoft Office 互操作程序集的更多在线信息,请参阅 MSDN。

供进一步研究

对于那些想了解更多关于正则表达式和 Microsoft Office 自动化的信息的人,我推荐以下优秀的书籍:《Mastering Regular Expressions》作者:Jeffrey E. F. Freidl,以及《Visual Studio Tools for Office - Using C# with Excel, Word, Outlook, and Infoview》作者:Eric Carter 和 Eric Lippert。

历史

  • 2008 年 6 月 13 日:初版
  • 2008 年 6 月 14 日:修复了 *.sln(解决方案文件),使其更整洁
  • 2008 年 6 月 16 日:添加了一个 ColorCheckedBoxList 组件(继承自 CheckeListBox),以便能够看到哪个颜色对应哪个正则表达式匹配。
    还添加了拖放功能。
© . All rights reserved.