【问题标题】:Replacing variables in a string替换字符串中的变量
【发布时间】:2019-04-25 17:07:13
【问题描述】:

我正在使用 PHP 开发一个多语言网站,在我的语言文件中,我经常有包含多个变量的字符串,这些变量稍后将被填写以完成句子。

目前我将{VAR_NAME} 放在字符串中,并在使用时手动将每个出现的地方替换为其匹配值。

所以基本上:

{X} created a thread on {Y}

变成:

Dany created a thread on Stack Overflow

我已经想到了sprintf,但我觉得它很不方便,因为它取决于变量的顺序,可以从一种语言更改为另一种语言。

我已经检查了How replace variable in string with value in php?,现在我基本上使用这种方法。

但我很想知道 PHP 中是否有内置的(或者可能没有)方便的方法来做到这一点,因为在前面的示例中我已经有完全命名为 X 和 Y 的变量,更像是 $$ for一个变量变量。

所以我可能会调用这样的函数,而不是对字符串执行 str_replace :

$X = 'Dany';
$Y = 'Stack Overflow';
$lang['example'] = '{X} created a thread on {Y}';

echo parse($lang['example']);

也会打印出来:

Dany created a thread on Stack Overflow

谢谢!

编辑

字符串用作模板,可以多次使用不同的输入。

所以基本上执行"{$X} ... {$Y}" 不会成功,因为我会丢失模板,并且字符串将使用尚未确定的$X$Y 的起始值进行初始化。

【问题讨论】:

标签: php variables


【解决方案1】:

我将在这里添加一个答案,因为在我看来,当前的答案都没有真正切中要害。我将直接潜入并向您展示我将用于执行此操作的代码:

function parse(
    /* string */ $subject,
    array        $variables,
    /* string */ $escapeChar = '@',
    /* string */ $errPlaceholder = null
) {
    $esc = preg_quote($escapeChar);
    $expr = "/
        $esc$esc(?=$esc*+{)
      | $esc{
      | {(\w+)}
    /x";

    $callback = function($match) use($variables, $escapeChar, $errPlaceholder) {
        switch ($match[0]) {
            case $escapeChar . $escapeChar:
                return $escapeChar;

            case $escapeChar . '{':
                return '{';

            default:
                if (isset($variables[$match[1]])) {
                    return $variables[$match[1]];
                }

                return isset($errPlaceholder) ? $errPlaceholder : $match[0];
        }
    };

    return preg_replace_callback($expr, $callback, $subject);
}

这是做什么的?

简而言之:

  • 使用指定的转义字符创建一个正则表达式,该字符将匹配三个序列之一(更多内容见下文)
  • 将其输入preg_replace_callback(),回调会在其中准确处理其中两个序列,并将其他所有内容视为替换操作。
  • 返回结果字符串

正则表达式

正则表达式匹配以下三个序列中的任何一个:

  • 出现两次转义字符,后跟出现零次或多次转义字符,然后是左大括号。只有前两次出现的转义字符被消耗。这将由一次出现的转义字符替换。
  • 单次出现的转义字符后跟左大括号。这被一个字面的开放花括号所取代。
  • 一个左大括号,后跟一个或多个 perl 单词字符(字母数字和下划线字符),后跟一个右大括号。这被视为占位符,并在 $variables 数组中的大括号之间的名称执行查找,如果找到则返回替换值,如果没有则返回 $errPlaceholder 的值 - 默认为 @ 987654334@,作为特例处理,返回原始占位符(即不修改字符串)。

为什么更好?

要了解为什么它更好,让我们看看其他答案所采用的替代方法。对于one exception(唯一的缺点是与 PHP

  • strtr() - 这没有提供处理转义字符的机制。如果您的输入字符串需要文字 {X} 怎么办? strtr() 不考虑这一点,它将被值 $X 替换。
  • str_replace() - 这与strtr() 存在相同的问题,以及另一个问题。当您使用搜索/替换参数的数组参数调用str_replace() 时,它的行为就像您多次调用它一样 - 每个替换对数组中的一个。这意味着,如果您的替换字符串之一包含稍后出现在搜索数组中的值,您最终也会替换它。

要使用str_replace() 演示此问题,请考虑以下代码:

$pairs = array('A' => 'B', 'B' => 'C');
echo str_replace(array_keys($pairs), array_values($pairs), 'AB');

现在,您可能希望这里的输出是 BC,但实际上是 CC (demo) - 这是因为第一次迭代将 A 替换为 B,并且在第二次迭代,主题字符串是BB - 所以B 的这两次出现都被C 替换。

这个问题还暴露了一个可能不会立即明显的性能考虑 - 因为每一对都是单独处理的,操作是 O(n),对于每个替换对,整个字符串都被搜索并处理单个替换操作。如果您有一个非常大的主题字符串和很多替换对,那么这就是在引擎盖下进行的一项相当大的操作。

可以说,这种性能考虑不是问题 - 您需要一个 very 大字符串和一个 lot 替换对,然后才能获得有意义的减速,但它仍然存在值得记住。还值得记住的是,正则表达式有其自身的性能损失,因此一般而言,这种考虑不应包含在决策过程中。

我们使用preg_replace_callback()。这将访问字符串的任何给定部分,在提供的正则表达式的范围内只查找一次匹配。我添加了这个限定符,因为如果你编写一个导致catastrophic backtracking 的表达式,那么它将不止一次,但在这种情况下这不应该是一个问题(为了帮助避免这种情况,我在表达式@987654327 中做了唯一的重复@)。

我们使用preg_replace_callback() 而不是preg_replace() 来允许我们在查找替换字符串时应用自定义逻辑。

这可以让你做什么

问题中的原始示例

$X = 'Dany';
$Y = 'Stack Overflow';
$lang['example'] = '{X} created a thread on {Y}';

echo parse($lang['example']);

这就变成了:

$pairs = array(
    'X' = 'Dany',
    'Y' = 'Stack Overflow',
);

$lang['example'] = '{X} created a thread on {Y}';

echo parse($lang['example'], $pairs);
// Dany created a thread on Stack Overflow

更高级的东西

现在假设我们有:

$lang['example'] = '{X} created a thread on {Y} and it contained {X}';
// Dany created a thread on Stack Overflow and it contained Dany

...我们希望第二个{X} 出现在结果字符串中字面意思。使用 @ 的默认转义字符,我们将其更改为:

$lang['example'] = '{X} created a thread on {Y} and it contained @{X}';
// Dany created a thread on Stack Overflow and it contained {X}

好的,到目前为止看起来不错。但是如果 @ 应该是文字呢?

$lang['example'] = '{X} created a thread on {Y} and it contained @@{X}';
// Dany created a thread on Stack Overflow and it contained @Dany

请注意,正则表达式被设计为仅关注紧接在左大括号之前的转义序列。这意味着您不需要转义转义字符,除非它立即出现在占位符前面。

关于使用数组作为参数的说明

您的原始代码示例使用的变量命名方式与字符串中的占位符相同。我的使用带有命名键的数组。这有两个很好的理由:

  1. 清晰性和安全性 - 更容易看到最终将被替换的内容,并且您不会冒着意外替换您不想暴露的变量的风险。如果有人可以简单地输入{dbPass} 并查看您的数据库密码,那不是很好,不是吗?
  2. 范围 - 除非调用者是全局范围,否则无法从调用范围导入变量。如果从另一个函数调用,这会使该函数无用,并且从另一个范围导入数据是非常糟糕的做法。

如果您真的想使用当前范围内的命名变量(由于上述安全问题,我确实推荐这样做),您可以传递调用 get_defined_vars() 到第二个参数。

关于选择转义字符的注意事项

您会注意到我选择了@ 作为默认转义字符。您可以通过将其传递给第三个参数来使用任何字符(或字符序列,它可以不止一个) - 您可能很想使用\,因为这是许多语言使用的,但在此之前坚持你这样做

您不想使用\ 的原因是因为许多语言都使用它作为自己的转义字符,这意味着当您想指定转义字符时,比如说, PHP字符串文字,你遇到了这个问题:

$lang['example'] = '\\{X}';   // results in {X}
$lang['example'] = '\\\{X}';  // results in \Dany
$lang['example'] = '\\\\{X}'; // results in \Dany

它可能导致可读性噩梦,以及一些具有复杂模式的不明显行为。选择一个不被任何其他语言使用的转义字符(例如,如果您使用此技术生成 HTML 片段,也不要使用 & 作为转义字符)。

总结一下

您正在做的事情有极端情况。要正确解决问题,您需要使用能够处理这些极端情况的工具——当涉及到字符串操作时,最常用的工具是正则表达式。

【讨论】:

  • 优秀的答案,包含我正在寻找的所有详细解释,感谢您分享您的经验,尤其是关于为什么它更好的部分,我非常感谢您花时间写这篇文章:)跨度>
  • @DanyKhalife 没问题,很乐意提供帮助 :-)
  • 这似乎只是替换字符串...恕我直言
  • @t1gor 如果您只想替换字符串,请使用str_replace()。但是如果你想要一个合适的模板系统,那么str_replace() 就无法处理太多的边缘情况。
  • @DaveRandom 你很可能是对的。但如果我们谈论模板系统,我会考虑 OOP 概念和include()-ing 模板文件。我的项目中有类似的东西:bitbucket.org/t1gor/strategy/src/…
【解决方案2】:

这是一个使用可变变量的便携式解决方案。耶!

$string = "I need to replace {X} and {Y}";
$X = 'something';
$Y = 'something else';

preg_match_all('/\{(.*?)\}/', $string, $matches);           

foreach ($matches[1] as $value)
{
    $string = str_replace('{'.$value.'}', ${$value}, $string);
}

首先你设置你的字符串和你的替换。然后,您执行一个正则表达式来获取一个匹配数组({ 和 } 中的字符串,包括那些括号)。最后,您循环这些并使用变量变量将它们替换为您在上面创建的变量。可爱!


只是想我会用另一个选项来更新它,即使你已将它标记为正确。您没有使用可变变量,可以使用数组代替它。

$map = array(
    'X' => 'something',
    'Y' => 'something else'
);

preg_match_all('/\{(.*?)\}/', $string, $matches);           

foreach ($matches[1] as $value)
{
    $string = str_replace('{'.$value.'}', $map[$value], $string);
}

这将允许您创建具有以下签名的函数:

public function parse($string, $map); // Probably what I'd do tbh

感谢comments 中的toolmakersteve 的另一个选项消除了循环的需要并使用strtr,但需要对变量和单引号而不是双引号进行少量添加:

$string = 'I need to replace {$X} and {$Y}';

$map = array(
    '{$X}' => 'something',
    '{$Y}' => 'something else'
);

$string = strtr($string, $map);

【讨论】:

  • 非常感谢!我只是把它包装到一个类/函数中:) 考虑到你使用的是 RE,我应该对性能有任何顾虑吗?
  • 这是一个非常简单的正则表达式,我在这里看不到任何问题。如果您真的很担心,请运行一些性能测试,但没有必要进行微优化 - 会没事的。
  • 非常感谢,关于变量的范围也很好:D
  • 是的,我删除了它 - 这已经足够了 ;) 不想太深入 ;)
  • @zzzzBov 更新了另一个选项以防万一。您仍然需要显然添加检查以确保您要求的变量存在等......
