灵活的文本解析策略

时间:2011-01-28 18:42:36

标签: c# text-parsing

问题

我正在尝试找到一种灵活的解析电子邮件内容的方法。下面是我正在使用的虚拟电子邮件文本的示例。如果可能的话,我也想避免使用正则表达式。然而,在解决问题的过程中,我开始认为这是不可避免的。请注意,这只是完整电子邮件的一个小虚拟子集。我需要的是将每个字段(例如,Ticket No,Cell Phone)解析为它们各自的数据类型。最后,电子邮件中不保证某些字段存在(您将在下面显示的当前解决方案中看到为什么这是一个问题)。

Header Code:EMERGENCY                               
Ticket No:   123456789 Seq. No: 2
Update of:             

Original Call Date:     01/02/2011     Time:      11:17:03 AM  OP: 1102
Second Call Date:     01/02/2011     Time:      12:11:00 AM  OP: 

Company:           COMPANY NAME
Contact:      CONTACT NAME          Contact Phone: (111)111-1111
Secondary Contact: SECONDARY CONTACT
Alternate Contact:                       Altern. Phone:                  
Best Time to Call: AFTER 4:30P           Fax No:        (111)111-1111
Cell Phone:                              Pager No:                       
Caller Address: 330 FOO
                FOO AVENUE 123

当前解决方案

对于这个简单的例子,我成功地使用下面的函数解析了大多数字段。

private T BetweenOperation<T>(string emailBody, string start, string end)
{
 var culture = StringComparison.InvariantCulture;
 int startIndex =
  emailBody.IndexOf(start, culture) + start.Length;
 int endIndex =
  emailBody.IndexOf(end, culture);
 int length = endIndex - startIndex;

 if (length < 0) return default(T);

 return (T)Convert.ChangeType(
  emailBody.Substring(startIndex, length).Trim(), 
  typeof(T));
}

基本上,我的想法是我可以解析两个字段之间的内容。例如,我可以通过

来实现标题代码
// returns "EMERGENCY"
BetweenOperation<string>("email content", "Header Code:", "Ticket No:")

然而,这种方法存在许多缺陷。一个很大的缺陷是end字段并不总是存在。正如您所看到的,有一些类似的键具有相同的关键字,无法解析得恰到好处,例如“联系人”和“次要联系人”。这会导致解析器获取太多信息。此外,如果我的结束字段不存在,我将得到一些不可预测的结果。最后,我可以解析整行,然后使用它将其传递给BetweenOperation<T>

private string LineOperation(string startWithCriteria)
{
    string[] emailLines = EmailBody.Split(new[] { '\n' });

    return 
        emailLines.Where(emailLine => emailLine.StartsWith(startWithCriteria))
        .FirstOrDefault();
}

我们会在字段名称不唯一的某些情况下使用LineOperation(例如时间),并将结果提供给BetweenOperation<T>

问题

如何根据键解析上面显示的内容。例如,键是“标题代码”和“手机”。请注意,我不认为基于制表符的空格进行解析,因为某些字段可能是多行(例如,来电者地址)或根本不包含任何值(例如,Altern电话)。

谢谢。

4 个答案:

答案 0 :(得分:3)

在我看来,我会按特定顺序对其进行解析,然后相应地修改您的电子邮件正文。

特定序列

Contact:      CONTACT NAME          Contact Phone: (111)111-1111
Secondary Contact: SECONDARY CONTACT
Alternate Contact: 

搜索字段的顺序应该以不是“字段”中任何其他关键字的子集的字开头(对于联系人,序列应为“次要联系人:”,“替代联系人:”然后最后“联系方式:”)

修改您的电子邮件正文,如果您找到了所需的字段信息,则需要修改电子邮件正文以将其删除。通过特定序列进行解析,将确保(我希望)您不会遇到整个不匹配问题,因为您最后删除子集。

现在还存在结束关键字字段的问题。由于结束字段并不总是保证存在(并且我不确定它们是否总是按特定顺序),您将不得不遍历所有关键字字段,并返回索引并根据索引确定最接近的关键字

答案 1 :(得分:2)

我必须在当天阅读来自Pick DB的报告时做类似的事情。如果您的字段是基于位置的,则只需创建电子邮件的XML Schema:

<message>
    <line0>
        <element name="Header Code" start="0" end="MAX" type="string"/> 
        <!-- MAX Indicates whole line -->
    </line0> 
    <line1>
        <element name="Ticket No" start="0" end="20" type="string"/>
        <element name="Seq. No" start="22" end="40" type="int" />
    </line1>
</message>

然后,要解析电子邮件,您将阅读所有文本文本行。 对于每一行(从0开始),您将在模式中找到“line”+索引号实体。

创建临时字符串。 “line”+ index实体中的Foreach元素在整个行上执行子字符串,从元素实体中定义的起始值到结束值开始....

根据元素的类型对子字符串进行转换。将实体保存到对象或其他内容。

您甚至可以通过类别对模式中的不同行+索引实体进行分组来获得更多创意:

<message>
    <header>
        <line0>
        ...
        </line0>
    </header>
</message>

答案 2 :(得分:2)

解决问题的一种方法是首先在整个文本中搜索键的出现次数。也就是说,构建一个看起来像这样的数组:

"Header Code:",1
"Contact Phone:",233
"Cell Phone:",-1  // not there

如果按位置对该数组进行排序,那么您就知道在哪里寻找东西。也就是说,你会知道每个字段都跟着它们。

您必须使用重复项(即通话日期中的“时间:”和“时间:”)执行某些操作。而你必须解决“联系方式:”和“次要联系方式:”,尽管这应该很容易。

如果使用标准字符串操作(即IndexOf)执行此操作,则效率会稍低,因为您必须在整个文本中搜索每个字符串的所有匹配项。这对你来说是否有问题很难说。取决于你需要做多少这些。

如果它成为一个问题,你可能想要建立一个Aho-Corasick字符串匹配器,或类似的东西。或者你可以建立一个大丑陋的正则表达式:

"(Header Code:)|(Contact Phone:)|(Cell Phone)" ...等等可能是命名捕获,所以你知道你在捕捉什么。尽管可能难以维护,但它应该可以很好地工作。

答案 3 :(得分:2)

我首先将行中的邮件分开,方法是使用一个StringReader,一次解析一条线,跳过完全空行。由于您正在寻找的标签是注释,循环遍历每行中的潜在标签,如果您发现了一个密码提取正确的部分(您可以使用空格)。不知道如何使用正则表达式是一种选择,但如果它们在预先使用的基础上使用它将作为一种魅力。