onlyliuxin / coding2017

218 stars 643 forks source link

如何用正则表达式求模板字符串替换前后的键值对 #506

Closed eulerlcs closed 7 years ago

eulerlcs commented 7 years ago

我是长沙-刘春生(41689722)。 深圳-滴滴滴(69671710) 在群里提了一个问题,我想想了,这个问题正是体现正则表达式威力的好例子,所以在这纪录一下,供大家讨论,我把他的问题整理了一下。

问题

有一个模板字符串,占位符(或者也可以叫变量)的格式是${变量名},同时有一个把各个占位符替换成真实值之后的文本字符串,请列出占位符和其替换后真实值的键值对。

例子

问题 模板字符串:【工银信用卡】于${startTime}至${endTime}申办奋斗卡,无年费,赢郎平签名排球!详情${link} 文本字符串:【工银信用卡】于昨天至今天申办奋斗卡,无年费,赢郎平签名排球!详情没有

答案

占位符 真实值
${startTime} 昨天
${endTime} 今天
${link} 没有
eulerlcs commented 7 years ago

代码我已经写好了,我先提供一下思路,有兴趣的同学可以想想,写写代码,过几天我公布我的答案。 真正的代码不超过30行。

思路

1.抽出所有的占位符

先写出占位符的正则表达式,然后用Matcher.find()查找模板字符串,Matcher.group()就是占位符。

2.改造模板字符串使其成为正则表达式

把占位符替换成正则表达式的子表达式(.*?),注意在替换后的正则表达式字符串前后要加 ^$,理由大家想想。

3.用上面改造后的正则表达式抽出替换后的真实值

同样,用Matcher.find()查找文本字符串,Matcher.group()就是替换后的真实值。

4.注意点

如果模板字符串存在特殊字符,比如模板字符串中有且仅有一个左括号时,如果不进行特殊处理,在第三步中改造后的正则表达式有语法错误,是不正确的。

eulerlcs commented 7 years ago

提示

/**
 * Matcher.group的例子:匹配 字母-数字
 * 
 * <pre>
 * group(0):正则表达式的匹配值
 * </pre>
 */
@Test
public void test05_00() {
    Pattern p = Pattern.compile("([a-z]+)-(\\d+)");
    Matcher m = p.matcher("type x-235, type y-3, type zw-465");

    while (m.find()) {
        System.out.println(m.group());
    }
}
BradyWZH commented 7 years ago

我来试一试,templateReplace()函数代码不超过30行^_^,欢迎大佬批评指正,想自己写的小伙伴可以先不看,以免影响思路。

`

public void test(){
    String tempStr = "【工银信用卡】于${startTime}至${endTime}申办奋斗卡,无年费,赢郎平签名排球!详情${link}";
    String textStr ="【工银信用卡】于昨天至今天申办奋斗卡,无年费,赢郎平签名排球!详情没有";
    Map<String, String> map = templateReplace(tempStr, textStr);
    // 遍历map
    for (Map.Entry<String, String> entry : map.entrySet()){
        System.out.println(entry.getKey() + " -- " + entry.getValue());
    }
}

public Map<String, String> templateReplace(String tempStr, String textStr){
    Map<String, String> map = new HashMap<>();
    List<String> list = new ArrayList<>();
    Pattern p = Pattern.compile("(\\$\\{.+?\\})");
    Matcher m = p.matcher(tempStr);
    // 找到占位符,并将其替换为(.*?)
    while(m.find()){
        list.add(m.group());
        map.put(m.group(), "");
        tempStr = tempStr.replace(m.group(), "(.*?)");
    }
    // 此处如果不加开始符^和结束符$的话,由于"?"是最短匹配,最后一个文本会是空字符串,即匹配不到示例中的  "没有"
    tempStr = "^" + tempStr + "$";
    p = Pattern.compile(tempStr);
    m = p.matcher(textStr);
    while(m.find()){
        for (int i = 1; i <= m.groupCount(); i++){
            map.put(list.get(i - 1), m.group(i));
        }
    }
    return map;
}

`

