【问题标题】:Find All URL That Is Not An HTML Attribute or Content of A Hyperlink Tag查找不是 HTML 属性或超链接标记内容的所有 URL
【发布时间】:2014-01-02 12:29:13
【问题描述】:

我正在尝试找出一个匹配所有不是元素属性或超链接内容的 URL 的正则表达式。

应该匹配:

 1. This is a url http://www.google.com

不应匹配:

 1. <a href="http://www.google.com">Google</a>
 2. <a href="http://www.google.com">http://www.google.com</a> 
 3. <img src="http://www.google.com/image.jpg">
 4. <div data-url="http://www.google.com"></div>

我目前正在使用这个正则表达式来匹配所有 URL,我想我知道我必须检测什么,但我就是不知道如何使用正则表达式。

\\b(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]

已编辑

我想要达到的目标如下。我想转换这个字符串。

This is a url http://www.google.com <a href="http://www.google.com" title="Go to Google">Google</a><a href="http://www.google.com">http://www.google.com</a><img src="http://www.google.com/image.jpg"><div data-url="http://www.google.com"></div>

This is a url <a href="http://www.google.com">http://www.google.com</a> <a href="http://www.google.com" title="Go to Google">Google</a><a href="http://www.google.com">http://www.google.com</a><img src="http://www.google.com/image.jpg"><div data-url="http://www.google.com"></div>

通过删除标签然后将它们放回去进行预处理并不能解决问题,因为实际上最终会删除现有超链接元素的所有数据属性。当在 href 之外的其他属性中使用其他 URL 时,它也不能解决问题。

到目前为止,我还没有找到任何人建议的解决方案,并且到目前为止我还没有找到使用 HTML 解析器执行此操作的方法。使用正则表达式实际上似乎更可行。


已编辑 2

在根据 Dean 的建议进行尝试之后,我准备排除 HTML 解析器能够实现这一点,因为它无法处理字符串而不使其成为有效的 HTML 文档。这是基于建议示例的代码 + 处理排除案例 2 的修复。

    Document doc = Jsoup.parseBodyFragment(htmlText);

    final List<TextNode> nodesToChange = new ArrayList<TextNode>();

    NodeTraversor nd  = new NodeTraversor(new NodeVisitor() {

        @Override
        public void tail(Node node, int depth) {
            if (node instanceof TextNode) {
                TextNode textNode = (TextNode) node;
                Node parent = node.parent();
                if(parent.nodeName().equals("a")){
                    return;
                }

                String text = textNode.getWholeText();

                List<String> allMatches = new ArrayList<String>();
                Matcher m = Pattern.compile("\\b(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]")
                        .matcher(text);
                while (m.find()) {
                    allMatches.add(m.group());
                }

                if(allMatches.size() > 0){
                    nodesToChange.add(textNode);
                }
            }
        }

        @Override
        public void head(Node node, int depth) {        
        }
    });

    nd.traverse(doc.body());

此代码将 HTML、HEAD 和 BODY 标记添加到结果中。关于这个问题,我能想到的唯一方法是检查字符串中是否存在 HTML、HEAD 和 BODY 标签。如果没有,请在处理后将它们剥离。

我希望其他人有比这个 hack 更好的建议。就处理时间而言,使用 JSOUP 已经非常昂贵,所以我真的不想在不必要的情况下增加更多开销。

