替代连续的 String.replace

Posted

技术标签:

【中文标题】替代连续的 String.replace【英文标题】:Alternative to successive String.replace 【发布时间】:2014-12-31 08:25:15 【问题描述】:

我想替换字符串输入中的一些字符串:

string=string.replace("<h1>","<big><big><big><b>");
string=string.replace("</h1>","</b></big></big></big>");
string=string.replace("<h2>","<big><big>");
string=string.replace("</h2>","</big></big>");
string=string.replace("<h3>","<big>");
string=string.replace("</h3>","</big>");
string=string.replace("<h4>","<b>");
string=string.replace("</h4>","</b>");
string=string.replace("<h5>","<small><b>");
string=string.replace("</h5>","</b><small>");
string=string.replace("<h6>","<small>");
string=string.replace("</h6>","</small>");

如您所见,这种方法并不是最好的,因为每次我都必须搜索要替换的部分等,而且字符串是不可变的......而且输入很大,这意味着要解决一些性能问题考虑。

有没有更好的方法来降低这段代码的复杂性?

【问题讨论】:

StringBuilder 将是要走的路。它不会创建临时字符串并具有replace 方法 看看StringBuilder:docs.oracle.com/javase/7/docs/api/java/lang/StringBuilder.html StringBuilder.replace(int,int,String)不能像String.replace(CharSequence,CharSequence)一样直接使用。你必须在循环中使用StringBuilder.indexOf(String),这是有代价的。 您还应该考虑使用正则表达式匹配器,它可以一次性找到所有标签。 @Gene - 使用replaceAll()Pattern / Matcher 的正则表达式会不会降低效率? 【参考方案1】:

虽然StringBuilder.replace()String.replace() 相比有了巨大的改进,但距离最佳状态还有很远

StringBuilder.replace() 的问题在于,如果替换部分的长度与可替换部分的长度不同(适用于我们的案例),则可能必须分配更大的内部 char 数组,并且必须复制内容,并且然后会发生替换(这也涉及复制)。

想象一下:您有一个包含 10.000 个字符的文本。如果要将在位置1(第二个字符)找到的"XY" 子字符串替换为"ABC",则实现必须重新分配至少大1 的char 缓冲区,必须将旧内容复制到新数组,它必须将 9.997 个字符(从位置 3 开始)向右复制 1 以将 "ABC" 放入 "XY" 的位置,最后将 "ABC" 的字符复制到起始位置1。每次更换都必须这样做!这很慢。

更快的解决方案:即时构建输出

我们可以构建输出on-the-fly:不包含可替换文本的部分可以简单地附加到输出中,如果我们找到可替换的片段,我们会附加替换其中。从理论上讲,仅循环输入一次 就足以生成输出。听起来很简单,实现起来并不难。

实施:

我们将使用预加载可替换替换字符串映射的Map

Map<String, String> map = new HashMap<>();
map.put("<h1>", "<big><big><big><b>");
map.put("</h1>", "</b></big></big></big>");
map.put("<h2>", "<big><big>");
map.put("</h2>", "</big></big>");
map.put("<h3>", "<big>");
map.put("</h3>", "</big>");
map.put("<h4>", "<b>");
map.put("</h4>", "</b>");
map.put("<h5>", "<small><b>");
map.put("</h5>", "</b></small>");
map.put("<h6>", "<small>");
map.put("</h6>", "</small>");

使用这个,这里是替换代码:(代码后有更多解释)

public static String replaceTags(String src, Map<String, String> map) 
    StringBuilder sb = new StringBuilder(src.length() + src.length() / 2);

    for (int pos = 0;;) 
        int ltIdx = src.indexOf('<', pos);
        if (ltIdx < 0) 
            // No more '<', we're done:
            sb.append(src, pos, src.length());
            return sb.toString();
        

        sb.append(src, pos, ltIdx); // Copy chars before '<'
        // Check if our hit is replaceable:
        boolean mismatch = true;
        for (Entry<String, String> e : map.entrySet()) 
            String key = e.getKey();
            if (src.regionMatches(ltIdx, key, 0, key.length())) 
                // Match, append the replacement:
                sb.append(e.getValue());
                pos = ltIdx + key.length();
                mismatch = false;
                break;
            
        
        if (mismatch) 
            sb.append('<');
            pos = ltIdx + 1;
        
    

