PHP 字符串包含判断:从基础函数到现代方法的高效实践与性能考量149


在 PHP 编程中,字符串操作是日常开发中不可或缺的一部分。无论是处理用户输入、解析日志文件、构建 URL,还是进行数据校验,判断一个字符串是否包含特定的子字符串都是一项极其常见的任务。这项看似简单的操作,在 PHP 中却有多种实现方式,每种方式都有其独特的适用场景、性能特点以及潜在的“陷阱”。本文将作为一份详尽的指南,深入探讨 PHP 中判断字符串是否包含子字符串的各种方法,从传统的 `strpos()` 到现代的 `str_contains()`,并对其性能、多字节字符支持以及最佳实践进行全面分析。

一、PHP 5/7 时代:传统与经典方法

在 PHP 8 引入 `str_contains()` 之前,开发者主要依赖以下几个函数来完成子字符串的查找任务。理解它们的工作原理和返回值至关重要,特别是对 `strpos()` 和 `stripos()` 这两个函数。

1.1 `strpos()`:区分大小写的子字符串查找


`strpos()` 是 PHP 中最基础也是最常用的子字符串查找函数之一。它用于查找一个字符串在另一个字符串中首次出现的位置(索引)。

函数签名:

`strpos(string $haystack, string $needle, int $offset = 0): int|false`
`$haystack`: 待搜索的原始字符串。
`$needle`: 待查找的子字符串。
`$offset`: 可选参数,指定从 `$haystack` 的哪个位置开始搜索。默认是 `0`。
返回值:如果找到了子字符串,返回它首次出现的起始位置(从 `0` 开始计数)。如果未找到,则返回 `false`。

关键点与注意事项:

`strpos()` 的返回值是 `int` 或 `false`。由于 `0` (代表子字符串在原始字符串的开头)在 PHP 的弱类型比较中会被视为 `false`,因此在使用 `strpos()` 判断子字符串是否存在时,必须使用严格比较 `!== false`,而不是 `!= false` 或 `== true`。<?php
$text = "Hello, world! PHP is great.";
$sub1 = "world";
$sub2 = "Hello";
$sub3 = "php"; // 小写,strpos区分大小写
$sub4 = "Python";
// 示例 1: 子字符串存在且不在开头
if (strpos($text, $sub1) !== false) {
echo "'$text' 包含 '$sub1'.<br>"; // 输出:'Hello, world! PHP is great.' 包含 'world'.
}
// 示例 2: 子字符串存在且在开头 (strpos返回0)
if (strpos($text, $sub2) !== false) { // 0 !== false 为真
echo "'$text' 包含以 '$sub2' 开头的字符串.<br>"; // 输出:'Hello, world! PHP is great.' 包含以 'Hello' 开头的字符串.
}
// 错误示例: 使用 == false
if (strpos($text, $sub2) == false) { // 0 == false 为真,但这不是我们想要的结果
echo "警告:'$text' 被误判为不包含 '$sub2' (或在开头).<br>"; // 会被输出!这是一个常见陷阱。
}
// 示例 3: 子字符串不存在 (strpos返回false)
if (strpos($text, $sub3) === false) { // 'php' 不存在,因为是区分大小写
echo "'$text' 不包含 '$sub3' (区分大小写).<br>"; // 输出:'Hello, world! PHP is great.' 不包含 'php' (区分大小写).
}
if (strpos($text, $sub4) === false) {
echo "'$text' 不包含 '$sub4'.<br>"; // 输出:'Hello, world! PHP is great.' 不包含 'Python'.
}
?>

1.2 `stripos()`:不区分大小写的子字符串查找


`stripos()` 与 `strpos()` 功能类似,但它在查找子字符串时不区分大小写。这对于用户输入或需要更灵活匹配的场景非常有用。

函数签名:

`stripos(string $haystack, string $needle, int $offset = 0): int|false`
参数和返回值与 `strpos()` 完全一致,只是不区分大小写。

