PHP strpos 函数源码分析

本文详细解析了 PHP 中的 strpos 函数实现原理,包括参数解析、处理流程及核心算法 Sunday 算法介绍。

PHP version 7.4

头文件 ext/standard/php_string.h(此文件声明了字符串函数) 中声明了strpos函数

PHP_FUNCTION(strpos);

头文件 ext/standard/string.c 找到函数实现

PHP_FUNCTION(strpos)
{
	zval *needle;
	zend_string *haystack;
	const char *found = NULL;
	char  needle_char[2];
	zend_long  offset = 0;
    // 解析参数 2 表示2个必传参数;3 表示共3个参数
	ZEND_PARSE_PARAMETERS_START(2, 3)
		Z_PARAM_STR(haystack)
		Z_PARAM_ZVAL(needle)
		Z_PARAM_OPTIONAL  // 之前的参数为必传参数;之后的参数为可选参数
		Z_PARAM_LONG(offset)
	ZEND_PARSE_PARAMETERS_END();

	if (offset < 0) {
		offset += (zend_long)ZSTR_LEN(haystack);
	}
	if (offset < 0 || (size_t)offset > ZSTR_LEN(haystack)) {
		php_error_docref(NULL, E_WARNING, "Offset not contained in string");
		RETURN_FALSE;
	}

	if (Z_TYPE_P(needle) == IS_STRING) {
		if (!Z_STRLEN_P(needle)) {
			php_error_docref(NULL, E_WARNING, "Empty needle");
			RETURN_FALSE;
		}

		found = (char*)php_memnstr(ZSTR_VAL(haystack) + offset,
			                Z_STRVAL_P(needle),
			                Z_STRLEN_P(needle),
			                ZSTR_VAL(haystack) + ZSTR_LEN(haystack));
	} else {
		if (php_needle_char(needle, needle_char) != SUCCESS) {
			RETURN_FALSE;
		}
		needle_char[1] = 0;

		php_error_docref(NULL, E_DEPRECATED,
			"Non-string needles will be interpreted as strings in the future. " \
			"Use an explicit chr() call to preserve the current behavior");

		found = (char*)php_memnstr(ZSTR_VAL(haystack) + offset,
							needle_char,
							1,
		                    ZSTR_VAL(haystack) + ZSTR_LEN(haystack));
	}

	if (found) {
		RETURN_LONG(found - ZSTR_VAL(haystack));
	} else {
		RETURN_FALSE;
	}
}

阅读代码可发现,逻辑由 php_memnstr函数实现

在 main/php.h 中可发现 php_memnstr 是宏

#define php_memnstr zend_memnstr

在 Zend/ zend_operators.h 文件中可找到 zend_memnstr 函数定义

static zend_always_inline const char *
zend_memnstr(const char *haystack, const char *needle, size_t needle_len, const char *end)
{
	const char *p = haystack;
	const char ne = needle[needle_len-1];
	ptrdiff_t off_p;
	size_t off_s;

    // 若needle字符串长度为1,直接调用memchr进行查询
	if (needle_len == 1) {
		return (const char *)memchr(p, *needle, (end-p));
	}

	off_p = end - haystack;
	off_s = (off_p > 0) ? (size_t)off_p : 0;

	if (needle_len > off_s) {
		return NULL;
	}
    
    // 字符串长度小于1024或者needle字符串长度小于9 则调用memchr
	if (EXPECTED(off_s < 1024 || needle_len < 9)) {	/* glibc memchr is faster when needle is too short */
		end -= needle_len;

		while (p <= end) {
            // 查找到第一个字符后,比较剩余字符是否相等
			if ((p = (const char *)memchr(p, *needle, (end-p+1))) && ne == p[needle_len-1]) {
				if (!memcmp(needle+1, p+1, needle_len-2)) {
					return p;
				}
			}

			if (p == NULL) {
				return NULL;
			}

			p++;
		}

		return NULL;
	} else {
        // 不易查找时调用zend_memnstr_ex
		return zend_memnstr_ex(haystack, needle, needle_len, end);
	}
}

// 采用Sunday算法,在匹配过程中,模式串发现不匹配时,算法能跳过尽可能多的字符以进行下一步的匹配,从而提高匹配效率
ZEND_API const char* ZEND_FASTCALL zend_memnstr_ex(const char *haystack, const char *needle, size_t needle_len, const char *end) /* {{{ */
{
	unsigned int td[256]; // 存储每一种字符最后出现的位置
	register size_t i;
	register const char *p;

	if (needle_len == 0 || (end - haystack) < needle_len) {
		return NULL;
	}

	zend_memnstr_ex_pre(td, needle, needle_len, 0);

	p = haystack;
	end -= needle_len;

	while (p <= end) {
		for (i = 0; i < needle_len; i++) {
			if (needle[i] != p[i]) {
				break;
			}
		}
		if (i == needle_len) {
			return p;
		}
		if (UNEXPECTED(p == end)) {
			return NULL;
		}
		p += td[(unsigned char)(p[needle_len])];
	}

	return NULL;
}