【问题讨论】:

  • “但我就是不知道如何使用正则表达式。” 正则表达式从来都不是用来解析 HTML 的。使用 HTML 解析器。 stackoverflow.com/q/1732348/418556
  • 正则表达式是一种强大的形式,但它们不太适合从 html 或 XML 中提取数据。您应该在预处理步骤中使用 XML 查询语言(例如 XQuery、XPath 或 XSLT)或 XML API(例如 SAX)。在此预处理步骤中,您可以摆脱所有属性和锚标记。如果您的 html 格式不正确,您将不得不在另一个预处理步骤中使用 HTML 清理器。
  • @AndrewThompson 我实际上也可以使用 HTML 解析器。你将如何处理它。假设我有这个字符串“这是一个 url google.com google.com">Google</a><a href="google.com">http://www.google.com</a><img src="google.com/image.jpg"><divdata-url="google.com"></div>"。每个人都一直建议使用 HTML/XML 解析器,但没有人提出解决这个问题的方法。 XML 解析器不能在这里使用,因为它不是格式良好的 XML。使用 HTML 解析器,我仍然需要找到一种方法来处理它。
  • @user152468 肯定更适合就具体细节提供建议。我在这件事上的“专业知识”是通过其他人获得的,并且在我最初的评论中几乎筋疲力尽。对不起。
  • 顺便说一句 - 请不要尝试将代码、HTML 等放入 cmets 中。请改为编辑问题。

标签: java html regex url


【解决方案1】:

期待有效的 HTML 输出

这里是帮助您入门的粗略指南。

  1. 使用 HTML5 解析引擎,例如 jsoup Java HTML Parser
    • HTML5 规范以已知的指定方式处理无效 HTML 以获得可预测的结果。
    • 这个解析引擎其实也提供了HTML修改方法。
  2. 像这样解析你的 HTML:

    String html = "This is a url http://www.google.com <a href=\"http://www.google.com\" title=\"Go to Google\">Google</a>";
    Document doc = Jsoup.parseBodyFragment(html);
    Element body = doc.body();
    
  3. 查找所有 文本节点(非 HTML 元素位)
  4. 测试文本是否看起来像一个链接(使用您的正则表达式)
  5. 替换same example中指示的文本。
  6. 获取完整修改文档的 HTML。
  7. 坐下来享受吧。

编辑 1 - 在无效 HTML 中替换的疯狂世界

似乎这个问题的作者已经指出内容是not有效的 HTML 并且需要维护 invalid HTML - 因为这样的 HTML 解析器不应该被用作任何 HTML 解析器在保存时可能会输出有效的 HTML。

正如我对原始问题的评论中所指出的,您可以在正则表达式中使用否定的外观。但只有傻瓜才会用 RegEx 解析 HTML - 显然我们不是这样,这里有一个可能的例子。

我不会在生产代码中使用它 - 但它回答了 OP 的问题

正则表达式

不幸的是,Java 不支持无限制的后视,所以我包含了以下限制:

  • 标签名称 - 最多 255 个字符
  • 空格 - 最多 30 个字符
  • 属性内容(包括属性和值) - 最多 4098 个字符

消极的后视

请注意,此可视化是不正确的,因为 [\p{L}0-9_.-] 被替换为 [A-Z0-9_.-] 以使可视化工作 - 但\p{L} 在技术上更正确,因为“任何 Unicode 字母”是可能的。

完成正则表达式

