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 ...
随机推荐
- 对标 Spring Boot & Cloud ,轻量框架 Solon 1.4.14 发布
Solon 是一个轻量的Java基础开发框架.强调,克制 + 简洁 + 开放的原则:力求,更小.更快.更自由的体验.支持:RPC.REST API.MVC.Job.Micro service.WebS ...
- 『居善地』接口测试 — 12、Moco框架介绍
目录 1.Mock功能介绍 2.Moco框架介绍 3.Moco框架在接口测试中的作用 4.Moco框架的优点 5.Moco框架的下载与启动 (1)Moco框架的下载 (2)Moco框架的启动 1.Mo ...
- 【NX二次开发】Block UI 从列表选择部件
属性说明 属性 类型 描述 常规 BlockID String 控件ID Enable Logical 是否可操作 Group ...
- 【模拟8.11】星空(差分转化,状压DP,最短路)
一道很好的题,综合很多知识点. 首先复习差分: 将原来的每个点a[i]转化为b[i]=a[i]^a[i+1],(如果是求和形式就是b[i]=a[i+1]-a[i]) 我们发现这样的方便在于我 ...
- 一台服务器能支撑多少个TCP连接
1. 困惑很多人的并发问题 在网络开发中,我发现有很多同学对一个基础问题始终是没有彻底搞明白.那就是一台服务器最大究竟能支持多少个网络连接?我想我有必要单独发一篇文章来好好说一下这个问题. 很多同学看 ...
- [Linux]Ansible自动化运维① - 入门知识
目录 一.Ansible 概述 1.1 Ansible 是什么 1.2 Ansible 优势 1.3 Ansible 特性 二.Ansible 入门 2.1 Ansible 架构 2.2 Ansibl ...
- Typecho 安装教程 -- Linux
1.下载宝塔面板 1 使用 SSH 连接工具,如堡塔SSH终端连接到您的 Linux 服务器后,挂载磁盘,根据系统执行相应命令开始安装(大约2分钟完成面板安装): 2 Centos安装脚本 yum i ...
- 9、SpringBoot整合之SpringBoot整合SpringSecurity
SpringBoot整合SpringSecurity 一.创建项目,选择依赖 选择Spring Web.Thymeleaf即可 二.在pom文件中导入相关依赖 <!-- 导入SpringSecu ...
- Blazor 组件入门指南
翻译自 Waqas Anwar 2021年3月19日的文章 <A Beginner's Guide to Blazor Components> [1] Blazor 应用程序是组件的组合, ...
- springboot集成swagger添加消息头(header请求头信息)
springboot集成swagger上篇文章介绍: https://blog.csdn.net/qiaorui_/article/details/80435488 添加头信息: package co ...