
每个知识点都配了可直接复制的代码示例,比如教你用getopt
快速解析name
这类长参数,用signal
处理脚本的优雅退出,甚至帮你避开命令行脚本常见的“坑”(比如路径错误、权限问题)。不管你是想补全PHP技能树,还是需要用命令行解决日常开发中的重复工作,跟着这份教程走,就能从“对着命令行发呆”到“熟练写出高效脚本”,真正把PHP-CLI变成提升效率的工具。
很多做PHP Web开发的朋友,平时写接口、调框架熟得很,但一碰到命令行脚本就发懵——比如要写个定时清理服务器日志的脚本,不知道怎么用PHP-CLI启动;想批量导入10万条CSV数据到数据库,Web脚本超时不说,还容易崩;甚至连“php -v”之外的命令行参数都认不全。其实PHP-CLI才是解决这些自动化、批量任务的神器,我去年帮一个做电商ERP的朋友优化过命令行脚本,原来他们导入订单数据要2小时,改完后只要15分钟,还不占Web服务器资源。今天就把我踩过的坑、摸透的技巧揉成实战干货,你跟着做,从新手到能写稳定的CLI脚本真的不难。
PHP-CLI入门:先搞懂这些基础,再写脚本不踩坑
先明确个核心认知:PHP-CLI是PHP的命令行接口,和Web模式最大的区别是——没有浏览器环境(比如没有$_GET/$_POST),但多了专门处理命令行的变量(比如$argv、$_SERVER[‘argv’])。我刚开始学的时候,犯过一个蠢错:写了个想接收“user=admin”参数的脚本,直接用$_GET[‘user’]去取,结果跑的时候啥都没有,后来才知道CLI模式下要自己解析参数——这是新手最容易踩的“环境差异坑”。
先把基础操作摸透:运行CLI脚本就两种方式,要么直接输“php 脚本名.php”,要么给脚本加可执行权限(chmod +x 脚本名.php),然后用“./脚本名.php”跑。比如你写了个test.php,内容是“echo ‘Hello CLI!’;”,直接输“php test.php”就能看到输出——这一步不难,但要注意参数处理:CLI模式下接收参数靠$argv数组,第一个元素是脚本文件名,后面的是你传的参数。比如“php test.php name=张三 123”,$argv[0]是“test.php”,$argv[1]是“name=张三”,$argv[2]是“123”。但手动拆分这些参数太麻烦,我推荐用PHP自带的getopt
函数,能直接解析短参数(比如“-n”)和长参数(比如“name”),举个实用的例子:
// 解析短参数-n(需要值)和长参数name(需要值)
$options = getopt('n:', ['name:']);
// 取参数值,优先短参数,没有就用长参数,默认值是“guest”
$username = $options['n'] ?? $options['name'] ?? 'guest';
echo "你输入的用户名是:{$username}";
我之前帮做电商ERP的朋友改脚本时,他原来用字符串分割处理参数,光处理“user=xxx”“-u xxx”这种情况就写了50行代码,用getopt
后缩到10行,还没再出过错——这就是“用对工具”的重要性。
再讲个基础但容易被忽略的点:CLI模式的php.ini配置。Web模式下的php.ini可能加载了mysqli、redis扩展,但CLI模式可能用的是另一个配置文件(比如Linux下是/etc/php/8.1/cli/php.ini)。我去年碰到过一个问题:写了个连接Redis的CLI脚本,跑的时候提示“Class ‘Redis’ not found”,查了半天才发现CLI的php.ini没开redis扩展——解决办法很简单,要么修改CLI的php.ini加“extension=redis.so”,要么运行脚本时用-d
参数临时开启:“php -d extension=redis.so 脚本名.php”。
为了帮你快速记牢常用命令,我整理了个PHP-CLI基础命令表:
参数 | 含义 | 示例 |
---|---|---|
-f | 运行指定PHP脚本 | php -f test.php |
-d | 临时设置php.ini选项 | php -d memory_limit=512M import.php |
-r | 直接运行PHP代码(不用写脚本文件) | php -r “echo date(‘Y-m-d H:i:s’);” |
help | 显示所有CLI参数帮助 | php help |
这个表是我平时常用的,你可以存下来——碰到不懂的参数,先查这个表,90%的问题都能解决。
PHP-CLI进阶:写出稳定脚本的核心技巧,我用了三年没踩坑
学会基础操作后,要写能扛住高负载、不崩的CLI脚本,得搞定这几个核心问题:多进程处理、内存优化、日志监控。
先讲多进程——这是解决批量任务的“效率神器”。比如导入10万条CSV数据到数据库,单进程循环读一行插一行,要2小时;开4个进程,每个进程处理2.5万条,加上数据库事务批量提交(每1000条commit一次),时间直接压到15分钟。我去年帮电商朋友优化的就是这种场景,用的是PHP的pcntl
扩展(需要先安装:apt install php8.1-pcntl
),核心代码逻辑是这样的:
// 要处理的总数据量
$total = 100000;
// 开启4个进程
$processNum = 4;
// 每个进程处理的数据量
$perProcess = ceil($total / $processNum);
for ($i = 0; $i < $processNum; $i++) {
$pid = pcntl_fork(); // fork子进程
if ($pid == -1) {
die('无法创建子进程');
} elseif ($pid == 0) {
// 子进程逻辑:处理从$i$perProcess到($i+1)$perProcess的数据
$start = $i $perProcess;
$end = min(($i+1)$perProcess
1, $total-1);
processData($start, $end); // 自己写的处理函数
exit(); // 子进程处理完要退出,避免继续fork
}
}
// 父进程回收子进程,避免僵尸进程
while (pcntl_waitpid(0, $status) != -1) {
$status = pcntl_wexitstatus($status);
echo "子进程退出,状态码:{$status}n";
}
这里要注意:pcntl_fork
出来的子进程会复制父进程的内存空间,所以别在父进程里打开大文件或数据库连接——不然每个子进程都会占一份资源,容易内存爆炸。我刚开始用的时候,没注意这点,开了5个进程,每个都连了一次数据库,结果数据库连接数直接超限,后来改成“父进程先连数据库,子进程复用连接”(其实PHP的pcntl
会自动继承文件描述符),才解决问题。
再讲内存优化——CLI脚本最容易犯的错是“内存泄漏”。比如循环读取大文件时,没及时unset
临时变量,导致内存越用越多,最后OOM(内存溢出)。我之前写过一个分析Nginx日志的脚本,读1G的日志文件,一开始没处理,内存用到800M,后来每读1000行就unset
掉临时数组,内存稳定在50M以内。举个例子:
$handle = fopen('access.log', 'r');
$lineCount = 0;
$tempData = [];
while (!feof($handle)) {
$line = fgets($handle);
$tempData[] = parseLine($line); // 解析日志行的函数
$lineCount++;
if ($lineCount % 1000 == 0) {
processTempData($tempData); // 处理临时数据
unset($tempData); // 释放内存
$tempData = []; // 重新初始化数组
}
}
fclose($handle);
这里的关键是“批量处理+及时释放”——别等把整个文件读完再处理,那样内存根本扛不住。 你可以用memory_get_usage()
函数监控内存使用情况,比如在脚本里加一行echo "当前内存使用:" . round(memory_get_usage()/1024/1024, 2) . "Mn";
,就能实时看到内存变化,方便调优。
最后讲日志和监控——CLI脚本一般跑在后台或定时任务里,没有界面看状态,所以日志要写得“够细”。我给朋友的脚本加了个日志函数,每一步关键操作都记录:
function logMessage($level, $message) {
$time = date('Y-m-d H:i:s');
$log = "{$time} [{$level}] {$message}n";
file_put_contents('/var/log/cli_script.log', $log, FILE_APPEND);
}
// 使用示例
logMessage('INFO', '开始导入订单数据,文件路径:/data/orders.csv');
logMessage('ERROR', '导入失败,原因:文件不存在');
除了写日志,还要会监控脚本状态——比如用ps aux | grep php
看脚本有没有在运行,用top
看CPU和内存占用,要是发现某个进程CPU占用100%,说明脚本里有死循环(比如while条件写错了);要是内存一直在涨,说明没unset
变量。我之前碰到过一个脚本,死循环读文件,CPU占了90%,查日志才发现feof
判断错了,把!feof
写成了feof
,结果一直循环读最后一行——这种问题,光看代码不一定能发现,监控工具才是“照妖镜”。
最后再给你个避坑清单,都是我踩过的疼坑:
session_start()
(CLI模式下没用)、header()
(没有HTTP响应),用了会报错;__DIR__
常量(比如__DIR__ . '/data.csv'
);extension_loaded('mysqli')
判断CLI模式下有没有加载mysqli扩展,没有的话就die掉,别等运行时才报错。你最近有没有碰到CLI脚本的问题?比如导入数据超时、参数解析错?可以试试我讲的getopt
函数或者多进程技巧,要是没效果,欢迎找我聊聊——毕竟我踩过的坑,能让你少走很多弯路。
本文常见问题(FAQ)
PHP-CLI脚本怎么运行?和Web脚本有啥不一样?
PHP-CLI脚本运行有两种方式,要么直接输“php 脚本名.php”,要么给脚本加可执行权限(chmod +x 脚本名.php)后用“./脚本名.php”跑。和Web脚本最大的区别是环境——Web脚本能拿到浏览器传的$_GET/$_POST参数,但CLI模式没有这些,得用$argv或$_SERVER[‘argv’]来接命令行参数,比如你传“php test.php name=张三”,$argv[1]就是“name=张三”;另外CLI脚本没有HTTP响应那套东西,别用header()这种Web函数,会报错。
命令行参数怎么解析?比如想接收user=admin这种参数该咋弄?
刚开始可以用$argv数组自己拆,比如“php test.php user=admin”里,$argv[0]是脚本名,$argv[1]是“user=admin”,但手动拆字符串太麻烦。推荐用PHP自带的getopt函数,能直接解析短参数(比如-n)和长参数(比如name),比如写“$options = getopt(‘u:’, [‘user:’])”,就能拿到-u或user后面的值,再用$options[‘u’]或$options[‘user’]取就行,比自己拆省事儿多了。
CLI脚本处理大文件时内存老不够用,咋优化?
核心是“批量处理+及时释放内存”。比如读1G的日志文件,别等整个文件读完再处理,每读1000行就把临时数据处理掉,然后unset临时数组。像我之前写的日志分析脚本,一开始没处理内存用到800M,后来每1000行unset一次临时数据,内存稳定在50M以内。另外可以用memory_get_usage()函数监控内存,比如在脚本里加一行“echo current memory usage: ” . round(memory_get_usage()/1024/1024, 2) . “Mn””,实时看内存变化,方便调优。
想让CLI脚本多进程跑,比如批量导入数据,该咋写?
可以用pcntl扩展(得先装,比如Ubuntu下输“apt install php8.1-pcntl”),通过pcntl_fork()创建子进程。比如要处理10万条数据,开4个进程,每个进程处理2.5万条——循环里fork子进程,子进程里处理对应区间的数据,处理完要exit(),不然会继续fork新进程;父进程要用pcntl_waitpid回收子进程,避免僵尸进程。我去年帮电商朋友优化导入脚本,用这方法把原来2小时的导入时间缩到15分钟,还不占Web服务器资源。
CLI脚本里的文件路径老是错,比如想读data.csv却找不到,咋解决?
因为CLI脚本的工作目录不一定是脚本所在目录,比如你在/home/user目录下运行/var/www/script.php,脚本里写“data.csv”会指向/home/user/data.csv,而不是/var/www/data.csv。解决办法是用绝对路径,比如用__DIR__常量(代表脚本所在目录的绝对路径),写成“__DIR__ . ‘/data.csv’”,这样不管在哪运行,都能找到脚本所在目录下的data.csv,再也不会找错文件了。