替换字符串中的变量

Replacing variables in a string

提问人:Dany Khalife 提问时间:8/13/2013 最后编辑:CommunityDany Khalife 更新时间:4/25/2019 访问量:5209

问:

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

目前,我正在放入字符串中,并在使用时手动将每个匹配项替换为其匹配值。{VAR_NAME}

所以基本上:

{X} created a thread on {Y}

成为:

Dany created a thread on Stack Overflow

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

我已经检查过如何在 php 中将字符串中的变量替换为值? 现在我基本上使用这种方法。

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

因此,与其对字符串进行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 变量

评论

0赞 ToolmakerSteve 4/25/2019
另请参阅此答案中的第二个选项,该选项使用 strtr,以避免需要循环。[稍后,该链接已添加到此问题中提到的链接的已接受答案中。

答:

0赞 jrd1 8/13/2013 #1

简单:

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

因此:

echo $lang['example'];

将输出:

Dany created a thread on Stack Overflow

按照您的要求。

更新:

根据 OP 关于使解决方案更具可移植性的评论:

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

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 关于提高可移植性的评论:

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'];

将输出前面引用的结果。

评论

0赞 Dany Khalife 8/13/2013
请查看我对 RiggsFolly 答案的评论
0赞 Dany Khalife 8/13/2013
有趣!但有一个缺点,据我所知,这需要我为每个包含变量的模板创建一个方法
1赞 jrd1 8/13/2013
@DanyKhalife:不一定。我再次更新了我的答案,以反映虽然我的实现很幼稚,但您可以概括它。
0赞 RiggsFolly 8/13/2013 #2

尝试

$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$Yecho sprintf( $template_string, $X, $Y );

参见PHP手册

评论

0赞 Dany Khalife 8/13/2013
对不起,我忘了提到$X和$Y在初始化此字符串时是未知的
0赞 Dany Khalife 8/13/2013
更清楚地说:这基本上是使用 X 和 Y 的当前值初始化字符串,但如果我想重用这个字符串模板,我不能......
0赞 pulsar 8/13/2013
你能为变量设置一个条件吗?$_SESSION
0赞 Dany Khalife 8/13/2013
它仍然无法解决问题。为了说明我的观点,让我们说 然后 .这两个 s 不应该相同,因为 $X 和 $Y 的值发生了变化(这就是为什么我说我可能需要在每次回显之前调用这个字符串上的函数)。$X = 1, $Y = 2; echo $lang['example'];$X = 3, $Y = 4; echo $lang['example'];echo
0赞 Dany Khalife 8/13/2013
感谢您的支持,但据我所知,Sprintf 要求变量按特定顺序排列,这在切换语言时我不确定:)
1赞 Slobodan Antonijević 8/13/2013 #3

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

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

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

str_replace 最初是为此而建造的。

评论

0赞 Dany Khalife 8/13/2013
我只是在寻找一种更便携的解决方案
0赞 bumperbox 8/13/2013
更便携,str_replace适用于php运行的所有操作系统是什么意思?
0赞 Slobodan Antonijević 8/13/2013
不确定我是否理解您所说的更便携是什么意思?
0赞 Dany Khalife 8/13/2013
我的意思是它不需要在每次调用时传递所有 3 个参数,只需要 1 个参数,即字符串
0赞 lafor 8/13/2013 #4

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

$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
);

评论

0赞 Dany Khalife 8/13/2013
你肯定引起了我的注意,你的array_map :)但我想稍等片刻,看看是否有人有办法做到这一点,而不需要在每次调用时传递变量数组
12赞 Jimbo 8/13/2013 #5

这是一个使用变量的可移植解决方案。耶!

$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

由于 toolmakersteve 在注释中提供了另一个选项,它消除了对循环的需求,并使用了 strtr,但需要对变量和单引号而不是双引号进行少量添加:

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

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

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

评论

0赞 Dany Khalife 8/13/2013
完美谢谢!我只是把它包装成一个类/函数:)考虑到您使用的是 RE,我是否应该担心性能方面的问题?
0赞 Jimbo 8/13/2013
这是一个非常简单的正则表达式,我在这里看不到任何问题。如果你真的很担心,可以运行一些性能测试,但微优化是没有意义的——没关系。
0赞 Dany Khalife 8/13/2013
非常感谢,关于变量范围的好点:D
0赞 Jimbo 8/13/2013
是的,我删除了它 - 这已经足够好了;)不想太深入;)
1赞 Jimbo 8/13/2013
@zzzzBov 更新了另一个选项,以防万一。显然,您仍然需要添加检查以确保您要求的变量存在等......
2赞 nice ass 8/13/2013 #6

对于这种事情,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();
0赞 zzzzBov 8/13/2013 #7

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

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

评论

0赞 Dany Khalife 8/13/2013
因为我必须为每个模板创建一个函数,所以这将立即失控:)
4赞 Bailey Parker 8/13/2013 #8

如果你运行的是 5.4 版本,并且你关心能够在字符串中使用 PHP 的内置变量插值,你可以使用如下方法:bindTo()Closure

// 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 演示)

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


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

评论

