

统一声明:
1.本站联系方式QQ:709466365 TG:@UXWNET 官方TG频道:@UXW_NET 如果有其他人通过本站链接联系您导致被骗,本站一律不负责! 2.需要付费搭建请联系站长QQ:709466365 TG:@UXWNET 3.免实名域名注册购买- 游侠云域名 4.免实名国外服务器购买- 游侠网云服务
从0到1:PHP-CLI入门,先搞定这3件事
先别急着写复杂脚本,入门阶段把“环境配置、第一个脚本、参数解析”搞懂,就能解决80%的基础需求——我当初就是靠这3件事,从“对着命令行发懵”变成“能写出能用的脚本”。
先确认:你的PHP已经支持CLI模式
其实PHP默认就带CLI模式,不用额外装插件。你打开终端(Linux/Mac是Terminal,Windows是CMD),输php -v
,如果能显示类似这样的结果,就说明没问题:
PHP 8.2.12 (cli) (built: Oct 25 2023 11:49:53) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.12, Copyright (c) Zend Technologies
要是没反应,就得装PHP-CLI组件了。不同系统的安装方法我整理了个表格,照着做就行:
系统 | 安装命令 | 验证方式 |
---|---|---|
Ubuntu/Debian | sudo apt install php-cli | php -v |
CentOS/RHEL | sudo yum install php-cli | php -v |
Windows | 下载PHP安装包,勾选“CLI”组件 | cmd中输入php.exe -v |
写第一个CLI脚本:比你想的还简单
环境搞定后,写第一个脚本只需要3步。我第一次写的是“计算文件大小”的脚本——想知道某个文件有多大,不用打开文件管理器,终端输个命令就行。
步骤1:新建一个file_size.php
文件,写代码:
<?php // 从命令行获取文件路径($argv[1]是第一个参数)
$filePath = $argv[1];
// 检查文件是否存在
if (!file_exists($filePath)) {
echo "Error:文件 {$filePath} 不存在!n";
exit(1); // 非0退出表示脚本执行失败
}
// 计算文件大小(转换为KB,保留2位小数)
$fileSize = filesize($filePath);
$sizeInKB = round($fileSize / 1024, 2);
// 输出结果
echo "文件 {$filePath} 的大小是:{$sizeInKB} KBn";
?>
步骤2:在终端运行脚本:
php file_size.php test.txt
如果test.txt
存在,就能看到类似“文件 test.txt 的大小是:12.34 KB”的输出;如果不存在,会提示错误。
这里要重点说下$argv
——这是PHP预定义的命令行参数数组,$argv[0]
永远是脚本本身的文件名(比如file_size.php
),$argv[1]
才是你输入的第一个参数。我之前犯过傻,把$argv[0]
当成用户输入的文件路径,结果脚本一直输出“file_size.php不存在”,后来查了文档才反应过来——原来$argv
的结构是固定的,第一个元素永远是脚本自己。
处理复杂参数:用getopt()搞定选项式输入
光用$argv
处理简单参数没问题,但要是碰到带选项的参数(比如php script.php name=张三 age=20
),就得用getopt()
函数了。我写批量发送邮件的脚本时,就用getopt()
处理过“收件人邮箱”“邮件主题”这类参数。
比如写一个send_email.php
脚本:
<?php // 定义可接受的参数:短选项(-e=邮箱、-s=主题)、长选项(email、subject)
$options = getopt('e:s:', ['email:', 'subject:']);
// 解析参数(优先用短选项,没有就用长选项,再没有就用默认值)
$recipient = $options['e'] ?? $options['email'] ?? 'default@example.com';
$subject = $options['s'] ?? $options['subject'] ?? '测试邮件';
$content = "这是一封来自PHP-CLI的测试邮件!";
// 模拟发送邮件(实际项目用PHPMailer等库)
echo "正在给 {$recipient} 发送主题为『{$subject}』的邮件...n";
echo "发送成功!n";
?>
运行脚本时,可以用短选项:
php send_email.php -e test@example.com -s "CLI测试邮件"
也可以用长选项:
php send_email.php email test@example.com subject "CLI测试邮件"
甚至混合用:
php send_email.php -e test@example.com subject "CLI测试邮件"
getopt()
的规则很简单:
'e:s:'
表示-e
和-s
后面必须跟参数(冒号:
表示“需要参数”);['email:', 'subject:']
表示email
和subject
后面必须跟参数;$options
数组里,键是短选项或长选项的名称。提醒下:如果参数里有空格(比如subject=Hello World
),一定要用引号把参数包起来——否则Hello
和World
会被当成两个不同的参数,$options['subject']
只会拿到Hello
,后面的World
会跑到$argv
的其他位置。
从会用到好用:PHP-CLI进阶,这4个技巧让脚本更稳
入门之后,你可能会发现——写出来的脚本能跑,但不够稳、不够快:比如定时脚本重复运行导致数据重复,批量处理时内存爆炸,跑异步任务占满CPU。我帮朋友优化过一个导入10万条CSV数据到MySQL的脚本,原来要跑30分钟,优化后只要5分钟;还修复过一个监控脚本,因为没处理进程冲突,导致同一时间跑了5个实例,把数据库连接池占满。下面这4个技巧,是我从这些经历里 出来的“稳脚本秘诀”。
很多CLI脚本是定时执行的(比如每分钟跑一次同步任务),如果前一次脚本还没跑完,下一次又启动,就会导致重复处理任务(比如重复同步数据到数据库)。我解决这个问题的办法是——创建PID文件:脚本启动时,检查是否有一个叫script.pid
的文件,如果有,就查这个文件里的进程ID(PID)是不是还在运行;如果在,就退出脚本;如果不在(比如上一次脚本崩溃没删PID文件),就删掉旧的PID文件,再把当前进程的PID写进去。
代码大概长这样(以同步数据库的脚本为例):
<?php // 定义PID文件路径(用__DIR__确保路径正确)
$pidFile = __DIR__ . '/sync_db.pid';
// 检查PID文件是否存在
if (file_exists($pidFile)) {
$existingPid = trim(file_get_contents($pidFile));
// 检查进程是否在运行(posix_kill(pid, 0)不发送信号,只检查进程存在性)
if (function_exists('posix_kill') && posix_kill($existingPid, 0)) {
echo "Error:脚本已在运行(PID:{$existingPid}),请勿重复启动!n";
exit(1);
}
// 进程不存在,删除旧PID文件
unlink($pidFile);
}
// 写入当前进程的PID
file_put_contents($pidFile, getmypid());
//
// 核心任务:同步数据库(示例代码)
//
echo "开始同步数据库...n";
// 模拟同步过程(实际项目用PDO或mysqli)
sleep(5); // 代替真实的同步操作
echo "数据库同步完成!n";
// 任务完成,删除PID文件
unlink($pidFile);
?>
这里要注意:
posix_kill()
函数需要PHP安装posix扩展(Linux系统一般默认装了,Windows没有);__DIR__
来定义PID文件路径,避免“工作目录”的问题(后面会讲);CLI脚本跑批量任务时,最容易踩的坑就是内存泄漏。我之前写过一个导入10万条CSV数据到MySQL的脚本,没做任何内存优化,结果导入到第5万条时,内存占用从100M涨到1.2G,直接把服务器的2G内存吃满,导致其他服务卡崩。后来我用了3个优化技巧,把内存占用降到了300M以内:
技巧1:分批处理,每批清理变量
比如导入CSV数据时,不要一次性把10万条数据读进内存,而是每1000条处理一次,处理完就清理变量,再手动触发垃圾回收(GC)。代码示例:
<?php // 打开CSV文件
$csvFile = fopen('users.csv', 'r');
// 跳过表头(如果有的话)
fgetcsv($csvFile);
$batch = [];
$batchSize = 1000; // 每批处理1000条
while (($row = fgetcsv($csvFile)) !== false) {
$batch[] = [
'name' => $row[0],
'email' => $row[1],
'age' => $row[2]
];
// 达到批次大小,插入数据库
if (count($batch) >= $batchSize) {
insertIntoDatabase($batch); // 插入数据库的函数
unset($batch); // 释放变量内存
$batch = []; // 重置批次数组
gc_collect_cycles(); // 手动触发垃圾回收
}
}
// 处理剩余的不足1000条数据
if (!empty($batch)) {
insertIntoDatabase($batch);
}
fclose($csvFile);
echo "数据导入完成!n";
?>
这里的关键是unset($batch)
和gc_collect_cycles()
——unset()
会释放变量占用的内存,gc_collect_cycles()
会强制PHP回收那些“不再使用的内存”,避免内存越积越多。
技巧2:限制内存使用上限
用ini_set('memory_limit', '512M')
给脚本设一个内存上限——比如我把导入脚本的内存限制设为512M,这样即使有内存泄漏,也不会吃满服务器的内存。代码开头加:
<?php // 限制脚本最多使用512M内存
ini_set('memory_limit', '512M');
?>
技巧3:关闭不必要的扩展
CLI脚本不需要Web相关的扩展(比如curl
、gd
、session
),可以在脚本开头关闭这些扩展,减少内存占用:
<?php // 关闭不需要的扩展
ini_set('extension=curl.so', 0);
ini_set('extension=gd.so', 0);
ini_set('session.auto_start', 0);
?>
CLI脚本最适合做异步任务——比如用户注册后发送验证邮件、下单后发送短信通知、生成用户账单PDF。这些任务如果放在Web请求里做,会让用户等好几秒(比如发送邮件要连接SMTP服务器,可能得1-2秒),但用CLI脚本异步处理,Web端只要把任务丢进队列,就能立即响应。
我帮朋友的社区网站做过一个“用户注册通知”的异步任务,流程是这样的:
步骤1:Web端把任务推到Redis队列
用户注册成功后,Web端将“收件人邮箱”“验证token”等信息JSON编码,推到Redis的列表(List)里:
<?php // 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 构造任务数据
$taskData = [
'email' => $user['email'],
'username' => $user['username'],
'verify_token' => $user['verify_token']
];
// 把任务推到Redis列表(lpush:从左边插入)
$redis->lpush('register_notify_queue', json_encode($taskData));
echo "注册成功!验证邮件将很快发送到你的邮箱~n";
?>
步骤2:CLI脚本从队列里取任务并处理
写一个process_notify.php
脚本,用brpop()
阻塞读取Redis列表(没有任务时会等待,不会占CPU):
<?php // 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
echo "开始监听注册通知队列...n";
while (true) {
// brpop:阻塞读取队列(从右边弹出,没有任务时等待0秒=一直等)
$result = $redis->brpop('register_notify_queue', 0);
// $result[0]是队列名,$result[1]是任务数据
$taskData = json_decode($result[1], true);
// 处理任务:发送验证邮件(实际项目用PHPMailer)
sendVerifyEmail($taskData['email'], $taskData['username'], $taskData['verify_token']);
echo "处理任务成功:给 {$taskData['email']} 发送了验证邮件n";
}
?>
这里的关键是brpop()
——它是阻塞式的,比用sleep(1)
轮询队列高效多了(轮询会每隔1秒查一次Redis,占CPU;brpop()
只有当队列有数据时才会唤醒进程)。
最后说几个我踩过的高频坑,帮你省点时间:
坑1:权限问题:脚本执行用户要和目标目录匹配
CLI脚本的执行用户是终端登录的用户(比如root、www-data),如果脚本要读写Web目录(比如/var/www/html
),得确保执行用户有权限。我之前写的脚本用root运行,生成的文件权限是700
(只有root能读),结果Web
本文常见问题(FAQ)
怎么知道自己的PHP有没有开启CLI模式?
其实PHP默认就带CLI模式,不用额外装插件。你打开终端(Linux/Mac是Terminal,Windows是CMD),输入php -v,如果显示的结果里有“cli”字样(比如PHP 8.2.12 (cli)),就说明已经支持了。
要是没反应,可能是没装PHP-CLI组件,得按对应系统的命令安装,比如Ubuntu/Debian用sudo apt install php-cli,Windows下载安装包时勾选“CLI”组件就行。
第一次写PHP-CLI脚本,有什么容易踩的坑?
最要注意的是$argv数组的结构——$argv[0]永远是脚本本身的文件名(比如file_size.php),$argv[1]才是你输入的第一个参数。比如你写计算文件大小的脚本,别把$argv[0]当成用户输入的文件路径,不然会提示“file_size.php不存在”,我之前就犯过这错。
脚本执行失败时最好用exit(1)退出,非0状态码能让系统或定时任务知道执行出错了,比如文件不存在时exit(1),这样运维能及时发现问题,别用exit(0)(0表示成功)。
CLI脚本要处理带选项的参数(比如name=张三),用什么方法?
可以用PHP的getopt()函数,它专门用来处理带选项的参数,能同时支持短选项(比如-e=邮箱、-s=主题)和长选项(比如email=邮箱、subject=主题)。比如定义$options = getopt(‘e:s:’, [’email:’, ‘subject:’]),就能解析-e/-s这样的短选项,或者email/subject这样的长选项。
解析的时候可以用“??”运算符兼容不同输入,比如$recipient = $options[‘e’] ?? $options[’email’] ?? ‘default@example.com’,优先用短选项,没有就用长选项,再没有就用默认值,这样用户怎么输入都能兼容。
CLI脚本定时运行,怕重复启动导致数据冲突怎么办?
可以用PID文件解决——脚本启动时先检查有没有对应的PID文件(比如sync_db.pid),如果有就取出里面的进程ID,用posix_kill(pid, 0)检查进程是不是还在运行;如果在,就输出错误退出;如果不在(比如上一次脚本崩溃没删文件),就删掉旧PID文件。
然后把当前进程的ID写进PID文件(用getmypid()获取),任务完成后再删掉这个文件。要注意用__DIR__定义PID文件路径,避免“工作目录”的问题,比如__DIR__ . ‘/sync_db.pid’,这样不管脚本在哪运行,路径都是对的。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!
8. 精力有限,不少源码未能详细测试(解密),不能分辨部分源码是病毒还是误报,所以没有进行任何修改,大家使用前请进行甄别!
站长QQ:709466365 站长邮箱:709466365@qq.com