/*
 * String matching - Sunday algorithm
 * http://www.iti.fh-flensburg.de/lang/algorithmen/pattern/sundayen.htm
 * 记录每一种字符最后出现的位置
 */
static zend_always_inline void zend_memnstr_ex_pre(unsigned int td[], const char *needle, size_t needle_len, int reverse) /* {{{ */ {
	int i;

	for (i = 0; i < 256; i++) {
		td[i] = needle_len + 1;
	}

	if (reverse) {
		for (i = needle_len - 1; i >= 0; i--) {
			td[(unsigned char)needle[i]] = i + 1;
		}
	} else {
		size_t i;

		for (i = 0; i < needle_len; i++) {
			td[(unsigned char)needle[i]] = (int)needle_len - i;
		}
	}
}

memchr与memcmp函数声明位于标准库string.h文件

// 在参数 s 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置
// 返回一个指向匹配字节的指针,如果在给定的内存区域未出现字符,则返回 NULL
void	*memchr(const void *__s, int __c, size_t __n);

// 把存储区 s1 和存储区 s2 的前 n 个字节进行比较
// 返回值 < 0,则表示 str1 小于 str2
// 返回值 > 0,则表示 str1 大于 str2
// 返回值 = 0,则表示 str1 等于 str2
int	 memcmp(const void *__s1, const void *__s2, size_t __n);

Sunday 算法 算法之Sunday算法_程序员的暴击的博客-优快云博客

