PHP中的startsWith()和endsWith()函数

1696

我该如何编写两个函数,能够接受一个字符串并返回它是否以指定的字符/字符串开头或结尾?

比如:

$str = '|apples}';

echo startsWith($str, '|'); //Returns true
echo endsWith($str, '}'); //Returns true

26
请查看 Laravel 的 Str 类 中的 startsWith() 和 endsWith() 方法,它们经过了充分测试。这些方法已经处理了边缘情况,因此广泛使用此代码会是一个优势。 - Gras Double
1
你可能会发现s($str)->startsWith('|')s($str)->endsWith('}')很有用,这些函数可以在这个独立的库中找到。 - caw
5
警告:这里的大多数答案在多字节编码(例如UTF-8)中不可靠。 - Álvaro González
继续我之前的评论,你可以确保使用最新版本(截至今天,5.4)。值得注意的是,startsWith()已经针对大型字符串进行了优化。 - Gras Double
10
PHP 8.0引入了新的方法,用于检查字符串是否以指定的前缀开始或以指定的后缀结尾:str_starts_withstr_ends_with。原文链接:https://stackoverflow.com/a/64160081/7082164 - Jsowa
35个回答

1857

PHP 8.0及更高版本

自PHP 8.0起,您可以使用

str_starts_with 手册

str_ends_with 手册

示例

echo str_starts_with($str, '|');

PHP 8.0之前的版本

function startsWith( $haystack, $needle ) {
     $length = strlen( $needle );
     return substr( $haystack, 0, $length ) === $needle;
}
function endsWith( $haystack, $needle ) {
    $length = strlen( $needle );
    if( !$length ) {
        return true;
    }
    return substr( $haystack, -$length ) === $needle;
}

