如何匹配不在字符串/注释中的模式


How to match a pattern which is NOT inside a string/comment?

环境:PHP 5.3

我正在尝试编写自己的查询参数替换方法。基本上我想采取这个:

select * from xxx where a=? and b>?

并将其转换为

select * from xxx where a=1 and b>2

当然,假设所有?参数的值都是已知的。好的,这有点简化了,但对于这个问题来说已经足够了。

所以,我需要做的是找到给定字符串中的所有?标记。很简单,对吧?但有一个问题:我不想找到字符串或注释中的标记。因此,在这个字符串中:

select *  -- I know * is bad, but just once can't hurt, right?
from xxx /* ? */ where a=? and b='Question?'

只应更换其中一个?标记。

我的直觉告诉我PHP的preg_replace()应该能够胜任任务。。。但是我的正则表达式知识使我无法构建适当的模式(我也可以"手动"解析它,但我担心性能会受到不适当的影响。

那么,这可以通过正则表达式快速完成吗(如果可以,模式是什么),还是应该逐个字符手动解析?

您可以尝试先删除注释中的所有问号并记住它们,然后在查询中放置占位符,然后通过preg_replace()解析查询,然后在注释中有占位符的地方插入问号。我指的是之类的东西

$matches = array();
preg_match_all('/'/'*.*?.*'*'//U', $query, $matches);
preg_replace('/'/'*.*?.*'*'//U', $arrayWithIndicesOfParameters, $query);
preg_replace(/*your replacement of parameters*/);
preg_replace($arrayWithIndicesOfParameters, $matches, $query); //str_replace should be sufficient here

这对正则表达式来说是个难题。解析器会更合适,但只要满足某些约束,正则表达式就可能正常工作。限制条件是:

  • 没有嵌套注释
  • 字符串中没有转义引号
  • 评论中没有单引号
  • 字符串中没有注释分隔符
  • 所有引号和注释都正确平衡
  • 您可以指定最大线路长度

如果是这样的话,那么你可以简单地寻找一个?,它是

  • 不在CCD_ 6之前(在同一行上)
  • 不跟在*/之后,除非首先出现/*并且
  • 后跟偶数个引号

假设最大线路长度为100,则会得到

$result = preg_replace(
    '%(?<!--.{0,100})    # Assert no -- preceding on this line
    '?                   # Match a ?
    (?!                  # Assert that it''s impossible to match...
     (?s:                #  (allowing the dot to match newlines here):
      (?!/'*)            #  (and making sure there is no intervening /*)
      .                  #  any character
     )*                  # zero or more times, if that string is followed by
     '*/                 # */
    )                    # End of lookahead
    (?=                  # Assert that it *is* possible to match
     (?:                 # the following regex:
      [^'']*''[^'']*''   #  a string containing exactly two quotes
     )*                  #  repeated zero or more times
     [^'']*              # followed by only non-quote characters
     $                   # until the end of the string.
    )                    # End of lookahead.
    %x', 
    'REPLACE', $subject);

其他人可能有一个更优雅的解决方案,但我的第一直觉是使用regex完全去除注释,然后进行模式匹配参数。

$expressions = array(
  "#/'*(.*)'*/#",
  "#[-]{2}(.*)''n#",
);
$query = preg_replace($expressions, "", $query);

我可能会这样做:

  • 为每个字符串语法和注释语法找到一个正则表达式
  • 将它们组合以匹配字符串语法、注释语法或"其他任何语法"
  • 对每个"其他任何"零件进行更换

下面是一个示例实现:

$escapeSequence = "(?:''''[0''"bnrtZ''''%_])";
$singleQuoted   = "'(?:[^''''']|{$escapeSequence}|'')*'";
$doubleQuoted   = "'"(?:[^'"'''']|{$escapeSequence}|'"'")*'"";
$string         = "(?:{$singleQuoted}|{$doubleQuoted})";
$lineEndComment   = "(?:#[^''r''n]*|--''s[^''r''n]*)";
$multiLineComment = "(?:'/''*(?:.|[''r''n])*?''*'/)";
$comment          = "(?:{$lineEndComment}|{$multiLineComment})";
$pattern = "/({$string}|{$comment})/";
$parts = preg_split($pattern, $query, -1, PREG_SPLIT_DELIM_CAPTURE);
for ($i=0, $n=count($parts)-1; $i<$n; $i+=2) {
    $part = $parts[$i]; // non-string, non-comment part
    // perform replacement of ?
}
$query = implode('', $parts);

这个模式可能不完整,但应该足以让人产生想法。