【问题标题】:Multiple simultaneous substring replacements in JavaJava中的多个同时子字符串替换
【发布时间】:2018-11-27 23:38:10
【问题描述】:

(我来自 python 世界,所以如果我在规范中使用 jar 的某些术语,我深表歉意。)

我有一个 String 和一个 List 的开始/结束索引要替换。无需过多详细介绍,请考虑以下基本模型:

String text = "my email is foo@bar.com and my number is (213)-XXX-XXXX"
List<Token> findings = SomeModule.someFnc(text);

Token的定义是

class Token {
    int start, end;
    String type;
}

这个List 代表我试图编辑的敏感数据的开始和结束位置。

实际上,API 返回的数据是我通过迭代获得的:

[{ "start" : 12, "end" : 22, "type" : "EMAIL_ADDRESS" }, { "start" : 41, "end" : 54, "type" : "PHONE_NUMBER" }]

使用这些数据,我的最终目标是编辑 text 中由这些 Token 对象指定的标记以获得以下信息:

"my email is [EMAIL_ADDRESS] and my number is [PHONE_NUMBER]"

使这个问题变得不平凡的原因是替换子字符串的长度并不总是与它们要替换的子字符串相同。

我目前的行动计划是从text 构建一个StringBuilder,按照起始索引的相反顺序对这些ID 进行排序,然后从缓冲区的右端进行替换。

但有件事告诉我应该有更好的方法......有吗?

【问题讨论】:

  • 等等...您开始是一个包含电子邮件地址的字符串,并且您想用令牌替换该地址?对吗?
  • 我可能会采用对所有字符串进行标记的方法,然后提供一个存储原始字符串及其替换的类 - 从中​​很容易重建为编辑版本的原始版本
  • @TimBiegeleisen 是的,我正在实施 PII 编辑器。
  • @rustyx 是的,完全正确。 List&lt;Token&gt; 列表按起始索引升序排序。
  • @jpmc26 我不想构建任何东西。有一个 API 可以嗅出敏感信息并返回可能的匹配项。我正在阅读那些匹配对象并手动编辑字符串。字符串替换没什么大不了的?

标签: java string stringbuilder


【解决方案1】:

提取 start 和 end 之间的子字符串,并用它分割。然后你得到一个包含 2 个元素的数组,在两者之间插入你想要的。接下来,您必须移动您的下一个字符串,以通过(您替换长度的前一个字符串)和(您放置在其位置的字符串)之间的差异来替换 ids。

代码(如果Token中的'end'是排他的):

public class Main {

    public static void main(String... args) {
        String text = "I want to replace AAA and B and scary wombat";
        Token[] tokens = {new Token(18, 21, "TEST"), new Token(26, 27, "TEST"), new Token(32, 44, "TEST")};
        int delta = 0;
        for (Token token : tokens) {
            String splitter = text.substring(token.start + delta, token.end + delta);
            System.out.println("Splitter: " + splitter);
            delta += token.replacement.length() - splitter.length();
            String[] beforeAndAfter = text.split(Pattern.quote(splitter));
            text = beforeAndAfter[0] + token.replacement + 
                    (beforeAndAfter.length == 2 ? beforeAndAfter[1] : ""); // in case where there are no more chars after splitter in text
        }
        System.out.println(text);
    }

    static class Token {
        public final int start, end;
        public final String replacement;

        public Token(int start, int end, String replacement) {
            this.start = start;
            this.end = end;
            this.replacement = replacement;
        }
    }
}

【讨论】:

  • 但这不仅仅是我要替换的一个字符串。正如我所解释的,我有一个指定开始、结束和替换子字符串的对象列表。这还能用吗?
  • 想象一下你有字符串“AA”并且你把“BBB”放在它的位置,现在你必须将所有下一个字符串的id移动一个。您不必更新下一个字符串的 ID,只需将该增量存储在单独的变量中即可。
  • 我不是反对者,但我不能 100% 确定这是否适用于我的用例?
  • 现在您必须将所有下一个字符串的 id 移动一个请说明您的意思
  • 我更喜欢使用新输出的@Robby 版本,而不必担心deltas
【解决方案2】:

这种方法有效:

import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        String text = "my email is foo@bar.com and my number is (213)-XXX-XXXX";

        List<Token> findings = new ArrayList<>();
        findings.add(new Token(12, 22, "EMAIL_ADDRESS"));
        findings.add(new Token(41, 54, "PHONE_NUMBER"));

        System.out.println(replace(text, findings));
    }

    public static String replace(String text, List<Token> findings) {
        int position = 0;
        StringBuilder result = new StringBuilder();

        for (Token finding : findings) {
            result.append(text.substring(position, finding.start));
            result.append('[').append(finding.type).append(']');

            position = finding.end + 1;
        }

        return result.append(text.substring(position)).toString();
    }
}

class Token {
    int start, end;
    String type;

    Token(int start, int end, String type) {
        this.start = start;
        this.end = end;
        this.type = type;
    }
}

输出:

my email is [EMAIL_ADDRESS] and my number is [PHONE_NUMBER]

【讨论】:

  • 我明白了。因此,您不是从头开始替换,而是从头开始追加。这与我开始考虑的迭代替换非常相似。没有更好的吗? (我可以回答“不”。)
  • 哦,那里可能有更优雅的解决方案。我再考虑一下。
  • 好的,谢谢。但我仍然感谢您的回答,所以 +1。
  • @coldspeed 你的字符串是小还是大?你有很多这样的字符串需要执行替换吗?根据这些因素,可能有一种方法可以并行化
  • 我会像new StringBuilder(text.length() + 32) 这样预先分配缓冲区。否则这是最快的解决方案(在 Java 中,目标是尽量减少对象创建)。
【解决方案3】:

确保所有令牌都按start索引升序排序:

List<Token> tokens = new ArrayList<>();
tokens.sort(Comparator.comparing(Token::getStart));

现在您可以替换从输入文本末尾开始的所有字符串:

public String replace(String text, List<Token> tokens) {
    StringBuilder sb = new StringBuilder(text);
    for (int i = tokens.size() - 1; i >= 0; i--) {
        Token token = tokens.get(i);
        sb.replace(token.start, token.end + 1, "[" + token.type + "]");
    }
    return sb.toString();
}

【讨论】:

  • 谢谢,但是这个解决方案比 Robby 的解决方案慢一点(因为数组在 StringBuilder.replace() 方法中移动)...
  • 它可能会慢一点,但它绝对是非常优雅的。 + 1
猜你喜欢
  • 2015-12-30
  • 1970-01-01
  • 2016-09-10
  • 2017-12-06
  • 2013-05-25
  • 2015-11-09
  • 2016-08-02
相关资源
最近更新 更多