通过从字符串中间删除带分隔符的单词,将 slug 截断到最大长度

Truncating a slug to maximum length by removing delimited words from the middle of string

提问人:David Pham 提问时间:5/27/2023 最后编辑:mickmackusaDavid Pham 更新时间:5/27/2023 访问量:134

问:

我试图通过在必要时从字符串中间删除单词来确保 slug 格式的字符串在总字符限制范围内。

样品蛞蝓:

'/job/hello-this-is-my-job-posting-for-a-daycare-im-looking-for-in-91770-rosemead-california-12345'

字符串将始终以 开头,以 结尾。但是,slug 有 150 个字符的限制,我希望一次截断一个邮政编码之前的单词,这样就不会超过这个字符限制。我知道我必须使用正则表达式/分解,但我该怎么做呢?我尝试了以下方法,但我的 matches 数组似乎有太多元素。/job/in-zipcode-city-state-job_id

$pattern = '/-in-\d{5}-(.*)-(.*)-(\d*)/';
$string = '/job/hello-this-is-my-job-posting-for-a-daycare-im-looking-for-in-91770-rosemead-california-12345';

preg_match($pattern, $string, $matches);
print_r($matches);

// Array
(
    [0] => -in-91770-rosemead-california-12345
    [1] => rosemead
    [2] => california
    [3] => 12345
)

为什么 , 被视为匹配项?不应该只有第一个元素吗?rosemeadcalifornia12345

如何确保完整的 slug 长度不超过 150 个字符,尾随部分(位置)全部包含在内,并在必要时截断前导部分(作业名称)?

php 正则表达式 分解 slug 截断

评论

0赞 Rob Eyre 5/27/2023
默认情况下,每个带括号的表达式都会在数组中获取一个条目$matches
0赞 David Pham 5/27/2023
你能详细说明一下吗?我只期望第一个元素出现在数组中。
1赞 Rob Eyre 5/27/2023
从手册中:“如果提供了匹配项,则会填充搜索结果。$matches[0] 将包含与完整模式匹配的文本,$matches[1] 将包含与第一个捕获的带括号的子模式匹配的文本,依此类推。
1赞 Rob Eyre 5/27/2023
如果你不想在数组中看到这些元素,那么你可以通过在左括号后添加来指定那些带括号的子模式应该是非捕获的,即$matches?:$pattern = '/-in-\d{5}-(?:.*)-(?:.*)-(?:\d*)/'
1赞 Markus AO 5/27/2023
这个问题被关闭为仅解决实际问题的一小部分的重复内容。与此同时,我正在写一个答案。那么,代码是这样的:3v4l.org/Kqh3o......请注意,它包装在一个函数中,默认最大长度为 150,但我用较短的长度调用它,因为您的示例字符串是 97 个字符。如果重新打开这个问题,我将在此处发布代码作为答案。

答:

1赞 Rob Eyre 5/27/2023 #1

您可以在不使用和迭代的情况下执行此操作,只需使用一些标准的字符串操作即可:explode()

$pattern = '/-in-\d{5}-.*-.*-\d*/';
$string = '/job/hello-this-is-my-job-posting-for-a-daycare-im-looking-for-in-91770-rosemead-california-12345';
$matches = [];

if (!preg_match($pattern, $string, $matches)) {
    // mismatched string - error handling here
}

$totalLength = 150;
$maxPrefixLength = $totalLength - strlen($matches[0]);
if ($maxPrefixLength < strlen('/job/')) {
    // no prefix words possible at all - error handling here
}
$prefixLength = max(strlen('/job/'), strrpos(substr($string, 0, $maxPrefixLength), '-'));
$slug = substr($string, 0, $prefixLength) . $matches[0];

评论

