【问题标题】:PHP: trim word OR part of it from begining/end of stringPHP:从字符串的开头/结尾修剪单词或部分
【发布时间】:2014-03-26 07:22:30
【问题描述】:

我需要从字符串的开头和结尾修剪单词。问题是,有时这些词可以缩写为 ie。只有前三个字母(后跟点)。

我努力寻找合适的正则表达式。基本上我需要聊天三个或更多初始字符直到替换长度,但我找不到正则表达式,它将匹配可变长度并保持字符顺序。

例如,如果我需要从句子'insur. companies are rich' 中修剪'insurance',那么我会想到模式\^[insurance]{3,9}\,但是这种模式也会捕捉到像'sensace' 这样的词,因为字符的顺序(以及它们的出现) 在[] 中对于正则表达式并不重要。

另外,在字符串的末尾,我需要删除从 beginig 缩写的序列号 - 比如 'XK-25F14' 有时显示为 '25F14'。所以我决定纯粹用逐个字符来比较。

因此我以下面的 php 函数结束

function trimWords($s, $dirt, $case_insensitive = false, $reverse = true)
{
    $pos = 0;
    $func = $case_insensitive ? 'strncasecmp' : 'strncmp';

    // Get number of initial characters, that match in both strings 
    while ($func($s, $dirt, $pos + 1) === 0)
        $pos++;

    // If more than 2 initial characters match, then remove the match   
    if ($pos > 2)
        $s = substr($s, $pos);

    // Reverse $s and $dirt so it will trim from the end of string
    $s = strrev($s);        
    if ($reverse)
        return trimWords($s, strrev($dirt), $case_insensitive, false);

    // After second run return back-reversed string 
    return trim($s, ' .-');
}

我对这个功能很满意,但它有一个缺点。它只修剪一次出现的单词。如何让它修剪更多的出现,即从'Insurance insur. companies'中删除'insurance '

我也很好奇,它真的不存在这样的正则表达式,它会匹配可变长度并尊重模式中的字符顺序?

最终解决方案

感谢 mrhobo,我以基于正则表达式的函数结束。此功能可以轻松改进,并且对于此任务也应该是最有效的。

我已经修改了我之前的函数,它比正则表达式快两倍,但它每次运行只能删除一个单词,所以要能够从开头和结尾删除单词,它必须自己运行两次,性能是与正则表达式相同,并且要删除多次出现的单词,它必须多次运行自己,然后会越来越慢。

最终的功能是这样的。

function trimWords($string, $word, $case_insensitive = false, $min_abbrv = 3)
{
    $exc = substr($word, $min_abbrv);
    $pat = null;

    $i = strlen($exc);
    while ($i--)
        $pat = '(?>'.preg_quote($exc[$i], '#').$pat.')?';

    $pat = substr($word, 0, $min_abbrv).$pat;
    $pat = '#(?<begin>^)?(?:\W*\b'.$pat.'\b\W*)+(?(begin)|$)#';
    if ($case_insensitive)
        $pat .= 'i';

    return preg_replace($pat, '', $string);
}

注意:使用此功能,无论缩写是否以点结尾,它都会消除任何较短的单词形式,并删除单词周围的所有非单词字符。

编辑:我刚刚尝试创建像 insu(r|ra|ran|ranc|rance) 这样的替换模式,使用原子组的函数速度快了约 30%,而且如果单词越长,它可能效率更高。

【问题讨论】:

  • 怎么样:insur(ance|\.)
  • @mrhobo:在这种情况下它可以工作,但如果有人将它缩写为'ins.''insura.'。我不想构建像/ins((u)|(ur)|(ura)|(uran)|(urance))\.?/ 这样的搜索模式,虽然它可以是一种方式,但对我来说似乎相当复杂和低效。
  • 困难的一个。我想出了这个: insu(?>r|\.)(?>a|\.)?(?>r|\.)?(?>n|\.)?(?>c|\.) ?(?>e|\.)?
  • @mrhobo: 很好地尝试前瞻,但是当你开始在模式中使用元字符? 时,字符的顺序也可能被破坏。

标签: php regex string trim


【解决方案1】:

在正则表达式中匹配一个单词和第 n 个字母的所有可能的缩写并不是一件容易的事。

这是我对第 4 个字母中的保险一词的处理方式:

insu(?>r(?>a(?>n(?>c(?>(?<last>e))?)?)?)?)?(?(last)|\.)

http://regex101.com/r/aL2gV4