20
我认为endsWith('foo', '') == false是正确的行为。因为“foo”没有以任何东西结尾。 'Foo'以'o'、'oo'和'Foo'结尾。 - MrHus
153
EndsWith 可以写得更短:return substr($haystack, -strlen($needle))===$needle; - Rok Kralj
10
只有当$needle不为空时才执行。 - Ja͢ck
15
可以通过将 $length 作为 substr 的第三个参数传递来完全避免使用 ifreturn (substr($haystack, -$length, $length);。这种方法处理 $length == 0 的情况,将返回空字符串而不是整个 $haystack - mxxk
22
我建议使用多字节安全的函数,例如mb_strlen和mb_substr。 - 19Gerhard85
显示剩余28条评论

1104

您可以使用substr_compare函数来检查字符串是否以某个特定的前缀或后缀开头:

function startsWith($haystack, $needle) {
    return substr_compare($haystack, $needle, 0, strlen($needle)) === 0;
}
function endsWith($haystack, $needle) {
    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
}

这应该是 PHP 7 上最快的解决方案之一(基准测试脚本)。针对8KB的大字符串、不同长度的子串以及完整匹配、部分匹配和未匹配的情况进行了测试。 strncmp 在匹配开头时略快,但无法检查结尾。


2
我不明白。根据http://php.net/manual/en/function.strrpos.php的说明:“如果该值为负,则搜索将从字符串末尾向前移动那么多个字符开始。”这似乎表明我们从字符0(由于`-strlength($haystack)`)开始*向后*搜索?这不意味着您没有搜索任何内容吗?我也不理解此处的“!== false”部分。我猜这依赖于PHP的一个怪癖,其中一些值是“真实”的,而其他值是“虚假”的,但在这种情况下,它是如何工作的? - Welbog
3
例如:haystack = xxxyyy,needle = yyy,使用 strrpos 开始搜索时从第一个 x 开始。现在我们没有成功匹配(找到了 x 而不是 y),并且我们不能再向后移动了(我们已经在字符串的开头),搜索立即失败。关于使用 !== false -- 在上面的示例中,strrpos 将返回 0 或 false,而不是其他值。同样,strpos 在上面的示例中可以返回 $temp(预期位置)或 false。我选择使用 !== false 是为了一致性,但你也可以分别在函数中使用 === 0=== $temp - Salman A
这上面有太多不必要的工作。为什么不使用strpos === 0来判断字符串是否以某个子串开头呢?因为你过度复杂化了次优代码的响应,所以我给你点了踩。 - Spoo
13
已经有结论表明,如果需要在一个大的字符串中查找一个不存在的子字符串时,使用 strpos === 0 是一个糟糕的解决方案。 - Salman A
警告:这是一个不区分大小写、不支持Unicode的解决方案。相比之下,PHP8的str_starts_with是区分大小写的。 - hanshenrik
显示剩余2条评论

260

更新于2016年8月23日

函数

function substr_startswith($haystack, $needle) {
    return substr($haystack, 0, strlen($needle)) === $needle;
}

function preg_match_startswith($haystack, $needle) {
    return preg_match('~' . preg_quote($needle, '~') . '~A', $haystack) > 0;
}

function substr_compare_startswith($haystack, $needle) {
    return substr_compare($haystack, $needle, 0, strlen($needle)) === 0;
}

function strpos_startswith($haystack, $needle) {
    return strpos($haystack, $needle) === 0;
}

function strncmp_startswith($haystack, $needle) {
    return strncmp($haystack, $needle, strlen($needle)) === 0;
}

function strncmp_startswith2($haystack, $needle) {
    return $haystack[0] === $needle[0]
        ? strncmp($haystack, $needle, strlen($needle)) === 0
        : false;
}

测试

echo 'generating tests';
for($i = 0; $i < 100000; ++$i) {
    if($i % 2500 === 0) echo '.';
    $test_cases[] = [
        random_bytes(random_int(1, 7000)),
        random_bytes(random_int(1, 3000)),
    ];
}
echo "done!\n";


$functions = ['substr_startswith', 'preg_match_startswith', 'substr_compare_startswith', 'strpos_startswith', 'strncmp_startswith', 'strncmp_startswith2'];
$results = [];

foreach($functions as $func) {
    $start = microtime(true);
    foreach($test_cases as $tc) {
        $func(...$tc);
    }
    $results[$func] = (microtime(true) - $start) * 1000;
}

asort($results);

foreach($results as $func => $time) {
    echo "$func: " . number_format($time, 1) . " ms\n";
}

结果(PHP 7.0.9)

(按速度排序,从快到慢)

strncmp_startswith2: 40.2 ms
strncmp_startswith: 42.9 ms
substr_compare_startswith: 44.5 ms
substr_startswith: 48.4 ms
strpos_startswith: 138.7 ms
preg_match_startswith: 13,152.4 ms

结果(PHP 5.3.29)

(按速度从快到慢排序)

strncmp_startswith2: 477.9 ms
strpos_startswith: 522.1 ms
strncmp_startswith: 617.1 ms
substr_compare_startswith: 706.7 ms
substr_startswith: 756.8 ms
preg_match_startswith: 10,200.0 ms

startswith_benchmark.php


3
如果字符串不为空,就像你的测试一样,这个函数实际上会比原来更快(20-30%)。这是一个判断字符串是否以指定字符开头的函数,我在下面添加了回复。 - FrancescoMM
3
因为110小于133,所以为什么会这样呢? - mpen
2
真糟糕,我不知道当时是怎么了。可能是因为缺乏睡眠。 - Jronny
1
这些测试无法有效地测试性能。你所做的是使用随机字符串作为针头。在99.99%的情况下,没有匹配。大多数函数将在匹配第一个字节后退出。如果找到匹配项怎么办?哪个函数需要最少的时间来确定成功匹配?如果99%的针头匹配但最后几个字节不匹配怎么办?哪个函数需要最少的时间来确定没有匹配? - Salman A
3
如果您不使用isset测试$haystack[0]和$needles,它们将会抛出一个提示错误。但是,如果您添加了测试,它将会降低其性能。 - Thanh Trung
显示剩余17条评论

147

到目前为止所有的答案都做了很多不必要的工作,如strlen计算字符串分配(substr)等。函数'strpos''stripos'返回$haystack中第一次出现$needle的索引:

function startsWith($haystack,$needle,$case=true)
{
    if ($case)
        return strpos($haystack, $needle, 0) === 0;

    return stripos($haystack, $needle, 0) === 0;
}

function endsWith($haystack,$needle,$case=true)
{
    $expectedPosition = strlen($haystack) - strlen($needle);

    if ($case)
        return strrpos($haystack, $needle, 0) === $expectedPosition;

    return strripos($haystack, $needle, 0) === $expectedPosition;
}

2
endsWith() 函数存在错误。 它的第一行应该是(去掉 -1): $expectedPosition = strlen($haystack) - strlen($needle); - Enrico Detoma
7
strlen() 函数不是多余的。如果字符串不以指定的关键词开头,则你的代码将不必要地扫描整个文本。 - AppleGrew
6
@Mark 是的,仅检查开头要快得多,特别是如果你正在检查 MIME 类型(或任何其他字符串很长的地方)。 - chacham15
2
@mark 我对1000个字符的干草堆和10或800个字符的针进行了一些基准测试,strpos比较快,快了30%。在声明某个东西更快或更慢之前,请先进行基准测试... - wdev
8
如果存在任何可能,例如如果字符串是来自json_decode()的话,你应该强烈考虑像这样引用类似于strpos($haystack, "$needle", 0)的needle。否则, strpos()的[奇怪]默认行为可能会导致意外的结果:“如果needle不是一个字符串,则它将被转换为整数并作为字符的序值应用。 - quietmint
显示剩余9条评论

55

PHP 8 更新

PHP 8 包含了新的str_starts_withstr_ends_with函数,为这个问题提供了一个高效且方便的解决方案:

$str = "beginningMiddleEnd";
if (str_starts_with($str, "beg")) echo "printed\n";
if (str_starts_with($str, "Beg")) echo "not printed\n";
if (str_ends_with($str, "End")) echo "printed\n";
if (str_ends_with($str, "end")) echo "not printed\n";

这个特性的RFC提供了更多信息,同时还讨论了明显(和不太明显)的用户空间实现的优点和问题。


53
function startsWith($haystack, $needle, $case = true) {
    if ($case) {
        return (strcmp(substr($haystack, 0, strlen($needle)), $needle) === 0);
    }
    return (strcasecmp(substr($haystack, 0, strlen($needle)), $needle) === 0);
}

function endsWith($haystack, $needle, $case = true) {
    if ($case) {
        return (strcmp(substr($haystack, strlen($haystack) - strlen($needle)), $needle) === 0);
    }
    return (strcasecmp(substr($haystack, strlen($haystack) - strlen($needle)), $needle) === 0);
}

致谢:

检查字符串是否以另一个字符串结尾

检查字符串是否以另一个字符串开头


2
strtolower 不是制作大小写不敏感函数的最佳方式。在某些语言环境中,大小写转换比仅仅转换为大写或小写更加复杂。 - Sander Rijken
9
我看到的只有抱怨,没有解决方案...如果你觉得这不好,那么你也应该给出一个应该如何做的例子。 - KdgDev
2
@WebDevHobo:这就是为什么在你的评论前一天我自己添加了一个答案。对于你的代码,strcasecmp确实是正确的选择。 - Sander Rijken

47

这个问题已经有了很多答案,但在某些情况下,您可以选择比它们更简单的解决方案。如果您要查找的字符串是已知的(硬编码),则可以使用正则表达式,而不需要引用等。

检查字符串是否以'ABC'开头:

preg_match('/^ABC/', $myString); // "^" here means beginning of string

以'ABC'结尾:

preg_match('/ABC$/', $myString); // "$" here means end of string

在我的简单情况下,我想检查一个字符串是否以斜杠结尾:

preg_match('#/$#', $myPath);   // Use "#" as delimiter instead of escaping slash

优点:由于它非常简短和简单,所以您不必像上面所示那样定义一个函数(例如endsWith())。

但是再次强调——这不是每种情况的解决方案,只适用于这个非常具体的情况。


你不需要硬编码字符串。正则表达式可以是动态的。 - Ryan
2
@self true,但如果字符串不是硬编码的,则必须对其进行转义。目前有两个回答在这个问题上做到了这一点。这很容易,但它会稍微复杂化代码。因此,我的观点是,在非常简单的情况下,可以保持简单,只要硬编码是可能的。 - noamtm

39

上述正则表达式函数可以使用其他提议的调整进行更改:

 function startsWith($needle, $haystack) {
     return preg_match('/^' . preg_quote($needle, '/') . '/', $haystack);
 }

 function endsWith($needle, $haystack) {
     return preg_match('/' . preg_quote($needle, '/') . '$/', $haystack);
 }

2
在 PHP 中进行字符串操作时,参数的顺序是 $haystack, $needle。这些函数的表现方式与数组函数相反,实际上顺序应该是 $needle, $haystack。 - Andrew

32

最快的 endsWith() 解决方案:

# Checks if a string ends in a string
function endsWith($haystack, $needle) {
    return substr($haystack,-strlen($needle))===$needle;
}

基准测试:

# This answer
function endsWith($haystack, $needle) {
    return substr($haystack,-strlen($needle))===$needle;
}

# Accepted answer
function endsWith2($haystack, $needle) {
    $length = strlen($needle);

    return $length === 0 ||
    (substr($haystack, -$length) === $needle);
}

# Second most-voted answer
function endsWith3($haystack, $needle) {
    // search forward starting from end minus needle length characters
    if ($needle === '') {
        return true;
    }
    $diff = \strlen($haystack) - \strlen($needle);
    return $diff >= 0 && strpos($haystack, $needle, $diff) !== false;
}

# Regex answer
function endsWith4($haystack, $needle) {
    return preg_match('/' . preg_quote($needle, '/') . '$/', $haystack);
}

function timedebug() {
    $test = 10000000;

    $time1 = microtime(true);
    for ($i=0; $i < $test; $i++) {
        $tmp = endsWith('TestShortcode', 'Shortcode');
    }
    $time2 = microtime(true);
    $result1 = $time2 - $time1;

    for ($i=0; $i < $test; $i++) {
        $tmp = endsWith2('TestShortcode', 'Shortcode');
    }
    $time3 = microtime(true);
    $result2 = $time3 - $time2;

    for ($i=0; $i < $test; $i++) {
        $tmp = endsWith3('TestShortcode', 'Shortcode');
    }
    $time4 = microtime(true);
    $result3 = $time4 - $time3;

    for ($i=0; $i < $test; $i++) {
        $tmp = endsWith4('TestShortcode', 'Shortcode');
    }
    $time5 = microtime(true);
    $result4 = $time5 - $time4;

    echo $test.'x endsWith: '.$result1.' seconds # This answer<br>';
    echo $test.'x endsWith2: '.$result4.' seconds # Accepted answer<br>';
    echo $test.'x endsWith3: '.$result2.' seconds # Second most voted answer<br>';
    echo $test.'x endsWith4: '.$result3.' seconds # Regex answer<br>';
    exit;
}
timedebug();

基准测试结果:

10000000x endsWith: 1.5760900974274 seconds # This answer
10000000x endsWith2: 3.7102129459381 seconds # Accepted answer
10000000x endsWith3: 1.8731069564819 seconds # Second most voted answer
10000000x endsWith4: 2.1521229743958 seconds # Regex answer

5
感谢您花时间比较不同的解决方案并实际进行基准测试,赞一个!您还应该提及使用的PHP版本,因为随着语言的发展,会进行优化!我曾经看到不同的PHP版本之间字符串比较函数的显著改进 :) - Christophe Deliens
2
回应 @ChristopheDeliens 的请求,提供 PHP 版本。我在 7.3.2 上运行了你的测试,并得到了类似的结果。 - Jeff