测试:

String in = "Yo<h1>TITLE</h1><h3>Hi!</h3>Nice day.<h6>Hi back!</h6>End";
System.out.println(in);
System.out.println(replaceTags(in, map));

输出:(包装以避免滚动条)

Yo<h1>TITLE</h1><h3>Hi!</h3>Nice day.<h6>Hi back!</h6>End

Yo<big><big><big><b>TITLE</b></big></big></big><big>Hi!</big>Nice day.
<small>Hi back!</small>End

此解决方案比使用正则表达式要快,因为这涉及很多开销,例如编译Pattern、创建Matcher 等,而正则表达式也更通用。它还在引擎盖下创建了许多临时对象,这些临时对象在替换后被丢弃。在这里,我只使用了StringBuilder(加上其内部的char 数组),并且代码只对输入String 进行了一次迭代。此外,此解决方案比使用此答案顶部的 StringBuilder.replace() 快得多。

注释及说明

我在replaceTags() 方法中初始化了StringBuilder,如下所示:

StringBuilder sb = new StringBuilder(src.length() + src.length() / 2);

所以基本上我创建的初始容量是原始String 长度的 150%。这是因为我们的替换比可替换的文本长,所以如果发生替换,输出显然会比输入长。为StringBuilder 提供更大的初始容量将根本不会导致内部char[] 重新分配(当然,所需的初始容量取决于可替换-替换对及其在输入中的频率/出现率,但这个 +50% 是良好的上估计)。

我还利用了所有可替换字符串都以 '&lt;' 字符开头的事实,因此找到下一个潜在的可替换位置变得非常快:

int ltIdx = src.indexOf('<', pos);

这只是一个简单的循环和charString 中的比较,并且由于它总是从pos 开始搜索(而不是从输入的开头),所以总体而言,代码只迭代输入String一次。

最后要判断一个可替换的String 是否确实出现在潜在位置,我们使用String.regionMatches() 方法来检查可替换的stings,它也非常快,因为它只是比较char 中的值一个循环并在第一个不匹配的字符处返回。

还有一个优点:

问题没有提到它,但我们的输入是一个 HTML 文档。 HTML 标签不区分大小写,这意味着输入可能包含&lt;H1&gt; 而不是&lt;h1&gt;。 对于这个算法,这不是问题。 String 类中的 regionMatches() 有一个重载,supports case-insensitive comparison:

boolean regionMatches(boolean ignoreCase, int toffset, String other,
                          int ooffset, int len);

所以如果我们想修改我们的算法来查找和替换相同但使用不同字母大小写的输入标签,我们只需要修改这一行:

if (src.regionMatches(true, ltIdx, key, 0, key.length())) 

使用此修改后的代码,可替换标签变得不区分大小写:

Yo<H1>TITLE</H1><h3>Hi!</h3>Nice day.<H6>Hi back!</H6>End
Yo<big><big><big><b>TITLE</b></big></big></big><big>Hi!</big>Nice day.
<small>Hi back!</small>End

【讨论】:

+1,这是 appendReplacement/appendTailMatcher 所做的变化。 “这个比使用正则表达式快得多”好吧,我不会说快得多,这取决于你如何使用正则表达式:) @Pshemo 可能你是对的,但是使用正则表达式通常会带来很多开销,比如编译 Pattern,创建 Matcher 等。它还会在引擎盖上创建许多临时对象,这些对象会被抛出使用后离开。这里我只使用了StringBuilder(加上char 数组)。 没错,我只是想捍卫一点正则表达式解决方案,使用replace(.., ..) 进行迭代肯定会比您的解决方案慢得多,即使每次它都需要从头开始。 这是我将使用的方法。多次遍历整个字符串是低效的。这种方法还利用了所有替换字符串都以“ 【参考方案2】:

您提供的特定示例似乎是 HTML 或 XHTML。尝试使用正则表达式编辑 HTML 或 XML 会遇到问题。对于您似乎有兴趣进行的那种编辑,您应该考虑使用 XSLT。另一种可能性是使用 SAX,流式 XML 解析器,并让您的后端动态编写编辑后的输出。如果文本实际上是 HTML,则最好使用容错的 HTML 解析器(例如 JSoup)来构建文档的解析表示(例如 DOM),并在输出之前对其进行操作。

【讨论】:

【参考方案3】:

使用Apache Commons StringUtils.replaceEach。

String[] searches =     new String[]"<h1>",                "</h1>",                  "<h2>", ...;
String[] replacements = new String[]("<big><big><big><b>",  "</b></big></big></big>", "<big><big>" ...;
string = StringUtils.replaceEach(string, searches, replacements);

【讨论】:

【参考方案4】:

我会做这样的事情

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < str.length(); i++) 
        if (tagEquals(str, i, "h1")) 
            sb.append("<big><big><big><b>");
            i += 2;
         else (tagEquals(s, i, "/h1"))  
            ...
         else 
            sb.append(str.charAt(i));
        
    

tagEquals 是一个检查标签名称的函数

【讨论】:

【参考方案5】:

为了提高性能 - 使用 StringBuilder。 为方便起见,您可以使用 Map 来存储值和替换。

Map<String, String> map = new HashMap<>();
map.put("<h1>","<big><big><big><b>");
map.put("</h1>","</b></big></big></big>");
map.put("<h2>","<big><big>");
...
StringBuilder builder = new StringBuilder(yourString);
for (String key : map.keySet()) 
    replaceAll(builder, key, map.get(key));

...要替换 StringBuilder 中的所有出现,您可以在此处查看: Replace all occurrences of a String using StringBuilder?

public static void replaceAll(StringBuilder builder, String from, String to)

    int index = builder.indexOf(from);
    while (index != -1)
    
        builder.replace(index, index + from.length(), to);
        index += to.length(); // Move to the end of the replacement
        index = builder.indexOf(from, index);
    

【讨论】:

【参考方案6】:

StringBuilder 由一个字符数组支持。因此,与 String 实例不同,它是 可变的。因此,您可以在StringBuilder 上致电indexOf()replace()

【讨论】:

【参考方案7】:

很遗憾,StringBuilder 没有提供replace(string,string) 方法,因此您可能需要考虑将PatternMatcherStringBuffer 结合使用:

String input = ...;
StringBuffer sb = new StringBuffer();

Pattern p = Pattern.compile("</?(h1|h2|...)>");
Matcher m = p.matcher( input );
while( m.find() )

  String match = m.group();
  String replacement = ...; //get replacement for match, e.g. by lookup in a map

  m.appendReplacement( sb, replacement );

m.appendTail( sb );

您可以使用 StringBuilder 执行类似的操作,但在这种情况下,您必须自己实现 appendReplacement 等。

至于表达式,您也可以尝试匹配 any html 标记(尽管这可能会导致问题,因为正则表达式和任意 html 不太适合)并且当查找没有任何结果,您只需将匹配项替换为自身即可。

【讨论】:

以上是关于替代连续的 String.replace的主要内容,如果未能解决你的问题,请参考以下文章

替代多个 String.Replaces [重复]

iCloud不支持有序集..替代解决方案?

CROSS JOIN + LEFT JOIN 子查询的替代策略?

应用程序保持文件锁定时的 ReplaceFile 替代方案

c语言中字符串能否转为代码执行?或者有啥替代办法?

在 C# 中打开大量文件流的替代方法