示例:<?php
$text = "Hello, world! PHP is great.";
$sub1 = "php"; // 小写
$sub2 = "WoRlD"; // 混合大小写
if (stripos($text, $sub1) !== false) {
echo "'$text' 包含 '$sub1' (不区分大小写).<br>"; // 输出:'Hello, world! PHP is great.' 包含 'php' (不区分大小写).
}
if (stripos($text, $sub2) !== false) {
echo "'$text' 包含 '$sub2' (不区分大小写).<br>"; // 输出:'Hello, world! PHP is great.' 包含 'WoRlD' (不区分大小写).
}
?>

1.3 `strstr()` / `stristr()`:查找并返回子字符串


`strstr()` 和 `stristr()` 函数不仅查找子字符串,而且在找到时会返回从子字符串首次出现位置到原始字符串末尾的部分。`strstr()` 区分大小写,`stristr()` 不区分大小写。

函数签名:

`strstr(string $haystack, string $needle, bool $before_needle = false): string|false`

`stristr(string $haystack, string $needle, bool $before_needle = false): string|false`
`$before_needle`: 可选参数。如果设置为 `true`,则返回 `needle` 首次出现之前的字符串部分。
返回值:如果找到 `needle`,返回从 `needle` 第一次出现位置开始到 `haystack` 结束的子字符串(默认行为),或者 `needle` 第一次出现之前的部分(如果 `$before_needle` 为 `true`)。如果未找到,则返回 `false`。

用于判断存在性:

虽然这两个函数的主要目的是提取字符串,但也可以通过判断返回值是否为 `false` 来确定子字符串的存在。不过,这通常不如 `strpos()` 或 `stripos()` 直观和高效,因为它们做了额外的工作来构建并返回子字符串。<?php
$text = "Email: user@";
$sub = "@";
if (strstr($text, $sub) !== false) {
echo "'$text' 包含 '$sub'.<br>"; // 输出:'Email: user@' 包含 '@'.
echo "从 '@' 开始的部分: " . strstr($text, $sub) . "<br>"; // 输出:从 '@' 开始的部分: @
}
if (strstr($text, $sub, true) !== false) {
echo "在 '@' 之前的部分: " . strstr($text, $sub, true) . "<br>"; // 输出:在 '@' 之前的部分: Email: user
}
$noSub = "PHP";
if (stristr($text, $noSub) === false) { // stristr 不区分大小写,但 'PHP' 不存在
echo "'$text' 不包含 '$noSub' (不区分大小写).<br>"; // 输出:'Email: user@' 不包含 'PHP' (不区分大小写).
}
?>

二、PHP 8+ 时代:现代与简洁方法

随着 PHP 语言的不断演进,PHP 8 引入了一个专门用于判断字符串是否包含子字符串的函数,极大地提升了代码的可读性和简洁性。

2.1 `str_contains()`:最直接的判断方法 (PHP 8.0+)


`str_contains()` 是 PHP 8.0 新增的函数,其目的就是为了更直观、更清晰地解决“字符串是否包含子字符串”这一常见的需求。它返回一个布尔值,省去了 `strpos()` 及其严格比较的复杂性。

函数签名:

`str_contains(string $haystack, string $needle): bool`
`$haystack`: 待搜索的原始字符串。
`$needle`: 待查找的子字符串。
返回值:如果 `$haystack` 包含 `$needle`,则返回 `true`;否则返回 `false`。

优点:
可读性强: 函数名直接表达了其意图,代码更易理解。
简洁: 返回布尔值,无需进行 `!== false` 的比较,避免了 `strpos()` 的常见陷阱。
性能优化: 底层实现经过优化,对于简单子字符串查找而言,通常比用户空间模拟 `strpos() !== false` 更快。

注意事项:
区分大小写: 默认情况下,`str_contains()` 是区分大小写的。如果需要不区分大小写的判断,可以结合 `strtolower()` 或 `strtoupper()` 函数。
PHP 版本要求: 仅适用于 PHP 8.0 及更高版本。