29

如果速度对你很重要,尝试这个方法。(我相信这是最快的方法)

仅适用于字符串,如果$haystack只有1个字符。

function startsWithChar($needle, $haystack)
{
   return ($needle === $haystack[0]);
}

function endsWithChar($needle, $haystack)
{
   return ($needle === $haystack[strlen($haystack) - 1]);
}

$str='|apples}';
echo startsWithChar('|',$str); //Returns true
echo endsWithChar('}',$str); //Returns true
echo startsWithChar('=',$str); //Returns false
echo endsWithChar('#',$str); //Returns false

1
这可能是最有效的答案,因为没有使用任何额外的函数,只是常规字符串... - user1646111
它应该检查字符串是否至少有一个字符,并且两个参数是否已交换。 - a1an
2
创意的。需要包含大量信息的数据结构。顺便提一下,有一个丑陋的警告:endsWithChar('','x'),但结果是正确的。 - Tino
1
我喜欢你的回答,但有点好笑...针和干草堆是颠倒了 :) ...也就是说,你应该在干草堆中搜索针,因此应该是:return ($needle === $haystack[0]); 不过还是很好的答案,谢谢! - Heider Sati
1
@HeiderSati:非常好的观察!这就是@Tino所说的“创造性。需要包含干草堆的针。”...我没有足够的注意力。谢谢!我已经修复了。 :) - lepe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接