<think>我们正在处理一个关于PHPstrpos()函数出现"Empty needle"错误的问题。根据用户提供的引用内容,我们可以了解到: - 引用[1]显示在Yii 2.0框架中执行命令时,在`Target.php`文件的第243行出现了`strpos(): Empty needle`的警告。 - 引用[2]介绍了`mb_strpos()`函数的用法,并指出如果查找的字符串不存在则返回false,同时提醒在处理中文字符时要声明编码。 - 引用[3]展示了`strpos`函数PHP源码中解析参数的部分,其中`zend_parse_parameters`用于获取参数,参数包括haystack(被搜索的字符串)、needle(要搜索的字符串)和offset(偏移量)。 - 引用[4]提到了PHP内部对`strpos`等函数的优化,其中memchr函数用于在haystack中查找needle的第一个字符,如果找不到则返回NULL。 - 引用[5]则提供了memchr和memcmp函数的声明和作用,这些函数strpos的实现中被使用。 错误信息"Empty needle"直接意思是:我们传递给`strpos()`函数的needle参数是空的。也就是说,我们在调用`strpos($haystack, $needle)`时,$needle是一个空字符串。 根据PHP官方文档,strpos()函数的第二个参数(即needle)不能为空字符串。否则,从PHP 7.3.0开始,会抛出Warning(警告);而在PHP 8.0.0中,会抛出ValueError异常。 因此,解决这个问题的关键就是确保在调用strpos()函数时,needle不能为空字符串。 解决步骤: 1. 检查调用strpos()的地方,确保传递的needle非空。 2. 在调用strpos()之前,对needle变量进行非空验证。 3. 如果needle有可能为空,则需要根据业务逻辑决定如何处理(例如跳过查找或使用默认值等)。 根据引用[1]中提到的错误发生在Yii2框架的`vendor\yiisoft\yii2\log\Target.php`的第243行,我们可以推测这可能是框架内部代码的问题。因此,如果是框架本身的问题,可能需要升级框架版本或者查看官方是否有修复。 但是,用户的问题是如何解决strpos()函数的"Empty needle"错误,因此我们主要关注在自己的代码中如何避免。 具体解决方案: ### 1. 检查needle是否为空 在调用strpos()之前,先判断needle是否为空字符串。例如: ```php if (is_string($needle) && $needle === '') { // 处理空needle的情况,比如返回false或者跳过操作 return false; } $pos = strpos($haystack, $needle); ``` 或者使用条件判断: ```php if (!empty($needle)) { $pos = strpos($haystack, $needle); } else { // 处理空的情况 } ``` ### 2. 使用类型声明(PHP 7+) 如果使用PHP 7及以上版本,可以在函数定义时对参数进行类型声明,确保needle不是空字符串。但注意,strpos()本身是内置函数,我们无法修改它的定义,所以这个方法适用于我们自定义的函数。 ### 3. 错误抑制(不推荐) 使用错误控制运算符`@`可以抑制警告,但是这不是好的做法,因为它只是隐藏了错误,并没有真正解决问题。 ```php $pos = @strpos($haystack, $needle); ``` ### 4. 升级PHP版本 如果是由于PHP版本升级导致的(如从PHP7.3以下升级到7.3或以上),那么需要检查代码中所有调用strpos()的地方,确保needle不为空。 ### 5. 框架特定问题(如Yii2) 如果是Yii2框架报错,可能是框架的某个版本存在此问题。可以尝试升级Yii2到最新版本,因为官方可能已经修复。或者查看该文件的第243行,看看是什么代码导致了错误。 通常,在框架中我们不应该直接修改vendor目录下的文件,所以正确的方式是: - 检查自己的代码中传递给日志相关的参数是否有空字符串。 - 升级框架。 - 如果问题在框架中,可以在GitHub上查看相关issue,看是否有临时解决方案。 ### 代码示例 假设我们自己的代码中调用strpos(),可以这样写: ```php function safe_strpos($haystack, $needle, $offset = 0) { if ($needle === '') { // 根据需求返回false或者抛出异常 return false; } return strpos($haystack, $needle, $offset); } ``` 然后,在代码中调用这个safe_strpos函数,而不是直接调用strpos。 ### 总结 出现"Empty needle"错误是因为传入的needle参数是空字符串。解决办法是在调用strpos()之前确保needle非空。 如果错误发生在第三方框架(如Yii2)中,需要检查是否使用了会导致传递空字符串的配置或代码,并考虑升级框架版本。 最后,建议在开发环境中启用错误报告(E_ALL)以便及时发现此类问题。 §§相关问题§§ 1. 如何防止PHP中其他字符串函数出现类似错误(如mb_strpos)? 2.PHP 8.0及以上版本中,strpos()函数对空needle的处理有什么变化? 3. 如何在Yii2框架中避免传递空字符串给日志目标(Target)? </think>### PHP `strpos()`函数出现"Empty needle"错误的解决方案 该错误是由于调用`strpos()`函数时传入的`needle`参数为空字符串导致的。以下是详细解决方案: #### 原因分析 1. **函数规范要求**:`strpos(string $haystack, string $needle)`要求`$needle`必须是非空字符串 - 当`$needle = ""`时触发警告(PHP < 8.0)或抛出`ValueError`异常(PHP ≥ 8.0)[^1][^3] 2. **源码验证机制**:PHP底层使用`zend_parse_parameters()`校验参数,空字符串会被拒绝[^3] 3. **常见触发场景**: - 外部输入未过滤(如API参数、表单数据) - 字符串处理逻辑错误导致空值 - 框架配置错误(如Yii2日志组件配置问题)[^1] #### 解决方案 ##### 1. 基础防御(推荐) ```php // 方法1:前置条件校验 if (!empty($needle)) { $position = strpos($haystack, $needle); } else { // 处理空needle逻辑(如记录日志/返回默认值) } // 方法2:三元运算符简化 $position = ($needle !== '') ? strpos($haystack, $needle) : false; ``` ##### 2. 兼容PHP 8+的健壮方案 ```php try { $position = strpos($haystack, $needle); } catch (\ValueError $e) { // PHP 8+ 异常处理 error_log("Needle不能为空: " . $e->getMessage()); $position = false; } ``` ##### 3. 针对Yii2框架的特殊处理 在`config/web.php`中检查日志组件配置: ```php 'log' => [ 'targets' => [ [ 'class' => 'yii\log\FileTarget', 'logVars' => [], // 确保非空数组,避免空needle 'prefix' => function() { return '[custom]'; } // 添加有效前缀 ], ], ], ``` #### 最佳实践建议 1. **输入验证**:所有外部输入必须过滤 ```php $needle = trim($_GET['needle'] ?? ''); if (empty($needle)) die("搜索内容不能为空"); ``` 2. **函数封装**:创建安全包装函数 ```php function safe_strpos($haystack, $needle) { return (is_string($needle) && $needle !== '') ? strpos($haystack, $needle) : false; } ``` 3. **调试定位**:快速定位空值来源 ```php // 在调用前添加调试语句 if ($needle === '') { debug_print_backtrace(); error_log("空needle出现在: " . __FILE__ . " line " . __LINE__); } ``` > **关键原则**:所有传递给`strpos()`, `mb_strpos()`, `strstr()`等字符串搜索函数的`needle`参数,必须确保是非空字符串[^2][^4]。PHP源码底层使用`memchr()`等函数执行搜索时,空needle会导致未定义行为[^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值