【解决方案3】:

如果您运行的是 5.4 并且您关心能够在字符串中使用 PHP 的内置变量插值,则可以使用 ClosurebindTo() 方法,如下所示:

// Strings use interpolation, but have to return themselves from an anon func
$strings = [
    'en' => [
        'message_sent' => function() { return "You just sent a message to $this->recipient that said: $this->message."; }
    ],
    'es' => [
        'message_sent' => function() { return "Acabas de enviar un mensaje a $this->recipient que dijo: $this->message."; }
    ]
];

class LocalizationScope {
    private $data;

    public function __construct($data) {
        $this->data = $data;
    }

    public function __get($param) {
        if(isset($this->data[$param])) {
            return $this->data[$param];
        }

        return '';
    }
}

// Bind the string anon func to an object of the array data passed in and invoke (returns string)
function localize($stringCb, $data) {
    return $stringCb->bindTo(new LocalizationScope($data))->__invoke();
}

// Demo
foreach($strings as $str) {
    var_dump(localize($str['message_sent'], array(
        'recipient' => 'Jeff Atwood',
        'message' => 'The project should be done in 6 to 8 weeks.'
    )));
}

//string(93) "You just sent a message to Jeff Atwood that said: The project should be done in 6 to 8 weeks."
//string(95) "Acabas de enviar un mensaje a Jeff Atwood que dijo: The project should be done in 6 to 8 weeks."

