首页 > 解决方案 > 在条件注释中解析 HTML 电子邮件内容(尽量避免使用正则表达式!)

问题描述

我知道(尝试)用正则表达式解析 HTML 是多么的错误,这就是为什么我真的非常非常难以避免它的原因。

我有一个生成 HTML 电子邮件的应用程序。我们在电子邮件编辑器中使用了一个大型的 WYSIWYG 插件,它负责生成响应式电子邮件,并为 MS Outlook 等客户端生成糟糕的标记。它使用条件注释完成最后一点,如下所示。请注意,<v:roundrect>有一个href属性,并包装了<a>非 mso 客户端将看到的标记。

<!--[if mso]>
    <!-- irrelevant <table> layout here, removed for your sanity and mine -->
    <v:roundrect href="https://google.com" irrelevant_attributes="snipped">
        <w:anchorlock/>
        <v:textbox inset="0,0,0,0">
        <center style="snipped">
<![endif]-->
<a href="https://google.com" target="_blank" <!-- style="snipped" --> > 
    <!-- some span tags with styles on them --> 
    click me!
    <!-- </spans> --> 
</a> 
<!--[if mso]>
    </center>
    </v:textbox>
    </v:roundrect>
    <!-- </table> layout stuff -->
<![endif]-->

当然,这只是我们需要处理的数十种(可能数百种?)可能格式中的一种。

在推出此编辑器之前,我们要求客户使用更基本的 WYSIWYG HMTL 编辑器生成自己的 HTML 电子邮件;但他们有责任制作响应式模板并在各种客户端中测试其内容。从他们的角度来看,这个新编辑器是一个巨大的胜利。

当我们发送电子邮件时,通过重定向到原始链接的跟踪链接跟踪链接点击非常重要。

迄今为止,我们已经使用jSoup来解析电子邮件内容,查找任何锚标记并替换它们的 href 属性内容。因为正则表达式 html 解析是邪恶的,对吧?

有条件的评论给这些齿轮带来了麻烦。

因为它们是评论,jSoup 会忽略它们,并且来自 MS Outlook 和其他处理<v:roundrect>标记的客户端的点击尚未转换为通过我们的链接跟踪器,因此不会跟踪点击。这对我们来说是个问题。


第一个想法:用自定义标签替换条件注释

起初,我希望在让 jSoup 拥有消息体之前对其进行预处理。我会<!--[if mso]><adam><![endif]-->替换</adam>。这很简单,即使对于评论中条件的复杂形式也是如此。我使用正则表达式进行了一些简单的替换:

请注意,我对原始评论进行了完整的 url 编码。url-encoding 它确保我可以轻松地使用正则表达式来查找我的标记注释并将它们转换回来(这样我就不必担心>“orig”属性内容内部......

当我意识到有多种可能的方式可以关闭评论时,这种情况开始崩溃。我花了一点时间研究结束标签的类似方法。

我不知道您是否可以在结束标签上添加属性。我从来没有测试过它,因为在我到达那一点之前我有另一个认识。意识到 using<adam></adam>不会从 jSoup 产生理想的输出,因为生成的 INPUT 通常看起来像:

<adam><table><v:roundrect><center></adam>
<a></a>
<adam></center></v:roundrect></table></adam>

这不是整洁的 HTML,jSoup 会尝试更正它,更改标签的顺序以使其认为更正确。当我意识到这一点时,我停止了我正在做的事情,并重新开始思考这个问题。


第二个想法:同样的事情,但有评论

如果(新)问题是 jSoup 不喜欢我的标签嵌套,如果我可以从条件注释中公开 HTML,就好像它没有被注释掉一样,但保留一些标记作为我以后可以转换的注释回到评论?目标是做到这一点:

<!-- adam --><table><v:roundrect><center><!-- /adam -->
<a></a>
<!-- adam --></center></v:roundrect></table><!-- /adam -->

这应该解析为相当整洁的 HTML,对吧?所以我对代码进行了修改并试了一下。

可悲的是,我们正在使用的文档比我从上面开始的简单示例要复杂得多。这是实际示例文档的前几行:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
    <head> 
        <!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]--> 
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 
        <meta name="viewport" content="width=device-width"> 
        <!--[if !mso]><!--> 
        <meta http-equiv="X-UA-Compatible" content="IE=edge"> 
        <!--<![endif]--> 
        <title></title> 
        <!--[if !mso]><!--> 
        <!--<![endif]--> 
        <style type="text/css">/* snip */</style> 
        <style type="text/css" id="media-query">/* snip */</style> 
    </head>
    <body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #FFFFFF;"> 
        <style type="text/css" id="media-query-bodytag">

在评论转换之后,我们已经有效地将一个<xml>块放入了<head>块中,jSoup 显然不是它的粉丝。在将条件注释转换为我的普通标记注释,使用 jSoup 解析,然后将我的标记转换回它们的条件注释之后,这就是我从上述输入中得到的结果:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> 
    <!--[if gte mso 9]>
    </head>
    <body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #FFFFFF;">
        <xml> <o:officedocumentsettings> <o:allowpng /> <o:pixelsperinch> 96 </o:pixelsperinch> </o:officedocumentsettings> </xml>
    <![endif]--> 
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 
    <meta name="viewport" content="width=device-width"> 
    <!--[if !mso]><!--> 
    <meta http-equiv="X-UA-Compatible" content="IE=edge"> 
    <!--<![endif]--> 
    <title></title> 
    <!--[if !mso]><!--><!--<![endif]--> 
    <style type="text/css">/* snip */</style> 
    <style type="text/css" id="media-query">/* snip */</style>   
    <style type="text/css" id="media-query-bodytag">/* snip */</style> 

这里有一些大问题。该<head>块基本上立即关闭。<body>标签向上移动到块之前,而在它之后的<xml>所有东西都向下移动到主体中。这是行不通的。


怎么办?

我觉得我们基本上没有选择。

  1. 什么都不做,只是不计算来自 MS Outlook/etc 客户端的点击次数。在某些情况下,我们或许能够通过该电子邮件的下游转化检测到点击。(即使我们没有您点击链接的记录,如果您付款了,我们也知道您到达了那里......)
  2. 我们可以让我们的邮件提供商为我们进行链接跟踪(需要进行实验;不肯定他们也会跟踪<v:roundrect>链接)。从历史上看,我们与不提供链接跟踪的提供商一起启动了这个系统,因此我们不得不自己推出。当前的供应商提供它,但我们有多年的现有代码和流程,必须更新以支持此更改。如果我们想不出别的办法,我们会把它放在我们的后兜里,但是在中游换船的前景……并不吸引人。
  3. 或者最后......也许......正则表达式?(/me ducks) 我们可以让 jSoup 为普通的 HTML 做它的事情,然后使用正则表达式来替换任何剩余的链接。这变成了具有当前和未来标记的打地鼠游戏。除了未来我们还会遇到什么<v:roundrect>?¯\_(ツ)_/¯ 如果没有定期的人工审查,我们不会知道我们错过了什么。

除非还有我们尚未探索的另一种选择。那么......我们是否被困在任何东西/正则表达式中?

我们在 JVM 上,所以我想任何 Java 都触手可及。

标签: htmlemailjvmjsoupconditional-comments

解决方案


推荐阅读