【问题标题】:Parse backslash escape with str_replace使用 str_replace 解析反斜杠转义
【发布时间】:2015-03-18 23:08:47
【问题描述】:

假设我有一个函数templateMap,它对于$array 的每个子数组,用来自该子数组的值替换给定$string 中的每个出现的@n(对于某些n),返回新的子数组的数组。还说我想让用户反斜杠 @ 符号,(这意味着允许 \ 也被反斜杠)。

例如:

function templateMap ($string, $array) {
    $newArray = array();
    foreach($array as $subArray) {
        foreach($subArray as $replacements) {
            ...
        }
    }
    return $newArray;
}


// for grouping mysql statements with parentheses
templateMap("(@)", array(" col1 < 5 && col2 > 6 ", " col3 < 3 || col4 > 7"));

这会产生

array("( col1 < 5 && col2 > 6 )", "( col3 < 3 || col4 > 7 )")

这是一个带有多个参数的更复杂的示例 - 可能不容易实现

templateMap("You can tweet @0 \@2 @1", array(
    array("Sarah", "ssarahtweetzz"),
    array("John", "jjohnsthetweetiest"),
    ...
));

/* output: 
array(
    "You can tweet Sarah @2 ssarahtweetzz",
    "You can tweet John @2 jjohnsthetweetiest"
)
*/

有没有办法通过一系列str_replace 调用来完成此操作? (与正则表达式或简单状态机相反。)

我想到的一件事是将出现的\@ 替换为当前字符串中找不到的一些奇异字符串,例如zzzzzz,但是当然,您必须检查该字符串是否为在给定的字符串中,并相应地修改它。