1赞 The fourth bird 5/27/2023
模式中的非捕获组没有用途,您可以省略它们-in-\d{5}-.*-.*-\d*
0赞 Markus AO 5/27/2023
是的,在适合的最后一个分隔符处截断最大子字符串比迭代并继续检查前缀长度更容易。你在那里有谁,你只有一个值?这句话总体上似乎有点乱码,因为不需要多个论点。看来内心的表达就足够了。$prefixLength = max(maxstrlenstrlenstrrpos(...)
0赞 Rob Eyre 5/27/2023
@MarkusAO 是的,我的错误 - 错过了收盘。该表达式的目的是在边界内找到最后一个“-”字符的位置,但如果根本没有找到破折号字符,则至少使用“/job/”前缀的长度。诚然,我本可以将其拆分为单独的步骤。
1赞 Rob Eyre 5/27/2023
@Thefourthbird很好的建议 - 从答案中删除
0赞 Markus AO 5/27/2023
然后,假设 slug .这条线的最大值是多少?(会回来。然后,我们是否要使用 ,或者不带分隔符的 slug,无论如何都以开头?此外,您已经在较早的条件中检查。/job/foobardodododostrrposfalse/job//job/strlen('/job/')
1赞 Markus AO 5/27/2023 #2

可以通过多种方式将 URL 段的前导部分修剪为指定长度,其中一些方法比其他方法更复杂。这是一个灵活的实用函数,带有信息丰富的注释。我们使用提取前导部分(作业名称)和尾随部分(位置)的正则表达式作为起点。然后,根据允许的总长度减去位置段长度来计算作业名称的最大允许长度。请参阅评论以获取更多见解。

function trim_slug(string $slug, int $maxlen = 150): string
{
    // check if trimming is required:
    if(strlen($slug) <= $maxlen) {
        return $slug; 
    }
    
    $pattern = '/^(?<job>.+)(?<loc>-in-\d{5}-.*-.*-\d*)$/';
    // $match will have 'job' and 'loc' named keys with the matched values
    preg_match($pattern, $slug, $match);
    
    // raw cut of job name to maximum length:
    $max_job_chars = $maxlen - strlen($match['loc']);
    $job_name = substr($match['job'], 0, $max_job_chars);
    
    // tidy up to last delimiter, if exists, instead of mincing words:
    if($last_delim = strrpos($job_name, '-')) {
        $job_name = substr($match['job'], 0, $last_delim);      
    }
    
    return $job_name . $match['loc'];
}

$string = '/job/hello-this-is-my-job-posting-for-a-daycare-im-looking-for-in-91770-rosemead-california-12345';

echo trim_slug($string, 80);
// result: /job/hello-this-is-my-job-posting-for-a-in-91770-rosemead-california-12345

在用法示例中,最大长度为 80,因为示例字符串只有 97 个字符,因此将按原样从函数返回,默认限制为 150 个字符。3v4l 演示

请注意,此答案使用非多字节感知的 PHP 标准字符串函数。如果需要多字节内容,则应使用相应的多字节字符串函数来避免数据损坏。(你是否希望在 URL 段中开始使用多字节字符,以及处理它的最佳方法是什么,这是另一个问题的主题。

1赞 mickmackusa 5/27/2023 #3
  1. 将输入段子解析为它的 3 个关键组件,
  2. 通过从总余量中减去第一个和第三个长度来计算中间部分允许的字符数,
  3. 在达到字符限制之前,通过找到最新出现的连字符来截断中间部分(干净),然后删除剩余的消耗性子字符串。

这样一来,您就可以获得一个优化为最大长度的字符串,而不会损坏 slug 中的整个单词。

代码:(演示)

$slug = '/job/hello-this-is-my-job-posting-for-a-daycare-im-looking-for-in-91770-rosemead-california-12345';

$slugLimit = 70;

echo preg_replace_callback(
         '~^(/job/)((?:[^-]*-)*)(in-\d{5}-[^-]*-[^-]*-\d*)$~u',
         fn($m) => implode([
             $m[1],
             preg_replace(
                 '~^.{0,' . ($slugLimit - mb_strlen($m[1] . $m[3]) - 1) . '}-\K.*~u',
                 '',
                 $m[2]
             ),
             $m[3]
         ]),
         $slug
     );

输出 slug 的总长度为 68 个字符:

/job/hello-this-is-my-job-posting-in-91770-rosemead-california-12345


或者将第一个和第二个组件组合在一起以简化处理:(演示)

echo preg_replace_callback(
         '~^((?:[^-]*-)*)(in-\d{5}-[^-]*-[^-]*-\d*)$~u',
         fn($m) => implode([
             preg_replace(
                 '~^.{0,' . ($slugLimit - mb_strlen($m[2]) - 1) . '}-\K.*~u',
                 '',
                 $m[1]
             ),
             $m[2]
         ]),
         $slug
     );

最后,我能想到的最紧凑的版本在前瞻中使用捕获组,以便在回调中替换完整的字符串匹配项。演示

echo preg_replace_callback(
         '~^(?:[^-]*-)*(?=(in-\d{5}-[^-]*-[^-]*-\d*)$)~u',
         fn($m) => preg_replace(
             '~^.{0,' . ($slugLimit - mb_strlen($m[1]) - 1) . '}-\K.*~u',
             '',
             $m[0]
         ),
         $slug
     );

如果您检查了新的 slug 并且它仍然超出限制,则应抛出异常或通知用户违规。mb_strlen()