# Negative look-behind
(?<!
## N1: Looks like an HTML attribute value inside a HTML tag
### N1: Tag name
<[A-Z0-9]{1,255}
### N1: Any HTML attributes and values
(?:\s{1,30}[^<>]{0,4098})?
### N1: The begining of a HTML attribute with value
\s{1,30}
[\p{L}0-9_.-]{1,255}
\s{0,30}=\s{0,30}
### N1: Optional HTML attribute quotes
["']?
|
## N2: Looks like the start of an HTML tag text content
### N2: Tag name
<[A-Z0-9]{1,255}\s{1,30}
### N2: All HTML attributes and values
[^<>]{0,4098}
### N2: End of HTML opening tag
>
)
## Positive match: The URL value
((?:https?|ftp|file)://[-a-zA-Z0-9+&@\#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@\#/%=~_|])

Java

import java.util.*;
import java.lang.*;
import java.io.*;
import java.util.regex.*;

class CrazyInvalidHtmlUrlTextFindAndReplacer
{
    public static final String EXAMPLE_TEST = "This is a url http://www.google.com <a href=\"http://www.google.com\" title=\"Go to Google\">Google</a><a href=\"http://www.google.com\">http://www.google.com</a><img src=\"http://www.google.com/image.jpg\"><div data-url=\"http://www.google.com\"></div>";
    public static final String EXPECTED_OUTPUT_TEST = "This is a url <a href=\"http://www.google.com\">http://www.google.com</a> <a href=\"http://www.google.com\" title=\"Go to Google\">Google</a><a href=\"http://www.google.com\">http://www.google.com</a><img src=\"http://www.google.com/image.jpg\"><div data-url=\"http://www.google.com\"></div>";

    public static void main (String[] args) throws java.lang.Exception
    {
        System.out.println("Starting our non-HTML search and replace...");
        StringBuffer resultString = new StringBuffer();
        String subjectString = new String(EXAMPLE_TEST);
        System.out.println(subjectString);
        try {
            Pattern regex = Pattern.compile(
    "# Negative lookbehind\n" +
        "(?<!\n" +
        "## N1: Looks like an HTML attribute value inside a HTML tag\n" +
        "### N1: Tag name\n" +
        "<[A-Z0-9]{1,255}\n" +
        "### N1: Any HTML attributes and values\n" +
        "(?:\\s{1,30}[^<>]{0,4098})?\n" +
        "### N1: The begining of a HTML attribute with value\n" +
        "\\s{1,30}\n" +
        "[\\p{L}0-9_.-]{1,255}\n" +
        "\\s{0,30}=\\s{0,30}\n" +
        "### N1: Optional HTML attribute quotes\n" +
        "[\"']?\n" +
        "|\n" +
        "## N2: Looks like the start of an HTML tag text content\n" +
        "### N2: Tag name\n" +
        "<[A-Z0-9]{1,255}\\s{1,30}\n" +
        "### N2: All HTML attributes and values\n" +
        "[^<>]{0,4098}\n" +
        "### N2: End of HTML opening tag\n" +
        ">\n" +
        ")\n" +
        "## Positive match: The URL value\n" +
        "((?:https?|ftp|file)://[-a-zA-Z0-9+&@\\#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@\\#/%=~_|])", 
            Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE | Pattern.COMMENTS);
            Matcher regexMatcher = regex.matcher(subjectString);
            while (regexMatcher.find()) {
                System.out.println("text");
                try {
                    // You can vary the replacement text for each match on-the-fly

                    // !!!!!!!!!
                    // @todo Escape the attribute values and content text.
                    // !!!!!!!!!


                    regexMatcher.appendReplacement(resultString, "<a href=\"$1\">$1</a>");
                    } catch (IllegalStateException ex) {
                    // appendReplacement() called without a prior successful call to find()
                    System.out.println("IllegalStateException");
                    } catch (IllegalArgumentException ex) {
                    // Syntax error in the replacement text (unescaped $ signs?)
                    System.out.println("IllegalArgumentException");
                    } catch (IndexOutOfBoundsException ex) {
                    // Non-existent backreference used the replacement text
                    System.out.println("IndexOutOfBoundsException");
                }
            }
            regexMatcher.appendTail(resultString);

            } catch (PatternSyntaxException ex) {
            // Syntax error in the regular expression
            System.out.println("PatternSyntaxException");
            System.out.println(ex.toString());
        }

        System.out.println("result:");
        System.out.println(resultString.toString());

        if (resultString.toString().equals(EXPECTED_OUTPUT_TEST)) {
            System.out.println("success!!!!");
            } else {
            System.out.println("failure - expected:");
            System.out.println(EXPECTED_OUTPUT_TEST);
        }
    }

}

不知道在这方面的表现会如何 - 后视是昂贵的 - 除了 RegEx 通常也很昂贵。

【讨论】:

  • +1。很高兴知道 Jsoup 可用于将 HTML 与非 HTML 元素分开。
  • 这个答案(和提到的例子)有两个缺陷。 1)它不处理排除情况 2。您需要添加检查 TextNode 是否具有 HyperLink 类型的父级(可以使用 JSOUP 完成)。 2) 它修改了我的字符串,使其在解析时成为格式良好的 HTML。因此,我不知道如何取回原始字符串+修改后的部分。我需要能够对无效的 HTML 字符串以及有效的 HTML 文档执行此操作。它不能只从我的字符串中给我一个有效的 HTML。根据我的经验,似乎没有办法用 JSOUP 做到这一点。
  • 在最初的问题中,有(并且目前)没有提及输入内容是无效的 HTML 并且 您想要保持这种状态。有效地使其成为“非 HTML”内容。如果是这种情况,我们就不能遵守 HTML 规则。
  • 我添加了 在无效 HTML 中替换的疯狂世界 示例 - 享受您的 RegEx。
