【问题标题】:An efficient and effective way to replace values in a string based on the context in PHPPHP中基于上下文替换字符串中值的有效方法
【发布时间】:2016-09-08 13:16:11
【问题描述】:

我正在处理一些我想在实际使用它们之前修改的 XPath 字符串。对于那些不熟悉 XPath 的人来说,XPath 是——简而言之——一种类似于 XML 结构的方式,它经常被用作基于 XPath/XQuery 的搜索引擎的正式输入。

目标

要查看下面 XPath sn-ps 的扩展/美化版本,我可以将您定向到 the following beautifier。免责声明,我是该工具的作者。

我的 XPath 字符串可以很简单

//node[@cat="smain" and node[@rel="su" and @pt="vnw"] and node[@rel="hd" and @pt="ww"] and node[@rel="predc" and @cat="np" and node[@rel="det" and @pt="lid"] and node[@rel="hd" and @pt="n"]]] 

但也很精致

//node[@cat="top" and node[@rel="--" and @cat="smain" and node[@rel="su" and @pt="vnw" and @word="Dit" and @lemma="dit" and number(@begin) < ../node[@rel="hd" and @pt="ww" and @lemma="zijn"]/number(@begin)] and node[@rel="hd" and @pt="ww" and @lemma="zijn" and number(@begin) < ../node[@rel="predc" and @cat="np"]/node[@rel="det" and @pt="lid" and @word="een" and @cs="no" and @lemma="een"]/number(@begin)] and node[@rel="predc" and @cat="np" and node[@rel="det" and @pt="lid" and @word="een" and @cs="no" and @lemma="een" and number(@begin) < ../node[@rel="hd" and @pt="n" and @cs="no" and @lemma="zin"]/number(@begin)] and node[@rel="hd" and @pt="n" and @cs="no" and @lemma="zin" and number(@begin) < ../../../node[@rel="--" and @pt="let"]/number(@begin)]]] and node[@rel="--" and @pt="let"]]

您可能已经注意到,node 是使用的基本元素。没有其他元素名称。但是,属性不同。我感兴趣的属性是@cs="no",这意味着在未来对属性@word 和/或@lemma 的搜索请求中不需要区分大小写。为了实现区分大小写,我想将transform these two attributes 转换为lower-case(@attr)。问题是我只想要包含 @cs="no" 的节点。

到目前为止我尝试了什么

在 PHP 中,我认为我会成为一个聪明的人并做这样的事情:

  1. 检查 (XPath) 字符串是否匹配 @cs="no"
  2. 如果有,用正则表达式查找所有单个节点

    preg_match_all("/(?<=node\[).*?(?=node\[|\])/", $xpath, $matches);
    
  3. 遍历所有这些匹配(字符串),并再次检查它们是否包含@cs="no"

  4. 如果是这样,请删除该属性,并将@word@lemma 标记替换为等效的小写字母。将结果放入虚拟变量中。

现在是棘手的部分:

  1. 在原始 XPath 字符串中,找到匹配的子字符串并将其替换为虚拟变量。

您可以在here 中看到这一点,但我也复制了下面的 PHP 代码。

  <?php
  $xpath = '//node[@cat="top" and node[@rel="--" and @cat="smain" and node[@rel="su" and @pt="vnw" and @word="Dit" and @lemma="dit" and number(@begin) < ../node[@rel="hd" and @pt="ww" and @lemma="zijn"]/number(@begin)] and node[@rel="hd" and @pt="ww" and @lemma="zijn" and number(@begin) < ../node[@rel="predc" and @cat="np"]/node[@rel="det" and @pt="lid" and @word="een" and @cs="no" and @lemma="een"]/number(@begin)] and node[@rel="predc" and @cat="np" and node[@rel="det" and @pt="lid" and @word="een" and @cs="no" and @lemma="een" and number(@begin) < ../node[@rel="hd" and @pt="n" and @cs="no" and @lemma="zin"]/number(@begin)] and node[@rel="hd" and @pt="n" and @cs="no" and @lemma="zin" and number(@begin) < ../../../node[@rel="--" and @pt="let"]/number(@begin)]]] and node[@rel="--" and @pt="let"]]';
  $xpath = applyCs($xpath);

  var_dump($xpath);

  function applyCs($xpath) {
    if (strpos($xpath, '@cs="no"') !== false) {
      preg_match_all("/(?<=node\[).*?(?=node\[|\])/", $xpath, $matches);
      foreach ($matches as $match) {
        var_dump($match);
        if (strpos($match, '@cs="no"') !== false) {
          $dummyMatch = preg_replace('/(?:and )?@cs="no"/', '', $match);

            if (strpos($dummyMatch, '@word="') !== false) {
                $dummyMatch = str_replace('@word="', 'lower-case(@word)="', $dummyMatch);
            }
            if (strpos($dummyMatch, '@lemma="') !== false) {
                $dummyMatch = str_replace('@lemma="', 'lower-case(@lemma)="', $dummyMatch);
            }

            $xpath = str_replace($match, $dummyMatch, $xpath);
        }
      }
    }
    return $xpath;
  }