<?php
$text = "PHP 8 is awesome!";
$sub1 = "awesome";
$sub2 = "php"; // 小写
$sub3 = "Python";
// 示例 1: 子字符串存在
if (str_contains($text, $sub1)) {
echo "'$text' 包含 '$sub1'.<br>"; // 输出:'PHP 8 is awesome!' 包含 'awesome'.
}
// 示例 2: 区分大小写
if (!str_contains($text, $sub2)) {
echo "'$text' 不包含 '$sub2' (区分大小写).<br>"; // 输出:'PHP 8 is awesome!' 不包含 'php' (区分大小写).
}
// 示例 3: 不区分大小写的判断 (通过转换大小写实现)
if (str_contains(strtolower($text), strtolower($sub2))) {
echo "'$text' 包含 '$sub2' (不区分大小写).<br>"; // 输出:'PHP 8 is awesome!' 包含 'php' (不区分大小写).
}
// 示例 4: 子字符串不存在
if (!str_contains($text, $sub3)) {
echo "'$text' 不包含 '$sub3'.<br>"; // 输出:'PHP 8 is awesome!' 不包含 'Python'.
}
?>

三、更高级与灵活的方法

除了上述直接查找子字符串的函数外,PHP 还提供了正则表达式和计数函数,用于处理更复杂或特定需求的场景。

3.1 正则表达式 `preg_match()`:模式匹配的利器


当需要查找的子字符串不是固定的,而是符合某种模式时,正则表达式是不可替代的工具。`preg_match()` 函数用于执行正则表达式匹配。

函数签名:

`preg_match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int|false`
`$pattern`: 要搜索的正则表达式模式。
`$subject`: 待搜索的原始字符串。
返回值:如果找到匹配项,返回 `1`;如果未找到,返回 `0`;如果发生错误,返回 `false`。

应用场景:
查找包含特定前缀或后缀的字符串。
验证邮箱、URL、电话号码等格式。
查找包含通配符或多个备选项的子字符串。
需要不区分大小写查找(通过 `i` 修正符)。

示例:<?php
$text = "Email: user@, Phone: +1-555-1234";
// 示例 1: 查找是否包含 "" (固定子字符串)
if (preg_match('/example\.com/', $text)) { // 注意点号需要转义
echo "'$text' 包含 ''.<br>";
}
// 示例 2: 不区分大小写查找 "email"
if (preg_match('/email/i', $text)) { // 'i' 修正符表示不区分大小写
echo "'$text' 包含 'email' (不区分大小写).<br>";
}
// 示例 3: 查找是否包含电话号码模式
if (preg_match('/\+\d{1,3}-\d{3}-\d{4}/', $text)) {
echo "'$text' 包含一个电话号码.<br>";
}
// 示例 4: 查找是否包含 'user' 或 'admin'
if (preg_match('/(user|admin)/', $text)) {
echo "'$text' 包含 'user' 或 'admin'.<br>";
}
?>

性能考量:

正则表达式引擎功能强大,但其解析和匹配过程相对复杂,对于简单的子字符串查找,`preg_match()` 的性能通常低于 `strpos()` 或 `str_contains()`。因此,只有当 `str_contains()` (或 `strpos()`) 无法满足需求时,才应考虑使用 `preg_match()`。

3.2 `substr_count()`:计算子字符串出现次数


`substr_count()` 函数用于计算一个字符串中子字符串出现的次数。虽然其主要目的是计数,但如果返回的次数大于 `0`,则意味着原始字符串包含该子字符串。

函数签名:

`substr_count(string $haystack, string $needle, int $offset = 0, ?int $length = null): int`
`$haystack`: 原始字符串。
`$needle`: 待查找的子字符串。
`$offset`, `$length`: 可选参数,指定在 `$haystack` 的哪个子部分进行搜索。
返回值:子字符串出现的次数。

示例:<?php
$text = "apple, banana, apple, orange";
$sub = "apple";
$sub_no = "grape";
if (substr_count($text, $sub) > 0) {
echo "'$text' 包含 '$sub',共出现 " . substr_count($text, $sub) . " 次.<br>"; // 输出:'apple, banana, apple, orange' 包含 'apple',共出现 2 次.
}
if (substr_count($text, $sub_no) === 0) {
echo "'$text' 不包含 '$sub_no'.<br>"; // 输出:'apple, banana, apple, orange' 不包含 'grape'.
}
?>

注意事项:

`substr_count()` 是区分大小写的,且不支持不重叠匹配(例如 `aaaa` 中查找 `aa`,结果是 `2` 而非 `3`)。对于简单的存在性判断,它通常不如 `str_contains()` 或 `strpos()` 直观。

四、性能考量与最佳实践

选择正确的函数不仅关乎功能实现,也影响着代码的性能和可维护性。以下是对各种方法在性能和适用性上的综合考量。

4.1 性能对比概览



`str_contains()` (PHP 8.0+):

最快: 对于简单、固定子字符串的查找,它是 PHP 8+ 中最快的选项。底层实现经过高度优化。
推荐: PHP 8+ 的首选。


`strpos()` / `stripos()`:

非常快: 仅次于 `str_contains()`。在 PHP 7 及更早版本中,是执行简单子字符串查找的最佳选择。
注意事项: 务必使用 `!== false` 进行严格比较。


`strstr()` / `stristr()`:

较慢: 如果仅仅是判断子字符串是否存在,它们会做额外的工作来构建并返回子字符串,因此性能上略逊于 `strpos()`。
适用场景: 当你不仅需要知道子字符串是否存在,还需要获取它之后(或之前)的字符串部分时。


`substr_count()`:

中等: 它的目标是计数,如果仅用于判断存在性,其效率不如 `strpos()`。
适用场景: 需要知道子字符串出现次数时。


`preg_match()`:

最慢(对于简单查找): 正则表达式引擎的初始化和匹配过程会带来显著的开销。
适用场景: 仅当需要复杂的模式匹配(如通配符、可选字符、多个备选模式、自定义边界等)时才使用。过度使用正则表达式会降低性能和可读性。



4.2 何时选择哪个函数?


遵循以下原则可以帮助你做出最佳选择:
PHP 8.0 及更高版本:

首选 `str_contains()`: 对于简单的、固定的、区分大小写的子字符串查找,直接使用 `str_contains()`。
不区分大小写: 如果需要不区分大小写,使用 `str_contains(strtolower($haystack), strtolower($needle))`。
复杂模式: 如果需要通配符、多选一、或更复杂的模式匹配,使用 `preg_match()`。
需要位置信息: 如果需要子字符串首次出现的位置,使用 `strpos()` / `stripos()`。
需要剩余部分: 如果需要子字符串之后(或之前)的部分,使用 `strstr()` / `stristr()`。


PHP 7.x 及更早版本:

首选 `strpos()` / `stripos()`: 对于简单的、固定的子字符串查找,使用 `strpos($haystack, $needle) !== false` 或 `stripos($haystack, $needle) !== false`。
复杂模式: 同样使用 `preg_match()`。
需要剩余部分: 同样使用 `strstr()` / `stristr()`。



4.3 编码问题:多字节字符串(UTF-8)


上述所有标准字符串函数(`strpos()`, `stripos()`, `str_contains()`, `strstr()`, `stristr()`, `substr_count()`) 都是字节安全的,而非字符安全的。这意味着它们是按照字节来处理字符串的,对于单字节编码(如 ASCII 或 ISO-8859-1)可能工作正常,但对于包含中文、日文、韩文等字符的多字节编码(如 UTF-8)来说,可能会产生错误或意想不到的结果。

例如,一个中文字符可能由 3 个字节组成,如果 `strpos()` 在一个中文字符的中间找到匹配的字节序列,它会返回一个不正确的“字符”位置。

为了正确处理多字节字符串,PHP 提供了 `mbstring` 扩展,其中包含了一系列 `mb_*` 函数:
`mb_strpos()`: 多字节区分大小写查找。
`mb_stripos()`: 多字节不区分大小写查找。
`mb_strstr()` / `mb_stristr()`: 多字节查找并返回子字符串。
`mb_substr_count()`: 多字节子字符串计数。
`mb_str_contains()` (PHP 8.0+): 多字节判断是否包含子字符串。

这些 `mb_*` 函数通常接受一个额外的 `encoding` 参数(通常是 `'UTF-8'`),确保按照字符而不是字节进行处理。

最佳实践:

如果你的应用程序处理的是 UTF-8 或其他多字节编码的字符串,强烈建议始终使用 `mb_*` 系列函数来避免潜在的编码问题。在使用之前,请确保 `mbstring` 扩展已在 `` 中启用。<?php
// 确保 mbstring 扩展已启用
if (!extension_loaded('mbstring')) {
echo "Warning: mbstring extension is not loaded. Multi-byte string functions might not work correctly.<br>";
}
$text_mb = "你好世界,PHP 是最好的!"; // 包含中文的多字节字符串
$sub_mb = "世界";
$sub_en = "PHP";
// PHP 8.0+ 的多字节 str_contains
if (function_exists('mb_str_contains')) {
if (mb_str_contains($text_mb, $sub_mb, false, 'UTF-8')) {
echo "'$text_mb' (mb_str_contains) 包含 '$sub_mb'.<br>";
}
if (mb_str_contains(mb_strtolower($text_mb, 'UTF-8'), mb_strtolower($sub_en, 'UTF-8'), false, 'UTF-8')) {
echo "'$text_mb' (mb_str_contains, 不区分大小写) 包含 '$sub_en'.<br>";
}
} else {
// PHP 7.x 或更早版本的多字节 strpos
if (mb_strpos($text_mb, $sub_mb, 0, 'UTF-8') !== false) {
echo "'$text_mb' (mb_strpos) 包含 '$sub_mb'.<br>";
}
if (mb_stripos($text_mb, $sub_en, 0, 'UTF-8') !== false) {
echo "'$text_mb' (mb_stripos) 包含 '$sub_en' (不区分大小写).<br>";
}
}
// 正则表达式 preg_match 对于 UTF-8 字符通常也需要 'u' 修正符
if (preg_match('/世界/u', $text_mb)) { // 'u' 修正符启用 UTF-8 模式
echo "'$text_mb' (preg_match 带 u) 包含 '世界'.<br>";
}
?>

4.4 避免常见错误



`strpos()` 返回 `0` 的陷阱: 这是最常见的错误。务必使用 `!== false` 而不是 `== true` 或 `!= false`。
不区分大小写需求: 明确你的需求是否区分大小写。如果需要不区分大小写,使用 `stripos()` 或 `strtolower()` 结合 `str_contains()`。
多字节字符问题: 如果处理非 ASCII 字符,请务必使用 `mb_*` 函数或在正则表达式中使用 `u` 修正符。
过度使用正则表达式: 对于简单的子字符串查找,避免使用 `preg_match()`,因为它通常会引入不必要的性能开销和代码复杂性。

五、实际应用场景

判断字符串是否包含子字符串在 PHP 开发中有广泛的应用:
用户输入验证: 检查用户提交的评论是否包含敏感词、邮件内容是否包含广告链接。
URL 路由与解析: 判断当前 URL 是否包含特定路径段或查询参数。
日志文件分析: 快速查找日志中是否包含“Error”、“Warning”或特定用户 ID。
文本搜索功能: 实现简单的搜索功能,查找文本内容是否包含用户输入的关键词。
数据过滤与清洗: 从数据库中检索到的文本数据中过滤掉不符合要求的部分。
文件类型判断: 检查文件名是否包含特定扩展名(如 `.jpg`, `.pdf`)。

六、总结

PHP 提供了多种灵活且强大的方法来判断字符串是否包含子字符串。从传统且高性能的 `strpos()` / `stripos()`,到 PHP 8 引入的简洁易读的 `str_contains()`,再到处理复杂模式的正则表达式 `preg_match()`,开发者可以根据具体的需求和 PHP 版本选择最合适的工具。

选择合适的函数是关键:对于简单的存在性判断,在 PHP 8+ 中首选 `str_contains()`,在旧版本中使用 `strpos() !== false`。在处理多字节字符时,务必使用 `mbstring` 扩展提供的 `mb_*` 系列函数。只有当遇到复杂的模式匹配需求时,才应考虑 `preg_match()`。

通过深入理解这些函数的特性、性能差异和最佳实践,PHP 程序员可以编写出更高效、更健壮、更易于维护的代码,从而更好地应对各种字符串处理挑战。

2025-11-03


上一篇:PHP字符串查找:判断字符是否存在及高效实践指南

下一篇:PHP数组到JSON的深度解析与实战:构建健壮Web API的基石