【解决方案2】:

根据 Dean 的建议和提到的示例,这里是问题的“解决方案”。请记住,它非常昂贵,主要是由于解析 HTML 字符串(四核/16GB RAM MBPr 上约 160 毫秒)。此解决方案还处理有效和无效的 HTML。请记住,围绕 JSOUP 的限制有一些小技巧,以确保不包含额外的标签以使最终结果成为有效的 HTML。我真的希望有人能想出一个更好的解决方案,但现在就在这里。

public static String makeHTML(String htmlText){
    boolean isValidDoc = false;
    if((htmlText.contains("<html") || htmlText.contains("<HTML")) && 
            (htmlText.contains("<head") || htmlText.contains("<HEAD")) &&
            (htmlText.contains("<body") || htmlText.contains("<BODY"))){
        isValidDoc = true;
    }

    Document doc = Jsoup.parseBodyFragment(htmlText);
    final String urlRegex = "\\b(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]";

    final List<TextNode> nodesToChange = new ArrayList<>();
    final List<String> changedContent = new ArrayList<>();

    NodeTraversor nd  = new NodeTraversor(new NodeVisitor() {

        @Override
        public void tail(Node node, int depth) {
            if (node instanceof TextNode) {
                TextNode textNode = (TextNode) node;
                Node parent = node.parent();
                if(parent.nodeName().equals("a")){
                    return;
                }

                String text = textNode.getWholeText();

                List<String> allMatches = new ArrayList<String>();
                Matcher m = Pattern.compile(urlRegex)
                        .matcher(text);
                while (m.find()) {
                    allMatches.add(m.group());
                }

                if(allMatches.size() > 0){
                    String result = text;
                    for(String match : allMatches){
                        result = result.replace(match, "<a href=\"" + match + "\">" + match + "</a>");
                    }
                    changedContent.add(result);
                    nodesToChange.add(textNode);
                }
            }
        }

        @Override
        public void head(Node node, int depth) {        
        }
    });

    nd.traverse(doc.body());

    int count = 0;
    for (TextNode textNode : nodesToChange) {
        String result = changedContent.get(count++);
        Node newNode = new DataNode(result, textNode.baseUri());
        textNode.replaceWith(newNode);
    }

    String processed = doc.toString();
    if(!isValidDoc){
        int start = processed.indexOf("<body>") + 6;
        int end = processed.lastIndexOf("</body>");
        processed = processed.substring(start, end);
    }

    return processed;
}

【讨论】:

    【解决方案3】:

    正如问题的 cmets 中所讨论的,仅使用正则表达式解决这个问题很困难(可能是不可能的?)。下面是一个 XSLT 样式表,它执行预处理步骤以从输入 html 中删除所有属性和所有锚标记。

    <?xml version="1.0" encoding="ISO-8859-1"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
      <xsl:template match="node()">
        <xsl:copy>
          <xsl:apply-templates select="node()"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="a">
      </xsl:template>
    
    </xsl:stylesheet>
    

    然后你可以运行你的正则表达式来提取剩余的url,这会简单得多。

    如果您输入的 html 无效,则使用 jtidy、htmlcleaner 或 htmltidy 作为进一步的预处理步骤。

    希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-06-06
      • 2015-08-23
      • 1970-01-01
      • 1970-01-01
      • 2012-06-24
      • 2015-12-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多