PHP 多进程下载必应壁纸
手里拿着锤子,看什么都像是钉子
在放假的这几天,断断续续的看了老李关于 PHP 多进程的文章。
- PHP多进程初探 --- 开篇
- PHP多进程初探 --- 孤儿和僵尸
- PHP多进程初探 --- 信号
- PHP多进程初探 --- 利用多进程开发点儿东西吧
- PHP多进程初探 --- 再次谈daemon进程
- PHP多进程初探 --- 进程间通信二三事
在此基础上又看了下 owner888/phpspider 的多进程实现代码,这个是《我用爬虫一天时间“偷了”知乎一百万用户,只为证明PHP是世界上最好的语言 》一文所使用的程序。
等到自我感觉差不多已经掌握多进程时候,它就变成了我手中的锤子:
手里拿着锤子,看什么都像是钉子。
在《QueryList + Redis 下载壁纸》这篇文章中有提到,可以手动多开几个黑窗口提高壁纸下载速度。
正如文章中所说,在此之前,需要用到多进程来处理任务的时候都是用的这种“笨方法”。虽然在启动任务的时候比较麻烦,需要手动打开 n 个黑窗口,然后到指定目录下运行对应的脚本,但是在写代码的时候比较轻松,不用考虑多进程的可能导致的一些问题。
由于文中的壁纸站点倒闭了(与我无瓜),所以后面的代码换了一个站点来进行演示。
PHP 多进程的一些概念
关于 PHP 多进程,上面列出来的文章其实已经讲的差不多了,这里其实就是个观后总结,已经看完文章的可以跳过。
孤儿进程和僵尸进程
父进程在创建子进程后,需要负责子进程的回收,否则就会出现 孤儿进程
或 僵尸进程
。
孤儿进程
:父进程在创建子进程后,子进程还在运行的时候自己先退出了,导致子进程没了爹,就变成了孤儿进程,然后被 Linux 的 “孤儿进程福利院”init 进程
(进程 id 为 1)所收养。僵尸进程
:父进程在创建子进程后,子进程退出了,但是父进程没有对其进行回收,导致子进程变成了僵尸进程,子进程的进程 ID、文件描述符等依然保存在系统中,极大的浪费了系统资源,相比孤儿进程危害更大。
回收子进程
在父进程中通过 pcntl_wait() 或 pcntl_waitpid() 函数对子进程进行回收,上面提到的回收其实就是对子进程的状态收集。
pcntl_wait()
:等待或返回创建的子进程状态。该函数是阻塞的,所以当执行到该函数时会阻塞在这里,直到有子进程退出或终止。pcntl_waitpid()
:等待或返回创建的子进程状态。该函数是非阻塞的,也就是说当没有子进程需要处理时,它会返回 0 并继续执行后面的代码。
信号
信号是异步传送给进程的一种事件通知,进程无法预测何时会出现信号。
信号的产生有多种方式,比如在键盘上按下组合键 ctrl+c 或 ctrl+d 就会产生 SIGINT
信号并终止当前运行的程序;使用 posix_kill()
函数可以向指定的进程发送某种信号。
进程在收到信号后有以下三种处理方式。
直接忽略
:对信号不做任何处理,SIGSTOP
和SIGKILL
两种信号无法忽略,因为这两个信号是提供给用户停止或杀死进程最可靠的手段。捕获信号
:程序自定义信号处理逻辑。系统默认动作
:Linux 内核为每种信号都提供了默认动作,当程序没有主动捕获某种信号时,就会交由系统执行默认动作。大多数默认动作都是终止进程。
捕获信号的处理方式:先通过 pcntl_signal() 函数安装某个信号的回调函数,然后使用 pcntl_signal_dispatch() 调用每个等待信号通过 pcntl_signal()
安装的信号回调函数。
守护进程
非守护进程在启动后,在终端按下组合键 ctrl+c 或 ctrl+d 就会终止当前运行的程序。想要成为守护进程,首先要在父进程中创建一个子进程,然后通过 posix_setsid()
函数将该子进程作为会话的主进程,并退出父进程,断开与终端的连接。
代码实现
进程模型用的是单 Master 多 Worker 进程模型,Master 进程用于收集子进程的状态,一个 Worker 进程用于提取所有的壁纸下载地址,剩下 Worker 进程用于下载下载壁纸,因为下载比较耗时,所以需要多个 Worker 进程同时处理,下载壁纸的 Worker 进程数量可以自定义。
入口函数
首先看一下入口函数 run()
:
public function run()
{
// 检查运行环境
$this->checkEnv();
// 守护进程
$this->daemonize();
// 安装信号处理器
$this->installSignalHandler();
// 初始化 Redis
$this->initRedis();
// 初始化进程
$this->initWorkers();
// 监听子进程状态
$this->monitor();
}
run()
函数已经概括了程序的运行流程。
首先检查一下当前运行环境,是否在 linux 系统中、是否安装相关扩展,最后是关于信号派遣的,PHP 7.1 新增了 pcntl_async_signals() 函数,在此之前需要 declare() 配合 pcntl_signal_dispatch() 函数进行信号派遣。
protected function checkEnv()
{
if ('//' == \DIRECTORY_SEPARATOR) {
exit('目前只支持 linux 系统'.PHP_EOL);
}
if (!\extension_loaded( 'pcntl') ) {
exit('缺少 pcntl 扩展'.PHP_EOL);
}
if (!\extension_loaded( 'posix') ) {
exit('缺少 posix 扩展'.PHP_EOL);
}
if (version_compare(PHP_VERSION, 7.1, '<')) {
declare(ticks = 1);
} else {
// 启用异步信号处理
\pcntl_async_signals(true);
}
}
守护进程
守护进程上面已经介绍过,可以再配合代码注释理解。
protected function daemonize()
{
if (self::$options['daemonize'] !== true) {
return;
}
// 设置当前进程创建的文件权限为 777
umask(0);
$pid = \pcntl_fork();
if ($pid < 0) {
$this->log('创建守护进程失败');
exit;
} else if ($pid > 0) {
// 主进程退出
exit(0);
}
// 将当前进程作为会话首进程
if (\posix_setsid() < 0) {
$this->log('设置会话首进程失败');
exit;
}
// 两次 fork 保证形成的 daemon 进程绝对不会成为会话首进程
$pid = \pcntl_fork();
if ($pid < 0) {
$this->log('创建守护进程失败');
exit;
} else if ($pid > 0) {
// 主进程退出
exit(0);
}
}
初始化 Redis
初始化 Redis 就是从配置中获取 Redis 参数,然后实例化 Predis/Client。
protected function initRedis()
{
$this->redisClient = new Client(self::$options['redis']);
}
安装信号处理器
这里只安装了 SIGINT
和 SIGPIP
信号的处理器,收到 SIGINT
信号后,调用 stopAllWorkers()
方法给所有的 Worker 发送 SIGINT
信号,停止所有的 Worker。而收到 SIGPIPE
信号则忽略不做任何处理。
protected function installSignalHandler()
{
// 捕获 SIGINT 信号,终端中断
\pcntl_signal(SIGINT, [$this, 'stopAllWorkers'], false);
// 捕获 SIGPIPE 信号,忽略掉所有管道事件
\pcntl_signal(\SIGPIPE, \SIG_IGN, false);
}
protected function stopAllWorkers()
{
if (self::$maserPid !== \posix_getpid()) {
// 子进程
unset(self::$workers[$this->workerId]);
exit(0);
}
// 父进程
foreach (self::$workers as $pid) {
// 给 worker 进程发送关闭信号
\posix_kill($pid, SIGINT);
}
}
初始化进程
接下来就是初始化进程,先通过 posix_getpid() 函数获取当前进程的进程 ID 作为 Master 进程 ID。
再通过 forkWorker()
方法创建提取壁纸地址进程,该进程的处理方法是 extractWallpaperUrl()
。因为 work id 为 0 的留给了 Master 进程,所以这里的 work id 从 1 开始。
然后根据配置项 worker_num
创建指定数量的下载壁纸的进程,该进程的处理方法是 downloadWallpaper()
方法。
protected function initWorkers()
{
self::$maserPid = \posix_getpid();
$this->forkWorker(1, [$this, 'extractWallpaperUrl']);
$workerNum = (int) self::$options['worker_num'];
for ($i = 0; $i < $workerNum; $i++) {
$this->forkWorker($i + 2, [$this, 'downloadWallpaper']);
}
}
上面提到了 forkWorker
方法,这个方法其实跟老李文章中写的创建子进程代码差不多,在父进程中记录子进程的进程 ID,在进程中调用匿名函数处理业务逻辑。
protected function forkWorker($workerId, $callback)
{
$pid = \pcntl_fork();
if ($pid > 0) {
// 父进程记录子进程 PID
self::$workers[$workerId] = $pid;
} elseif ($pid === 0) {
// 子进程处理业务逻辑
$this->workerId = $workerId;
if ($callback instanceof \Closure) {
$callback();
} else if (isset($callback[1]) && is_object($callback[0])) {
\call_user_func($callback);
}
exit(0);
} else {
$this->log('进程创建失败');
exit;
}
}
壁纸采集逻辑
提取壁纸地址和下载壁纸的逻辑跟之前写的那篇文章差不多。
protected function extractWallpaperUrl()
{
$this->log('提取壁纸地址进程启动...');
$page = 1;
do {
$html = \file_get_contents("https://bing.ioliu.cn/?p={$page}");
\preg_match_all('/<img([^>]*)\ssrc="([^\s>]+)"/', $html,$matches);
if (empty($matches[2]) || \count($matches[2]) === 3) {
$this->log('壁纸地址提取完毕, 当前页码: %s', $page);
break;
}
$urls = \array_unique(\array_filter($matches[2]));
if (!empty($urls)) {
// 将壁纸 url 放入队列中
$this->redisClient->sadd(self::$options['queue_key'], $urls);
}
$this->log('提取壁纸数量: %s, 当前页面: %s', count($urls), $page++);
} while (true);
}
protected function downloadWallpaper()
{
$this->log('下载壁纸进程启动...');
while (self::$freeTime < self::$options['max_free_time']) {
$url = $this->redisClient->spop(self::$options['queue_key']);
if (empty($url)) {
$this->log('空闲时间: %s/%ss', self::$freeTime++, self::$options['max_free_time']);
\sleep(1);
continue;
}
try {
$result = $this->saveWallpaper($url);
if (!$result) {
$this->redisClient->sadd(self::$options['queue_key'], [$url]);
}
} catch (\Exception $e) {
$result = false;
$this->log('保存壁纸异常: %s', $e->getMessage());
}
$this->log('壁纸下载%s, %s', $result ? '成功' : '失败', $url);
}
}
监听子进程状态
进程到目前已经创建完了,接下来就是父进程对子进程状态进行监听,如果该已经已退出就将它从 self::workers
数组中删除,如果没有在运行中的子进程则退出父进程。
在 acceptSignal()
方法中通过 pcntl_wait()
函数阻塞获取退出的进程 ID。
protected function monitor()
{
while (true) {
$pid = $this->acceptSignal();
if ($pid > 0) {
$this->log('子进程退出信号, PID: %s', $pid);
// 翻转 workers 的键值
$workers = \array_flip(self::$workers);
$workerId = $workers[$pid];
// 删除子进程
unset(self::$workers[$workerId]);
// 如果没有在运行的子进程则退出主进程
count(self::$workers) === 0 && exit(0);
} else {
$this->log('其它信号, PID: %s', $pid);
exit(0);
}
}
}
protected function acceptSignal()
{
if (\version_compare(PHP_VERSION, 7.1, '>=')) {
return \pcntl_wait($status, WUNTRACED);
}
// 调用等待信号的处理器
\pcntl_signal_dispatch();
$pid = \pcntl_wait($status, WUNTRACED);
\pcntl_signal_dispatch();
return $pid;
}
使用
$options
为构造函数的可选参数,以下为配置项的默认参数。
$options = [
'daemonize' => false, // 是否 daemon 化
'worker_num' => 3, // 下载壁纸进程数量
'max_free_time' => 60, // 最大空闲时间(秒)
'save_dir' => __DIR__.'/wallpaper', // 壁纸保存位置
'queue_key' => 'wallpaper_url_queue', // 壁纸下载地址的 redis key
'redis' => [ // redis 配置
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
],
];
$wallpaper = new BingWallpaperDownloader($options);
$wallpaper->run();
运行效果
vagrant@homestead:~/code/her-cat/download_bing_wallpaper$ php index.php
[2020-02-02 10:41:34] [worker-1] 提取壁纸地址进程启动...
[2020-02-02 10:41:34] [worker-3] 下载壁纸进程启动...
[2020-02-02 10:41:34] [worker-2] 下载壁纸进程启动...
[2020-02-02 10:41:34] [worker-4] 下载壁纸进程启动...
[2020-02-02 10:41:35] [worker-1] 提取壁纸数量: 12, 当前页面: 1
[2020-02-02 10:41:35] [worker-2] 壁纸下载成功, http://h1.ioliu.cn/bing/NutcrackerSeason_EN-AU8373379424_1920x1080.jpg
[2020-02-02 10:41:35] [worker-3] 壁纸下载成功, http://h1.ioliu.cn/bing/zhenghe_ZH-CN9628081460_1920x1080.jpg
[2020-02-02 10:41:36] [worker-4] 壁纸下载成功, http://h1.ioliu.cn/bing/MonumentFountain_EN-AU10536043652_1920x1080.jpg
[2020-02-02 10:41:37] [worker-2] 壁纸下载成功, http://h1.ioliu.cn/bing/JeanLafitte_EN-AU11428973003_1920x1080.jpg
[2020-02-02 10:41:37] [worker-1] 提取壁纸数量: 12, 当前页面: 2
[2020-02-02 10:41:37] [worker-3] 壁纸下载成功, http://h1.ioliu.cn/bing/MorondavaBaobab_EN-AU11363642614_1920x1080.jpg
[2020-02-02 10:41:37] [worker-3] 壁纸下载成功, http://h1.ioliu.cn/bing/SnowHare_ZH-CN9767012872_1920x1080.jpg
[2020-02-02 10:41:38] [worker-4] 壁纸下载成功, http://h1.ioliu.cn/bing/ShenandoahAutumn_EN-AU11784755049_1920x1080.jpg
^C[2020-02-02 10:41:38] [worker-0] 其它信号, PID: -1
保存的壁纸
vagrant@homestead:~/code/her-cat/download_bing_wallpaper/wallpaper$ ls
AbstractSaltBeds_ZH-CN8351691359_1920x1080.jpg MauiEucalyptus_ZH-CN5616197787_1920x1080.jpg
AcadiaBlueberries_ZH-CN6014510748_1920x1080.jpg may1_ZH-CN8582006115_1920x1080.jpg
AdelieBreeding_ZH-CN1750945258_1920x1080.jpg MeerkatHuddle_ZH-CN1358126294_1920x1080.jpg
AdobeSantaFe_EN-AU3063917358_1920x1080.jpg MeerkatMob_ZH-CN3788674757_1920x1080.jpg
AerialKluaneNP_ZH-CN4080112842_1920x1080.jpg MeteorCrater_EN-AU9993563603_1920x1080.jpg
最后
完整代码:https://github.com/her-cat/wallpaper_crawler/blob/master/BingWallpaperDownloader.php
关于 PHP 多进程的实践到这里就结束了,目前来看代码好像没啥太问题,后面有问题再来改吧。
溜了...
原文地址:https://her-cat.com/2020/02/02/php-multi-process-download-bing-wallpaper.html
PHP 多进程下载必应壁纸的更多相关文章
- java 必应壁纸批量下载
基于java 必应壁纸批量下载 - rookie丶k - 博客园 (cnblogs.com)实现 上面代码运行本地有点小问题,改了改 1.ssl验证 2.请求头 3.需要优化下载速度,多线程方式(还不 ...
- [uwp]MVVM模式实战之必应壁纸查看器
最近学习MVVM,至于什么是MVVM我也在这儿不多说了,一是关于它的解释解释网上非常多,二是我怕自己讲不清,误导自己没关系,误导别人就不好了.. 好了,废话结束,看是实战...... 这个必应壁纸的d ...
- Python 爬取必应壁纸
import re import os import requests from time import sleep headers = { "User-Agent": (&quo ...
- 如何使用 Github Actions 自动抓取每日必应壁纸?
如何白嫖 Github 服务器自动抓取必应搜索的每日壁纸呢? 如果你访问过必应搜索网站,那么你一定会被搜索页面的壁纸吸引,必应搜索的壁纸每日不同,自动更换,十分精美.这篇文章会介绍如何一步步分析出必应 ...
- 使用Qt实现一个必应壁纸客户端
概要 必应的每日壁纸很好看,但是看不到一周以前的壁纸图片,日前使用python开发了必应壁纸收集站,可惜这样的收集站只能在线浏览,我在想要是有一款软件能够下载每日必应壁纸,并应用到windows的桌面 ...
- Python爬虫实例(六)多进程下载金庸网小说
目标任务:使用多进程下载金庸网各个版本(旧版.修订版.新修版)的小说 代码如下: # -*- coding: utf-8 -*- import requests from lxml import et ...
- php利用curl实现多进程下载文件类
批量下载文件一般使用循环的方式,逐一执行下载.但在带宽与服务器性能允许的情况下,使用多进程进行下载可以大大提高下载的效率.本文介绍PHP利用curl的多进程请求方法,实现多进程同时下载文件. 原理: ...
- python之爬虫-必应壁纸
python之爬虫-必应壁纸 import re import requests """ @author RansySun @create 2019-07-19-20:2 ...
- python之下载每日必应壁纸
#!/usr/bin/env python3 # -*- coding: utf-8 -*- __author__ = 'jiangwenwen' from bs4 import BeautifulS ...
随机推荐
- 开发掉坑(二)前端静态资源 Uncaught SyntaxError: Unexpected token <
某天,有同学反馈后台管理系统出现静态资源无法加载的问题. 复现如下: 进入首页. 点击侧边栏某个子功能,静态资源可正常访问到. 等待10分钟左右,点击侧边栏其他子功能,无法访问到静态资源. 查看控制台 ...
- Java日期时间API系列39-----中文语句中的时间语义识别(time NLP 输入一句话,能识别出话里的时间)原理分析
NLP (Natural Language Processing) 是人工智能(AI)的一个子领域.自然语言是人类智慧的结晶,自然语言处理是人工智能中最为困难的问题之一(来自百度百科). 其中中文更是 ...
- 【NX二次开发】Block UI 图层
属性说明 常规 类型 描述 BlockID String 控件ID Enable Logical 是否可操作 Group Logical ...
- 【NX二次开发】打开信息窗口UF_UI_open_listing_window
头文件:uf_ui_ugopen.h函数名:UF_UI_open_listing_window 函数说明:打开信息窗口 测试代码: #include <uf.h> #include < ...
- sql:group by和 max
通过group by,having,max实现查询出每组里指定列中最大的内容 例如:我需要实现的功能是 获取每个模块中点击量最大的内容(表中有许多内容,内容里) 我写的查询语句如下 查询结果如下: 然 ...
- 「10.15」梦境(贪心)·玩具(神仙DP)·飘雪圣域(主席树\树状数组\莫队)
A. 梦境 没啥可说的原题.... 贪心题的常见套路我们坐标以左端点为第一关键字,右端点为第二关键字 然后对于每个转折点,我们现在将梦境中左端点比他小的区间放进$multiset$里 然后找最近的右端 ...
- 最多能创建多少个 TCP 连接?
我是一个 Linux 服务器上的进程,名叫小进. 老是有人说我最多只能创建 65535 个 TCP 连接. 我不信这个邪,今天我要亲自去实践一下. 我走到操作系统老大的跟前,说: "老操,我 ...
- (先导)Git Api对接:获取private_token的两种方式
" Git是一个开源的分布式版本控制系统,可以有效.高速地处理从很小到非常大的项目版本管理.在公司一般用于代码管理:开发用例管理平台时我们选择使用git来管理用例,期间使用了很多git ap ...
- 敢为人先,从阿里巴巴云原生团队实践Dapr案例,看分布式应用运行时前景
背景 Dapr是一个由微软主导的云原生开源项目,国内云计算巨头阿里云也积极参与其中,2019年10月首次发布,到今年2月正式发布V1.0版本.在不到一年半的时间内,github star数达到了1.2 ...
- nexus AD 集成配置
nexus AD 集成配置 管理用户登录 点击设置图标-->LDAP-->Create connection 进入AD 集成配置页面 Connection配置 User and group ...