(Codepad Demo)

也许,这感觉有点hacky,我不太喜欢在这种情况下使用$this。但是你确实获得了依赖 PHP 的变量插值的额外好处(它允许你做一些事情,比如转义,这是用正则表达式很难实现的)。


编辑:添加了LocalizationScope,这增加了另一个好处:如果本地化匿名函数尝试访问未提供的数据,则不会发出警告。

【讨论】:

  • 很好的答案!这正是我正在寻找的,但不幸的是我正在运行 5.3 :( 这就是为什么我不会将您的答案标记为所选答案的原因 :)
  • 这实际上非常聪明(+1),但我花了一分钟左右的时间阅读它,然后我才能真正看到它在做什么(主要是因为你提到的$this 问题你自己)。我会以不明显的行为为由避开这个问题,但同时这在机制上是解决问题的一个很好且非常灵活的解决方案。
  • @DaveRandom 谢谢!我同意$this 问题。当我第一次编写代码时,我省略了它(假设一个更多的 javascript-esque 范围绑定)并很快发现它不起作用。如果有办法后期绑定use 变量,这可能更直观,但现在我完全同意$this 的非显而易见性。
【解决方案4】:

strtr 可能是这类事情的更好选择,因为它首先替换最长的键:

$repls = array(
  'X' => 'Dany',
  'Y' => 'Stack Overflow',
);

foreach($data as $key => $value)
  $repls['{' . $key . '}'] = $value;

$result = strtr($text, $repls);

(想想你有像 XX 和 X 这样的键的情况)


如果您不想使用数组而是公开当前范围内的所有变量:

$repls = get_defined_vars();

