用于在HTML中查找URL的RegEx在Java / Android中需要25秒

时间:2013-09-26 04:38:26

标签: java android regex

在Android / Java中,鉴于网站的HTML源代码,我想提取所有XML和CSV文件路径。

我在做什么(使用RegEx)是这样的:

final HashSet<String> urls = new HashSet<String>();
final Pattern urlRegex = Pattern.compile(
        "[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|].(xml|csv)");
final Matcher url = urlRegex.matcher(htmlString);
while (url.find()) {
    urls.add(makeAbsoluteURL(url.group(0)));
}

public String makeAbsoluteURL(String url) {
    if (url.startsWith("http://") || url.startsWith("http://")) {
        return url;
    }
    else if (url.startsWith("/")) {
        return mRootURL+url.substring(1);
    }
    else {
        return mBaseURL+url;
    }
}

不幸的是,对于长度正常的普通网站,这会运行大约25秒。出了什么问题?我的RegEx是不是很糟糕?或者RegEx是如此之慢?

如果没有RegEx,我可以更快地找到网址吗?

修改

有效字符的来源是(大致)this answer。但是,我认为必须交换两个字符类(方括号),以便为URL的第一个字符设置更有限的字符集,为所有剩余的字符设置更宽的字符类。这是意图。

6 个答案:

答案 0 :(得分:4)

你的正则表达式的编写方式使得长输入变慢。 *运算符贪婪。

例如输入: http://stackoverflow.com/questions/19019504/regex-to-find-urls-in-html-takes-25-seconds-in-java-android.xml

正则表达式的[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*部分将使用整个字符串。然后它将尝试匹配下一个字符组,这将失败(因为整个字符串被消耗)。然后它将在正则表达式的第一部分的匹配中回溯一个字符并尝试再次匹配第二个字符组。它会匹配。然后它将尝试匹配点并失败,因为整个字符串被消耗。另一个回溯等...

从本质上讲,你的正则表达式正在强制进行大量的回溯以匹配任何东西。它也会浪费很多时间在无法取得成功的比赛上。

对于单词forest,它将首先在表达式的第一部分中使用整个单词,然后在未能与表达式的其余部分匹配后重复回溯。浪费大量时间。

此外:

  • 正则表达式中的.未转义,它将匹配任何字符。
  • url.group(0)是多余的。 url.group()具有相同的含义

为了加速正则表达式,你需要找到一种减少回溯量的方法,如果你没有一般的比赛开始也会有所帮助。现在每个单词都会导致匹配开始并且通常会失败。例如,通常在html中,所有链接都在2 "内。如果是这种情况,您可以在"开始匹配,这将极大地加快速度。试着找到更好的表达方式。

答案 1 :(得分:3)

我在U Mad所做的理论概述中没有任何说法,他突出了我注意到的一切。

考虑到你对RE的看法,我想建议你的是改变你的RE的观点:)

您正在寻找xml和csv文件,那么为什么不反转html字符串,例如使用:

new StringBuilder("bla bla bla foo letme/find.xml bla bla").reverse().toString()
之后你可以寻找模式:

final Pattern urlRegex = Pattern.compile(
    "(vsc|lmx)\\.[-a-zA-Z0-9+&@#/%=~_|][-a-zA-Z0-9+&@#/%?=~_|!:,.;]*");

urlRegex模式可以像U Mad已经建议的那样进行优化。但通过这种方式,您可以减少失败的匹配数量。

答案 2 :(得分:1)

建议仅使用正则表达式查找文件扩展名.xml.csv)。这应该快得多,当找到时,你可以向后看,检查每个字符,当你到达一个不在URL中的字符时停止 - 见下文:

final HashSet<String> urls = new HashSet<String>();
final Pattern fileExtRegex = Pattern.compile("\\.(xml|csv)");
final Matcher fileExtMatcher = fileExtRegex.matcher(htmlString);

// Find next occurrence of ".xml" or ".csv" in htmlString
while (fileExtMatcher.find()) {
    // Go backwards from the character just before the file extension
    int dotPos = fileExtMatcher.start() - 1;
    int charPos = dotPos;
    while (charPos >= 0) {
        // Break if current character is not a valid URL character
        char chr = htmlString.charAt(charPos);
        if (!((chr >= 'a' && chr <= 'z') ||
              (chr >= 'A' && chr <= 'Z') ||
              (chr >= '0' && chr <= '9') ||
              chr == '-' || chr == '+' || chr == '&' || chr == '@' ||
              chr == '#' || chr == '/' || chr == '%' || chr == '?' ||
              chr == '=' || chr == '~' || chr == '|' || chr == '!' ||
              chr == ':' || chr == ',' || chr == '.' || chr == ';')) {
            break;
        }
        charPos--;
    }

    // Extract/add URL if there are valid URL characters before file extension
    if ((dotPos > 0) && (charPos < dotPos)) {
        String url = htmlString.substring(charPos + 1, fileExtMatcher.end());
        urls.add(makeAbsoluteURL(url));
    }
}