我的功能有问题

首先,您将在通过上面的链接提供的 Ideone 示例中看到,具有word 属性的第一个节点确实 具有@cs="no" 属性,但在生成的 XPath 中它确实得到lower-case()'d。其次,在示例中您可能看不到重现的内容:因为我只是用新的虚拟对象查找并替换旧匹配项,所以很可能我替换了原始 XPath 节点中没有 @ 的值987654342@ 属性可用。我显然不希望那样。最后我不确定这是最好的方法。效率对我来说很重要,因此我大多不喜欢使用正则表达式。这就是我尽可能多地使用strposstr_replace 的原因。但是,如果有一种“解析”XPath 的方法(类似于在 Perl 中使用 Twig 解析 XML 的方法),并以快速的方式相应地操作 XPath,那也很好。然而,效率高于效率。

Tl;dr:在 XPath 字符串中,如果使用 PHP 不使用其他模块将其姊妹属性设置为(特定值),我如何将一个属性替换为另一个字符串。

想法

  • 找到一个可以匹配每个节点而不会留下任何间隙的正则表达式,并在必要时编辑匹配后将所有节点重新粘合在一起
  • 使用 PREG_OFFSET_CAPTURE 在输入 XPath 中查找匹配的索引,然后以一种或另一种方式替换您从该索引获得的第一个匹配项。

【问题讨论】:

    标签: php regex xpath replace


    【解决方案1】:

    此解决方案适用于您当前和以后任何类似的 XPath 查询。不过,我不确定是否有任何失败案例。

    这个想法是提取node 声明,然后如果其中出现@cs="no",则执行查找/替换。

    Live demo

    echo preg_replace_callback('~node\[(?:[^[]+(?=\]|node))~', function($match) {
        if (strpos($match[0], '@cs="no"') !== false) {
            return preg_replace(
                ['/@(lemma|word)/', '/\s*and\s*@cs="no"/'],
                ['lower-case(@$1)', ''],
                $match[0]
            );
        }
        return $match[0];
    }, $xpathStr);
    

    【讨论】:

      【解决方案2】:

      知道了。

      首先,循环中有一个愚蠢的错误:我应该使用matches[0] 而不是matches。不过,神奇之处在于替换。我现在使用的是preg_replace,而不是字符串替换(这并没有让我很开心......由于它的可选参数,它允许我将我想要的替换限制为 1。因为匹配数组是从左侧构建的向右,我也可以假设替换会以正确的顺序发生。最终代码如下所示:

        function applyCs($xpath) {
          var_dump($xpath);
          if (strpos($xpath, '@cs="no"') !== false) {
            preg_match_all("/(?<=node\[).*?(?=node\[|\])/", $xpath, $matches);
            foreach ($matches[0] as $match) {
              if (strpos($match, '@cs="no"') !== false) {
                $dummyMatch = preg_replace('/(?: and )?@cs="no"/', '', $match);
      
                  if (strpos($dummyMatch, '@word="') !== false) {
                      $dummyMatch = str_replace('@word="', 'lower-case(@word)="', $dummyMatch);
                  }
                  if (strpos($dummyMatch, '@lemma="') !== false) {
                      $dummyMatch = str_replace('@lemma="', 'lower-case(@lemma)="', $dummyMatch);
                  }
      
                  $xpath = preg_replace('/'.preg_quote($match, '/').'/', $dummyMatch, $xpath, 1);
              }
            }
          }
          return $xpath;
        }
      

      我暂时搁置这个问题,以寻找更高效的解决方案

      【讨论】:

      • 不确定为什么有人会对此投反对票,因为它是我自己问题的解决方案并且它有效。
      猜你喜欢
      • 2016-08-31
      • 2012-01-05
      • 2012-10-26
      • 2016-10-28
      • 1970-01-01
      • 2018-03-28
      • 2011-07-25
      • 2021-09-18
      相关资源
      最近更新 更多