它通过使用原子组来强制正则表达式引擎使用嵌套模式(?&gt;a(?&gt;b)?)? 尽可能向前超过最后一个“rance”字母。如果最后一个字母匹配,我们不处理缩写,因此不需要点,否则需要点。这是由(?(last)|\.) 编码的。

为了修剪,我将创建一个函数来构建上述正则表达式的缩写。然后,您可以编写一个 while 循环,用空格替换每个缩写正则表达式,直到没有更多匹配项为止。

非正则表达式版本

这是我的非正则表达式版本,它从字符串中删除多个单词和缩写词:

function trimWords($str, $word, $min_abbrv, $case_insensitive = false) {
  $len      = 0;
  $word_len = strlen($word);
  $strlen   = strlen($str);
  $cmp      = $case_insensitive ? strncasecmp : strncmp;

  for ($i = 0; $i < $strlen; $i++) {
    if ($cmp($str[$i], $word[$len], $i) == 0) {
      $len++;
    } else if ($len > 0) {
      if ($len == $word_len || ($len >= $min_abbrv && ($dot = $str[$i] == '.'))) {
        $i     -= $len;
        $len   += $dot;
        $str    = substr($str, 0, $i) . substr($str, $i+$len);
        $strlen = strlen($str);
        $dot    = 0;
      }
      $len = 0;
    }
  }

  return $str;
}

例子:

$string = 'ins. <- "ins." / insu. insuranc. insurance / insurance. <- "."';
echo trimWords($string, 'insurance', 4);

输出是:

ins. <- "ins." / / . <- "."

【讨论】:

  • 这种嵌套的原子前瞻看起来很有希望。我一到家就会深入检查。
  • 当然。明确一点:不涉及前瞻。
  • 你是对的。我考虑将前瞻作为保留字符顺序的可能解决方案,但这是不行的。另一方面,您的解决方案非常出色,尽管相当复杂。
  • 我改进了正则表达式版本以捕获更多单词并在字符串末尾捕获单词,最终正则表达式版本几乎比非正则表达式解决方案快 4 倍。检查我最初的帖子,我在那里添加了最终功能。
【解决方案2】:

我编写了根据 mrhobo 构造正则表达式模式的函数,并通过纯 PHP 字符串比较对我的函数进行了简单的测试和基准测试。

代码如下:

$string = 'Insur. companies are nasty rich';
$dirt = 'insurance';
$cycles = 500000;


$start = microtime(true);

$i = $cycles;
while ($i) {
    $i--;
    regexpStyle($string, $dirt, true);
}

$stop = microtime(true);

$i = $cycles;
while ($i) {
    $i--;
    trimWords($string, $dirt, true);
}

$end = microtime(true);

$res1 = $stop - $start;
$res2 = $end - $stop;


$winner = $res1 < $res2 ? '<<<' : '>>>';

echo 'regexp: '.$res1.' '.$winner.' string operations: '.$res2;

function trimWords($s, $dirt, $case_insensitive = false, $reverse = true)
{
    $pos = 0;
    $func = $case_insensitive ? 'strncasecmp' : 'strncmp';

    // Get number of initial characters, that match in both strings 
    while ($func($s, $dirt, $pos + 1) === 0)
        $pos++;

    // If more than 2 initial characters match, then remove the match   
    if ($pos > 2)
        $s = substr($s, $pos);

    // After second run return back-reversed string 
    return trim($s, ' .-');
}

function regexpStyle($s, $dirt, $case_insensitive, $min_abbrev = 3)
{
    $ss = substr($dirt, $min_abbrev);
    $arr = str_split($ss);
    $patt = '(?>(?<last>'.array_pop($arr).'))?';
    $i = count($arr);
    while ($i)
        $patt = '(?>'.$arr[--$i].$patt.')?';
    $patt = '#^'.substr($dirt, 0, $min_abbrev).$patt.'(?(last)|\.)#';
    $patt .= $case_insensitive ? 'i' : null;
    return trim(preg_replace($patt, '', $s));
}

获胜者是......沉默的时刻......它是......

平局

regexp: 8.5169589519501 >>> string operations: 8.0951890945435

但我强烈认为可以更好地利用正则表达式方法。

【讨论】:

  • 更好地利用它的一种方法是重用正则表达式。如果您有要删除的单词列表,则可以提前为其构建所有正则表达式。还有 S 修饰符。
  • 同时检查我的非正则表达式版本。
猜你喜欢
  • 1970-01-01
  • 2011-03-01
  • 2013-08-18
  • 2021-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多