【讨论】:

    【解决方案5】:

    如果您对 sprintf 的唯一问题是参数的顺序,您可以使用参数交换。

    来自文档 (http://php.net/manual/en/function.sprintf.php):

    $format = 'The %2$s contains %1$d monkeys';
    echo sprintf($format, $num, $location);
    

    【讨论】:

    • 为什么这被否决了?对我来说似乎是显而易见的答案。虽然顺序可能因语言而异,但您在编写模板字符串时知道顺序。要使用 OP 示例“%1$s 在 %2$s 上创建了一个线程”或“%2$s 有 %1$s 的新帖子”或 sprintf($template_string, 'Dany', '堆栈溢出')。这正是有多少系统解决翻译问题(例如,如果您需要字符串中的参数,Wordpress 使用 gettext 并建议以这种方式通过 printf 传递您的 gettext 字符串)codex.wordpress.org/I18n_for_WordPress_Developers#Placeholders
    • 好的。我看到OP希望变量完全如此命名。由于无论如何都需要为每个字符串记录,我不相信这是值得的。既然他还说他考虑过 sprintf 但由于订购问题而打折了,所以我认为这仍然是一个有效的答案
    【解决方案6】:

    gettext 是一种广泛使用的通用本地化系统,可以完全满足您的需求。 大多数编程语言都有库,PHP 有一个内置引擎。 它由 po-files 驱动,基于简单文本的格式,周围有许多编辑器,它与 sprintf 语法兼容。

    它甚至还有一些功能可以处理某些语言所具有的复杂复数。

    这里有一些例子来说明它的作用。注意 _() 是 gettext() 的别名:

    • echo _('Hello world'); // 将以当前选择的语言输出 hello world
    • echo sprintf(_("%s has created a thread on %s"), $name, $site); // 翻译字符串,并将其交给 sprintf()
    • echo sprintf(_("%2$s has created a thread on %1$s"), $site, $name); // 同上,只是改变了参数的顺序。

    如果您有多个字符串,则绝对应该使用现有的引擎,而不是编写自己的引擎。 添加新语言只需翻译一个字符串列表,大多数专业翻译工具也可以使用这种文件格式。

    查看 Wikipedia 和 PHP 文档以了解其工作原理的基本概述:

    Google 发现大量文档,而您最喜欢的软件存储库很可能拥有一些用于管理 po-files 的工具。

    我用过的有:

    • poedit:非常轻巧简单。如果您没有太多要翻译的内容并且不想花时间思考这些内容是如何工作的,那就太好了。
    • Virtaal:有点复杂,有一点学习曲线,但也有一些不错的功能,可以让您的生活更轻松。如果您需要大量翻译,那就太好了。
    • GlotPress 是一个网络应用程序(来自 wordpress 人),它允许对翻译数据库文件进行协作编辑。

    【讨论】:

    • 与 xml 相比,gettext 最好吗?它是否向服务器发送请求以翻译每个 msgid?
    • 不,它与 XML 没有任何共同之处。它不会在运行时发送任何请求。它几乎是为每种语言设置的字符串集合。
    【解决方案7】:

    那为什么不使用 str_replace 呢?如果你想要它作为模板。

    echo str_replace(array('{X}', '{Y}'), array($X, $Y), $lang['example']);
    

    对于您需要的每一次出现这种情况

    str_replace 最初就是为此而构建的。

    【讨论】:

    • 我只是在寻找更便携的解决方案
    • 更便携是什么意思,str_replace 适用于所有运行 php 的操作系统?
    • 不明白你所说的更便携是什么意思?
    • 我的意思是它不需要在每次调用时都传递所有 3 个参数,只需要 1 个字符串
    【解决方案8】:

    如何将“变量”部分定义为一个数组,其中的键对应于字符串中的占位符?

    $string = "{X} created a thread on {Y}";
    $values = array(
       'X' => "Danny",
       'Y' => "Stack Overflow",
    );
    
    echo str_replace(
       array_map(function($v) { return '{'.$v.'}'; }, array_keys($values)),
       array_values($values),
       $string
    );
    

    【讨论】:

    • 你的 array_map 肯定引起了我的注意 :) 但我想等一下,看看是否有人有办法做到这一点,而不需要在每次调用时传递变量数组
    【解决方案9】:

    为什么不能只在函数中使用模板字符串?

    function threadTemplate($x, $y) {
        return "{$x} created a thread on {$y}";
    }
    echo threadTemplate($foo, $bar);
    

    【讨论】:

    • 因为我必须为每个模板创建一个函数,这很快就会失控:)
    【解决方案10】:

    简单:

    $X = 'Dany';
    $Y = 'Stack Overflow';
    $lang['example'] = "{$X} created a thread on {$Y}";
    

    因此:

    echo $lang['example'];
    

    将输出:

    Dany created a thread on Stack Overflow
    

    如您所愿。

    更新:

    根据 OP 关于使解决方案更便携的 cmets:

    每次都有一个班级为你做解析:

    class MyParser {
      function parse($vstr) {
        return "{$x} created a thread on {$y}";
      }
    }
    

    这样,如果出现以下情况:

    $X = 3;
    $Y = 4;
    
    $a = new MyParser();
    $lang['example'] = $a->parse($X, $Y);
    
    echo $lang['example'];
    

    哪个会返回:

    3 created a thread on 4;
    

    而且,双重检查:

    $X = 'Steve';
    $Y = 10.9;
    
    $lang['example'] = $a->parse($X, $Y);
    

    将打印:

    Steve created a thread on 10.9;
    

    根据需要。

    更新 2:

    根据 OP 关于提高可移植性的 cmets:

    class MyParser {
      function parse($vstr) {
        return "{$vstr}";
      }
    }
    
    $a = new MyParser();
    
    $X = 3;
    $Y = 4;
    $vstr = "{$X} created a thread on {$Y}";
    
    $a = new MyParser();
    $lang['example'] = $a->parse($vstr);
    
    echo $lang['example'];
    

    将输出之前引用的结果。

    【讨论】:

    • 请查看我对 RiggsFolly 回答的评论
    • 有趣!一个缺点,据我所见,这需要我为每个包含变量的模板创建一个方法
    • @DanyKhalife:不一定。我再次更新了我的答案,以反映虽然我的实现很幼稚,但您可以概括它。
    • 是的,我明白你的意思 :)
    【解决方案11】:

    试试

    $lang['example'] = "$X created a thread on $Y";
    

    编辑:基于最新信息

    也许你需要看看 sprintf() 函数

    然后你可以将你的模板字符串定义为这个

    $template_string = '%s created a thread on %s';
    
    
    $X = 'Fred';
    $Y = 'Sunday';
    
    echo sprintf( $template_string, $X, $Y );
    

    $template_string 不会更改,但稍后在您为 $X$Y 分配不同的值时,您仍然可以使用 echo sprintf( $template_string, $X, $Y );

    See PHP Manual

    【讨论】:

    • 对不起,我忘了提到 $X 和 $Y 在初始化这个字符串时是未知的
    • 为了更清楚:这基本上用 X 和 Y 的当前值初始化字符串,但如果我想重用这个字符串模板,我不能......
    • 您可以为$_SESSION 变量设置条件吗?
    • 还是解决不了问题。为了说明我的观点,让我们说$X = 1, $Y = 2; echo $lang['example']; 然后$X = 3, $Y = 4; echo $lang['example'];。这两个echos 不应该相同,因为 $X 和 $Y 改变了值(这就是为什么我说我可能需要在每次回显之前调用这个字符串上的一个函数)。
    • 感谢您的支持,但据我所知,sprintf 要求变量按特定顺序排列,这在切换语言时我不确定:)
    【解决方案12】:

    只是在使用关联数组时抛出另一个解决方案。这将遍历关联数组并替换模板或将其留空。

    示例:

    $list = array();
    $list['X'] = 'Dany';
    $list['Y'] = 'Stack Overflow';
    
    $str = '{X} created a thread on {Y}';
    
    $newstring = textReplaceContent($str,$list);
    
    
        function textReplaceContent($contents, $list) {
    
    
                    while (list($key, $val) = each($list)) {
                        $key = "{" . $key . "}";
                        if ($val) {
                            $contents = str_replace($key, $val, $contents);
                        } else {
                            $contents = str_replace($key, "", $contents);
                        }
                    }
                    $final = preg_replace('/\[\w+\]/', '', $contents);
    
                    return ($final);
                }
    

    【讨论】:

    • Zzzz Zzzzz Zzzzz zzZZzzzz :)
    猜你喜欢
    • 2020-02-04
    • 2013-02-16
    • 1970-01-01
    • 2020-07-09
    • 2020-03-30
    • 1970-01-01
    • 2011-08-18
    • 1970-01-01
    相关资源
    最近更新 更多