【问题讨论】:

    标签: php string replace escaping string-interpolation


    【解决方案1】:

    我认为仅限于使用 str_replace 时的主要问题是您几乎无法控制哪些字符串已被替换(因为所有匹配项都被一次替换)并且在选择占位符时需要特别小心\@ 转义序列。有可能两个插入的值结合起来会产生占位符字符串,因此当占位符替换被还原时会变成一个@ 字符。

    以下是尝试处理该问题的蛮力解决方案。它一次检查一个占位符与模板字符串、替换值和最终字符串,确保占位符不会出现在任何这些字符串中,并且最初为 \@ 引入的占位符数量与占位符的数量相匹配恢复。您可能希望设置一个最适合您的默认占位符而不是 xyz(如零字符或其他内容),以避免不必要的处理。

    可以使用两种替换模式(@@&lt;n&gt;)调用它,但目前它们不能混合使用。

    这不是我写过的最漂亮的代码,但考虑到str_replace 的约束,它仍然是我的尝试,我希望它可以对你有所帮助。

    function templateMap ($string, $array, $defaultPlaceholder = "xyz")
    {
        $newArray = array();
    
        // Create an array of the subject string and replacement arrays
        $knownStrings = array($string);
        foreach ($array as $subArray) {
            if (is_array($subArray)) {
                $knownStrings = array_merge($knownStrings, array_values($subArray));
            }
            else {
                $knownStrings[] = $subArray;
            }
        }
    
        $placeHolder = '';
    
        while (true) {
            if (!$placeHolder) {
                // This is the first try, so let's try the default placeholder
                $placeHolder = $defaultPlaceholder;
            }
            else {
                // We've been here before - we need to try another placeholder
                $placeHolder = uniqid('bs-placeholder-', true);
            }
    
            // Try to find a placeholder that does not appear in any of the strings
            foreach ($knownStrings as $knownString) {
                // Does $placeHolder exist in $knownString?
                str_replace($placeHolder, 'whatever', $knownString, $count);
                if ($count > 0) {
                    // Placeholder candidate was found in one of the strings
                    continue 2; // Start over
                }
            }
    
            // Will go for placeholder "$placeHolder"
            foreach ($array as $subArray) {
                $newString = $string;
    
                // Apply placeholder for \@ - remember number of replacements
                $newString = str_replace(
                    '\@', $placeHolder, $newString, $numberOfFirstReplacements
                );
    
                if (is_array($subArray)) {
                    // Make substitution on @<n>
                    for ($i = 0; $i <= 9; $i++) {
                        @$newString = str_replace("@$i", $subArray[$i], $newString);
                    }
                }
                else {
                    // Make substitution on @
                    @$newString = str_replace("@", $subArray, $newString);
                }
    
                // Revert placeholder for \@ - remember number of replacements
                $newString = str_replace(
                    $placeHolder, '@', $newString, $numberOfSecondReplacements
                );
    
                if ($numberOfFirstReplacements != $numberOfSecondReplacements) {
                    // Darn - value substitution caused used placeholder to appear,
                    // ruining our day - we need some other placeholder
                    $newArray = array();
                    continue 2;
                }
    
                // Looks promising
                $newArray[] = $newString;
            }
    
            // All is well that ends well
            break;
        }
        return $newArray;
    }
    
    $a = templateMap(
        "(@ and one escaped \@)",
        array(" col1 < 5 && col2 > 6", " col3 < 3 || col4 > 7")
    );
    print_r($a);
    
    $a = templateMap(
        "You can tweet @0 \@2 @1",
        array(
            array("Sarah", "ssarahtweetz"),
            array("John", "jjohnsthetweetiest"),
        )
    );
    print_r($a);
    

    输出:

    Array
    (
        [0] => ( col1 < 5 && col2 > 6 and one escaped @)
        [1] => ( col3 < 3 || col4 > 7 and one escaped @)
    )
    Array
    (
        [0] => You can tweet Sarah @2 ssarahtweetz
        [1] => You can tweet John @2 jjohnsthetweetiest
    )
    

    【讨论】:

      【解决方案2】:

      在进行替换时,除了需要替换的那些之外,不能有任何@s...所以我们必须摆脱所有\@ 序列。 但是当我们去掉所有的\@ 序列时,就不可能有任何\@ 实际上是\\@(两个反斜杠后跟@)序列的一部分。 为了摆脱\\ 序列,我们可以使用新的转义字符%

      具体来说,如果我们将% 转义为%%,那么我们可以将任何其他序列转义为?%?,其中? 是任何字符,并保证?%? 可以被反转义,因为@ 987654334@永远不会单独出现在中间。

      // wrapper for native strings to make chaining easier
      class String {
          private $str;
          public function __construct ($str) {
              $this->str = $str;
          }
          public function replace ($search, $substitute) {
              return new self(str_replace($search, $substitute, $this->str));
          }
          public function toRaw () {
              return $this->str;
          }
      }
      
      function templateMap ($str, $arr) {
          $encodedStr = (new String($str))->replace('%', '%%')
              ->replace('\\\\', '?%?')->replace('\@', '!%!');
          $newArr = array();
          foreach($arr as $el) {
              $encodedStrPieces = explode("@", $encodedStr->toRaw());
              foreach($encodedStrPieces as $i => $piece) {
                  $encodedStrPieces[$i] = (new String($piece))->replace("@", $el)
                  ->replace('!%!', '@')->replace('?%?', '\\')
                  ->replace('%%', '%')->toRaw();
              }
              $newArr[] = implode($el, $encodedStrPieces);
          }
          return $newArr;
      }
      
      
      $arr = templateMap("(@\@)", array("hello", "goodbye"));
      var_dump($arr); // => ["(hello@)", "(goodbye@)"]
      

      【讨论】:

      • 我不知道这是否是 OP 的问题,但此解决方案不会保留双百分号。对于$arr = templateMap("(@\@)", array("hel%%o"));,它将产生(hel%o@) 而不是(hel%%o@)
      • @mhall,1)感谢您在上面的贡献,我有兴趣阅读它,2)(我是 OP),3)很好 - 我已经修改了代码在“@”上拆分,然后在替换之前解码。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-19
      • 1970-01-01
      • 2016-11-27
      • 2015-09-21
      • 2011-11-09
      相关资源
      最近更新 更多