eulerlcs commented 7 years ago

@BradyWZH 对于例子的处理,你的结果是对的,有三个小地方,再改一下就完美了。 在和深圳-滴滴滴(69671710) 的讨论中,如果字符串中存在某些特殊字符,要特殊处理。 我 思路 里又加了第四点。

  1. 下边这行,占位符的正则表达式外层的括号是多余的。

    Pattern p = Pattern.compile("(\\$\\{.+?\\})");
  2. 改造模板字符串使其成为正则表达式的处理,可以replaceAll一次性作成,当然你那样做没有错。可以写成下面这样。

    // 模板字符串中变量的正则表达式
    String keyRegex = "\\$\\{.*?\\}";
    // **关键想法** 把模板字符串改装成正则表达式
    String tmplRegex = "^" +tempStr.replaceAll(keyRegex, "(.*?)") + "$";
  3. 第二个 while(m.find())if(m.find())就可以,因为你做的是整句匹配,不存在多次匹配。

eulerlcs commented 7 years ago

@BradyWZH 升一下级,在原来的模板字符串前加一个左括号,你再运行一下你的程序,看看还能得到想要的结果吗?

问题

模板字符串:(【工银信用卡】于${startTime}至${endTime}申办奋斗卡,无年费,赢郎平签名排球!详情${link} 文本字符串:(【工银信用卡】于昨天至今天申办奋斗卡,无年费,赢郎平签名排球!详情没有

BradyWZH commented 7 years ago

@eulerlcs 谢谢大佬指出问题,加上左括号的情况下程序会报错,原因应该是当把模板字符串替换成正则表达式之后,原来文本中的特殊符号需要加转义字符,否则就会出现错误,我去想想怎么改会更好些^_^

BradyWZH commented 7 years ago

@eulerlcs 换了一种思路,虽然解决了问题,但是感觉似乎过于复杂了,还请各位大佬给指点指点。

`

public void test() {
    String tempStr = "(【工银信用卡】于${startTime}至${endTime}申办奋斗卡,无年费,赢郎平签名排球!详情${link}";
    String textStr = "(【工银信用卡】于昨天至今天申办奋斗卡,无年费,赢郎平签名排球!详情没有";
    Map<String, String> map = templateReplace1(tempStr, textStr);
    // 遍历map
    for (Map.Entry<String, String> entry : map.entrySet()) {
        System.out.println(entry.getKey() + " -- " + entry.getValue());
    }
}
public Map<String, String> templateReplace1(String tempStr, String textStr) {
    Map<String, String> map = new HashMap<>();
    // 找出匹配的占位符
    List<String> tempList = findAll("\\$\\{.+?\\}", tempStr);
    // 相当于是一个唯一的符号,也可用其他替代
    Long currentTime = System.currentTimeMillis();
    // 必须保证字符串中不能存在
    while(tempStr.contains((++currentTime).toString()));
    tempStr = tempStr.replaceAll("\\$\\{.+?\\}", currentTime.toString());
    // 【工银信用卡】于、至、申办奋斗卡,无年费,赢郎平签名排球!详情   等替换成唯一字符
    for (String str : tempStr.split(currentTime.toString())){
        if ("".equals(str)) continue ;
        textStr = textStr.replace(str, currentTime.toString());
    }
    String textArr[] = textStr.split(currentTime.toString());
    int j = 0;
    // 由于String.split()方法的原因,得到数组的第一个可能是空字符串,即:""
    if ("".equals(textArr[j])) j++;
    for (int i = 0; i < tempList.size(); i++, j++)
        map.put(tempList.get(i), textArr[j]);
    return map;
}
public List<String> findAll(String regex, String replacement){
    List<String> list = new ArrayList<>();
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(replacement);
    // 找到占位符,并将其添加到list中
    while (m.find()) {
        list.add(m.group());
    }
    return list;
}

`

eulerlcs commented 7 years ago

@BradyWZH 你的想法很好。有一个小错误,有一地方应该用replaceFirst,你用了replace,导致下边这个情况求不出解

        tempStr = "a${startTime}b${endTime}";
        textStr = "abbc";

我把你的代码改了一下,你参考以下

@Test
public void test_BradyWZH() {
    String tempStr = null;
    String textStr = null;

    tempStr = "【工银信用卡】于${startTime}至${endTime}申办奋斗卡,无年费,赢郎平签名排球!详情${link}";
    textStr = "【工银信用卡】于昨天至今天申办奋斗卡,无年费,赢郎平签名排球!详情没有";

    tempStr = "a${startTime}b${endTime}";
    textStr = "abbc";

    tempStr = "(${startTime}至${endTime}";
    textStr = "(昨天至今天";

    tempStr = "a${startTime}b${endTime}";
    textStr = "abbc";

    // 模板字符串中占位符号(变量)的正则表达式
    String keyRegex = "\\$\\{.+?\\}";

    // 找出所有的占位符
    List<String> keyList = new ArrayList<>();
    {
        Pattern p = Pattern.compile(keyRegex);
        Matcher m = p.matcher(tempStr);

        while (m.find()) {
            keyList.add(m.group());
        }
    }

    // 找出一个字符串:模板字符串中不包含它,文本字符串中也不包含它
    String onlyStr = null;
    {
        // 相当于是一个唯一的符号,也可用其他替代
        Long currentTime = System.currentTimeMillis();
        // 必须保证字符串中不能存在
        while (tempStr.contains(currentTime.toString())) {
            currentTime++;
        }

        onlyStr = currentTime.toString();
    }

    // 找出模板字符串中,用占位符分割的字符串列表,也就是固定字符串的列表
    List<String> literalList = new ArrayList<>();
    {
        String temp = tempStr.replaceAll(keyRegex, onlyStr);

        String[] tempArray = temp.split(onlyStr);
        for (int i = 0; i < tempArray.length; i++) {
            if (i == 1 || i == tempArray.length - 1) {
                if ("".equals(tempArray[i])) {
                    continue;
                }
            }

            literalList.add(tempArray[i]);
        }
    }

    // 找出文本字符串中替换值列表
    List<String> valueList = new ArrayList<>();
    {
        String text = textStr;
        for (String literal : literalList) {
            text = text.replaceFirst(literal, onlyStr);
        }

        String valueArray[] = text.split(onlyStr);
        for (int i = 0; i < valueArray.length; i++) {
            if (i == 1 || i == valueArray.length - 1) {
                if ("".equals(valueArray[i])) {
                    continue;
                }
            }

            valueList.add(valueArray[i]);
        }
    }

    // 输出结果
    if (valueList.isEmpty()) {
        System.out.println("the text file format is not correct");
    } else {
        for (int i = 0; i < keyList.size(); i++) {
            System.out.println(keyList.get(i) + "\t\t" + valueList.get(i));
        }
    }
}
eulerlcs commented 7 years ago

我的解法

https://github.com/eulerlcs/eulerlcs.core/wiki/%E5%A6%82%E4%BD%95%E7%94%A8%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E6%A8%A1%E6%9D%BF%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%9B%BF%E6%8D%A2%E5%89%8D%E5%90%8E%E7%9A%84%E9%94%AE%E5%80%BC%E5%AF%B9

BradyWZH commented 7 years ago

@eulerlcs 明白了很重要的一点,就是原来普通字符加不加\,它的意思并不会改变,比如说:\:,\~,\中 等等,匹配的还是 :、~、中这些字符本身,之前以为加了就变了,所以就觉得第一种想法判断起来太麻烦[笑哭];第二份代码确实有考虑不周的地方,忽略了一些特殊情况,非常感谢大佬的指点。