问题 Java中的多个同时子串替换


(我来自蟒蛇世界,所以我道歉,如果我使用的标准中的一些术语与常规。)

我有一个 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]"

使这个问题变得非常重要的是,替换子串并不总是与它们所替换的子串长度相同。

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

但有些东西告诉我应该有更好的方法......有吗?


8398
2018-06-19 05:18


起源

等等......你 开始 使用包含电子邮件地址的字符串,您想要 更换 那个地址的代币?是对的吗? - Tim Biegeleisen
我可能会使用令牌 - 所有字符串的方法,然后提供一个存储原始字符串及其替换的类 - 从中​​可以很容易地重建为编辑版本的原始字符串 - Scary Wombat
@TimBiegeleisen是的,我正在实施一个PII编辑器。 - coldspeed
@rustyx是的,确切地说。该 List<Token> list按起始索引的升序排序。 - coldspeed
@ jpmc26我不打算建造任何东西。有一个API可以嗅出敏感信息并返回可能的匹配。我正在阅读那些匹配对象并手动编辑字符串。没有什么广泛的字符串替换? - coldspeed


答案:


这种方法有效:

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]

9
2018-06-19 05:34



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


确保所有令牌都按排序 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();
}

3
2018-06-19 15:36



Oleksandr好一个! - Coder-Man
谢谢,但是这个解决方案比Robby的解决方案要慢一点(因为内部的数组移位) StringBuilder.replace() 方法)... - Oleksandr
它可能会慢一点,但它绝对非常优雅。 + 1 - Robby Cornelissen
@Oleksandr是的,我注意到了。 - Coder-Man


在开始和结束之间提取子字符串,然后按它进行拆分。然后你得到一个包含2个元素的数组,插入你想要的内容。接下来,你必须移动你的下一个字符串'来替换id(你替换长度的前一个字符串)和(你放在它的位置的字符串)之间的差异。

代码(以令牌中的'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;
        }
    }
}

1
2018-06-19 05:30



但它不仅仅是我想要替换的一个字符串。正如我解释的那样,我有一个指定开始,结束和替换子字符串的对象列表。这仍然有效吗? - coldspeed
好吧想象你有一个字符串“AA”并且你把“BBB”放在它的位置,现在你必须将你所有的下一个字符串的id移动一个。您不必更新下一个字符串的ID,只需将该delta存储在单独的变量中即可。 - Coder-Man
为什么选择downvote? - Coder-Man
我不是downvoter,但我不是100%肯定这对我的用例有用吗? - coldspeed
我更喜欢使用新输出的@Robby版本而不必担心 deltas - Scary Wombat