小免责声明:我使用部分原始正则表达式来获取有效的网址字符:[-a-zA-Z0-9+&@#/%?=~_|!:,.;]。尚未验证这是否全面,可能还有进一步的改进,例如它目前会找到本地文件路径(例如C:\TEMP\myfile.xml)以及URL。希望保持上面的代码简单,以证明该技术,所以没有解决这个问题。

编辑关于效率的评论后,我已修改为不再使用正则表达式来检查有效的网址字符。相反,它会手动将字符与有效范围进行比较。 Uglier代码但应该更快......

答案 3 :(得分:1)

我怀疑,如果有一个String真的足够长,需要25秒才能解析。所以我尝试并且现在必须承认,使用大约27MB的文本,用给定的正则表达式解析它需要大约25秒。

我好奇地用@ FabioDch的方法改变了小测试程序(所以,如果你想在任何地方投票,请投票给他: - )

结果令人印象深刻:@ FabioDch的方法不需要25秒,而是需要不到1秒(100ms到800ms)+ 70ms到85ms进行倒车!

这是我使用的代码。它从我找到的最大文本文件中读取文本,并将其复制10次以获得27MB文本。然后对它运行正则表达式并打印出结果。

@Test
public final void test() throws IOException {
    final Pattern urlRegex = Pattern.compile("(lmx|vsc)\\.[-a-zA-Z0-9+&@#/%=~_|][-a-zA-Z0-9+&@#/%?=~_|!:,.;]*");
    printTimePassed("initialized");

    List<String> lines = Files.readAllLines(Paths.get("testdata", "Aster_Express_User_Guide_0500.txt"), Charset.defaultCharset());
    StringBuilder sb = new StringBuilder();
    for(int i=0; i<10; i++) { // Copy 10 times to get more useful data 
        for(String line : lines) {
            sb.append(line);
            sb.append('\n');
        }
    }
    printTimePassed("loaded: " + lines.size() + " lines, in " + sb.length() + " chars");
    String html = sb.reverse().toString();
    printTimePassed("reversed");

    int i = 0;
    final Matcher url = urlRegex.matcher(html);
    while (url.find()) {
        System.out.println(i++ + ": FOUND: " + new StringBuilder(url.group()).reverse() + ", " + url.start() + ", " + url.end());
    }
    printTimePassed("ready");
}

private void printTimePassed(String msg) {
    long current = System.currentTimeMillis();
    System.out.printf("%s: took %d ms\n", msg, (current - ms));
    ms = current;
}

答案 4 :(得分:0)

我知道people love to use regex to parse html,但您是否考虑过使用jsoup

答案 5 :(得分:0)

为了清楚起见,我为这个正则表达式创建了一个单独的答案:

编辑以逃避点并删除不情愿的数量。

(?<![-a-zA-Z0-9+&@#/%=~_|])[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]‌\\​.(xml|csv)

请尝试这个并告诉我它是怎么回事。

此外,这是一个类,它使您能够搜索反向字符串而不实际反转它:

    public class ReversedString implements CharSequence {
    public ReversedString(String input) {
        this.s = input;
        this.len = s.length();
    }
    private final String s;
    private final int len;
    @Override
    public CharSequence subSequence(final int start, final int end) {
        return new CharSequence() {
            @Override
            public CharSequence subSequence(int start, int end) {
                throw new UnsupportedOperationException();
            }

            @Override
            public int length() {
                return end-start;
            }

            @Override
            public char charAt(int index) {
                return s.charAt(len-start-index-1);
            }
            @Override
            public String toString() {
                StringBuilder buf = new StringBuilder(end-start);
                for(int i = start;i < end;i++) {
                    buf.append(s.charAt(len-i-1));
                }
                return buf.toString();
            }
        }; 
    }

    @Override
    public int length() {
        return len;
    }

    @Override
    public char charAt(int index) {
        return s.charAt(len-1-index);
    }

}

您可以这样使用此类:

pattern.matcher(new ReversedString(inputString));