1赞 Dany Khalife 8/13/2013
很好的答案!这正是我想要的,但不幸的是,我正在运行 5.3 :(所以这就是为什么我不会把你的答案标记为被选中的原因:)
1赞 DaveRandom 8/14/2013
这实际上非常聪明(+1),但是在我真正看到它在做什么之前,我花了一分钟左右的时间阅读它(主要是因为你自己提到的问题)。我会以不明显的行为为由避开这一点,但与此同时,这在机械上是解决问题的一个很好的、非常灵活的解决方案。$this
0赞 Bailey Parker 8/17/2013
@DaveRandom谢谢!我同意这个问题。当我第一次编写代码时,我省略了它(假设一个更像 javascript 式的范围绑定),很快就发现这不起作用。如果有一种方法可以延迟绑定变量,这可能会更直观,但现在我完全同意 .$thisuse$this
45赞 DaveRandom 8/13/2013 #9

我将在这里添加一个答案,因为在我看来,当前的答案都没有真正削减芥末。我将直接深入研究并向您展示我将用于执行此操作的代码:

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$errPlaceholdernull

为什么更好?

为了理解为什么它更好,让我们看看其他答案所采取的替代方法。除了一个例外(唯一的缺点是与 PHP<5.4 的兼容性和略微不明显的行为),它们分为两类:

  • 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');

现在,您可能期望这里的输出是,但实际上它是(演示) - 这是因为第一次迭代替换为 ,而在第二次迭代中主题字符串是 - 因此这两个出现的 都被替换为 。BCCCABBBBC

此问题还暴露了性能方面的考虑,这可能不会立即显现出来 - 因为每对都是单独处理的,所以操作是 ,对于每个替换对,将搜索整个字符串并处理单个替换操作。如果你有一个非常大的主题字符串和大量的替换对,那么在引擎盖下进行的一个相当大的操作。O(n)

可以说,这种性能考虑不是问题 - 在获得有意义的减速之前,您需要一个非常大的字符串和大量替换对,但它仍然值得记住。还值得记住的是,正则表达式本身有性能损失,因此通常不应将此考虑因素包含在决策过程中。

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

我们使用 instead of 允许我们在寻找替换字符串时应用自定义逻辑。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 片段,也不要用作转义字符)。&

总结一下

你正在做的事情有边缘情况。为了正确解决问题,您需要使用能够处理这些边缘情况的工具 - 当涉及到字符串操作时,该作业的工具通常是正则表达式。

评论

5赞 Dany Khalife 8/14/2013
优秀的答案,包含我一直在寻找的所有详细解释,感谢您分享您的经验,尤其是关于为什么它更好的部分,我非常感谢您花时间写这个:)
0赞 t1gor 1/13/2014
这似乎只是为了替换字符串......恕我直言
1赞 DaveRandom 1/14/2014
@t1gor 如果您想要的只是字符串替换,请使用 .但是,如果你想要一个合适的模板系统,那么有太多的边缘情况是无法应对的。str_replace()str_replace()
0赞 t1gor 1/14/2014
@DaveRandom 你很可能是对的。但是,如果我们谈论的是模板系统,我会考虑 OOP 概念和 -ing 模板文件。我的项目中有一些类似的东西:bitbucket.org/t1gor/strategy/src/......include()
2赞 nijel 1/13/2014 #10

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

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

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

评论

0赞 Adam 1/17/2014
为什么这被否决了?对我来说似乎是显而易见的答案。虽然顺序可能因语言而异,但在编写模板字符串时,您知道顺序。要使用 OP 示例,“%1$s 在 %2$s 上创建了一个线程”或“%2$s 在 %1$s 上有一个新帖子”或 sprintf($template_string, 'Dany', 'StackOverflow') 支持的任何内容。这正是有多少系统解决翻译问题(例如,Wordpress 使用 gettext,如果你需要字符串中的参数,建议以这种方式通过 printf 传递你的 gettext 字符串)codex.wordpress.org/I18n_for_WordPress_Developers#Placeholders
0赞 Adam 1/17/2014
还行。我看到 OP 希望变量完全如此命名。由于无论如何都需要记录每个字符串,因此我不相信这是值得的。既然他也说他嘲笑了 sprintf,但因为排序问题而打了折扣,那么我认为这仍然是一个有效的答案
2赞 Phil 1/13/2014 #11

gettext 是一个广泛使用的通用本地化系统,可以完全按照您的要求执行任务。 大多数编程语言都有库,PHP 有一个内置引擎。 它由 po 文件驱动,这是基于简单文本的格式,周围有许多编辑器,并且与 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);与上述相同,但参数顺序已更改。

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

查看维基百科和 PHP 文档,了解其工作原理的基本概述:

谷歌会找到大量的文档,而你最喜欢的软件存储库很可能有一些用于管理po文件的工具。

我使用过的一些是:

  • poedit:非常轻巧和简单。如果你没有太多的东西要翻译,也不想花时间思考这些东西是如何工作的,那就太好了。
  • Virtaal:有点复杂,有点学习曲线,但也有一些不错的功能,让你的生活更轻松。如果你需要翻译很多,那就太好了。
  • GlotPress 是一个 Web 应用程序(来自 wordpress 人),允许协作编辑翻译数据库文件。

评论

0赞 Praveen D 1/17/2014
与 xml 相比,gettext 最好吗?它是否向服务器发送请求以翻译每个 msgid?
0赞 Phil 1/18/2014
不,它与 XML 没有任何共同之处。它不会在运行时发送任何请求。它几乎是为每种语言设置的字符串集合。
0赞 John Ezell 1/16/2014 #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);
            }

评论

0赞 hakre 1/16/2014
嗵:)嗵嗵��