艾逗笔

有逻辑的脑子万里挑一。

在php中使用curl多线程下载图片

2016-12-30


遇到一个需求:要下载这个网站http://www.laredoute.com/上面的商品图片到本地。 分析了一下,这个网站是一个国外的站点,受 cdn 节点的影响,在国内打开的速度比较慢。另一方面,要下载的商品图片较大,单张图片的大小有超过 200kb 的。 现在的需求是,要在短时间内批量下载该网站上面的商品图片到本地,鉴于这两点考虑,如果使用 php 来做的话,单纯的用 file_get_contents 可能行不通,于是想到了用 php 的 curl 库来处理。

虽然用 curl 来下载图片比起用 file_get_contents 来处理效率更高,稳定性更好,但是如果使用 curl 单线程来处理的话,效率并不会得到非常明显的提升。所幸的是,curl 库提供了 curl_multi 功能,让我们可以使用 curl 多线程来处理业务逻辑,可以显著的提升效率。下面拿下载上述站点上面的 8 张图片为例,分别使用 curl 单线程和 curl 多线程来进行图片下载,并对两者的效率做一个比较。

single.php:curl 单线程下载图片

<?php

/**
 * 单线程下载远程图片
 * @author 艾逗笔<765532665@qq.com>
 */

set_time_limit(300);                        // 设置程序执行超时时间为5分钟

// 要下载的图片数组
$pics = array(
    'http://media.laredoute.com/products/1200by1200/37/01/01/37010132_90_CO_1_2850804.jpg',
    'http://media.laredoute.com/products/1200by1200/37/01/01/37010132_10_CO_2_2850799.jpg',
    'http://media.laredoute.com/products/1200by1200/57/02/02/57020206_1_CO_1_057020206-9be173d2-6bc3-44ba-b9cb-111010ed0051.jpg',
    'http://media.laredoute.com/products/1200by1200/37/01/01/37010132_10_CO_1_2850800.jpg',
    'http://media.laredoute.com/products/641by641/35/00/42/350042687_1_CO_1_7639577.jpg',
    'http://media.laredoute.com/products/641by641/35/00/34/350034174_1_CO_1_7669890.jpg',
    'http://media.laredoute.com/products/641by641/35/00/42/350042718_1_CO_1_7009919.jpg',
    'http://media.laredoute.com/products/641by641/35/00/39/350039748_1_CO_3_6958274.jpg'
);

$beginTime = time();            // 开始下载图片的时间
$lastTime = $beginTime;         // 上一次下载图片的时间
$count = 0;                     // 计数器
echo 'begin download at ' . date('Y-m-d H:i:s', $beginTime) . '<br/>';      // 输出开始下载图片的时间

// 循环下载图片
foreach ($pics as $k => $v) {
    $ch = curl_init();                                      // 初始化curl句柄
    curl_setopt($ch, CURLOPT_URL, $v);                      // 设置要下载的图片
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);            // 设置获取图片内容而不直接在浏览器输出
    curl_setopt($ch, CURLOPT_HEADER, 0);                    // 设置只获取图片内容,不返回header头信息
    $content = curl_exec($ch);                              // 获取图片内容
    curl_close($ch);                                        // 关闭curl句柄
    $picName = substr($v, strrpos($v, '/')+1);              // 获取图片名称
    $savePath = './single/';                                // 设置保存图片的路径
    if (!is_dir($savePath)) {                               // 如果图片路径不存在,则创建路径
        @mkdir($savePath, 0777);
    }
    $saveName = $savePath . $picName;                       // 设置图片保存为的文件名称
    $fp = @fopen($saveName, 'w');                           // 打开文件
    fwrite($fp, $content);                                  // 把获取到的图片内容写入到新图片
    fclose($fp);                                            // 关闭文件句柄
    $nowTime = time();                                      // 当前时间
    $takeTime = $nowTime - $lastTime;                       // 下载此张图片消耗的时间
    ++$count;                                               // 计数器+1
    echo 'downloaded ' . $count . 'th picture take time ' . $takeTime . 's<br/>';               // 输出下载当前图片消耗的时间
    $lastTime = $nowTime;                                   // 把当前时间设置为上一张图片下载时间

}

$endTime = time();                                          // 结束下载图片的时间
$totalTime = $endTime - $beginTime;                         // 总耗时
echo 'end download at ' . date('Y-m-d H:i:s', $endTime) . '<br/>';          // 输出下载图片完成的时间
echo 'downloaded ' . $count . ' pictures take time ' . $totalTime . ' s<br/>';         // 输出总耗时

在浏览器执行 single.php,程序运行结束后,我们看到要下载的 8 张图片已经全部下载到了本地: 00

因为在上述代码中写了一部分调试代码,所以在浏览器我们可以看到这样的输出结果: 42

multi.php:curl 多线程下载图片

<?php

/**
 * 多线程下载远程图片
 * @author 艾逗笔<765532665@qq.com>
 */

set_time_limit(300);                        // 设置程序执行超时时间为5分钟

// 要下载的图片数组
$pics = array(
    'http://media.laredoute.com/products/1200by1200/37/01/01/37010132_90_CO_1_2850804.jpg',
    'http://media.laredoute.com/products/1200by1200/37/01/01/37010132_10_CO_2_2850799.jpg',
    'http://media.laredoute.com/products/1200by1200/57/02/02/57020206_1_CO_1_057020206-9be173d2-6bc3-44ba-b9cb-111010ed0051.jpg',
    'http://media.laredoute.com/products/1200by1200/37/01/01/37010132_10_CO_1_2850800.jpg',
    'http://media.laredoute.com/products/641by641/35/00/42/350042687_1_CO_1_7639577.jpg',
    'http://media.laredoute.com/products/641by641/35/00/34/350034174_1_CO_1_7669890.jpg',
    'http://media.laredoute.com/products/641by641/35/00/42/350042718_1_CO_1_7009919.jpg',
    'http://media.laredoute.com/products/641by641/35/00/39/350039748_1_CO_3_6958274.jpg'
);

$beginTime = time();            // 开始下载图片的时间
$lastTime = $beginTime;         // 上一次下载图片的时间
$count = 0;                     // 计数器
echo 'begin download at ' . date('Y-m-d H:i:s', $beginTime) . '<br/>';      // 输出开始下载图片的时间

// 循环添加curl句柄
$mh = curl_multi_init();                                        // 开启curl多线程
foreach ($pics as $k => $v) {
    $ch[$k] = curl_init();                                      // 初始化curl句柄
    curl_setopt($ch[$k], CURLOPT_URL, $v);                      // 设置要下载的图片
    curl_setopt($ch[$k], CURLOPT_RETURNTRANSFER, 1);            // 设置获取图片内容而不直接在浏览器输出
    curl_setopt($ch[$k], CURLOPT_HEADER, 0);                    // 设置只获取图片内容,不返回header头信息
    curl_multi_add_handle($mh, $ch[$k]);                        // 添加curl多线程句柄
}

// 开启curl多线程下载图片
do {
    $status = curl_multi_exec($mh, $active);
    $result = curl_multi_info_read($mh);
    if ($result !== false) {
        $content = curl_multi_getcontent($result['handle']);                          // 获取图片内容
        $picName = substr($pics[$count], strrpos($pics[$count], '/')+1);              // 获取图片名称
        $savePath = './multi/';                                 // 设置保存图片的路径
        if (!is_dir($savePath)) {                               // 如果图片路径不存在,则创建路径
            @mkdir($savePath, 0777);
        }
        $saveName = $savePath . $picName;                       // 设置图片保存为的文件名称
        $fp = @fopen($saveName, 'w');                           // 打开文件
        fwrite($fp, $content);                                  // 把获取到的图片内容写入到新图片
        fclose($fp);                                            // 关闭文件句柄
        $nowTime = time();                                      // 当前时间
        $takeTime = $nowTime - $lastTime;                       // 下载此张图片消耗的时间
        ++$count;                                               // 计数器+1
        echo 'downloaded ' . $count . 'th picture take time ' . $takeTime . 's<br/>';               // 输出下载当前图片消耗的时间
        $lastTime = $nowTime;                                   // 把当前时间设置为上一张图片下载时间
    }
} while ($status == CURLM_CALL_MULTI_PERFORM || $active);
curl_multi_close($mh);                                      // 关闭curl多线程句柄

$endTime = time();                                          // 结束下载图片的时间
$totalTime = $endTime - $beginTime;                         // 总耗时
echo 'end download at ' . date('Y-m-d H:i:s', $endTime) . '<br/>';          // 输出下载图片完成的时间
echo 'downloaded ' . $count . ' pictures take time ' . $totalTime . ' s<br/>';         // 输出总耗时

对 single.php 进行简单的修改,我们在 multi.php 中使用 curl_multi 来初始化多个 curl 句柄,使用 curl 多线程来下载图片,在浏览器执行 multi.php,程序执行完毕后我们可以看到远程的 8 张图片也下载到了本地: 01

依然来看一下浏览器输出的调试结果: 41

通过上面的结果对比,我们可以大概的看出使用 curl 单线程与 curl 多线程下载图片的效率差别:curl 单线程下载是一个同步执行的过程,在循环下载图片的过程中,每张图片的下载耗时基本一致。而 curl 多线程下载图片是一个异步执行的过程,curl 句柄会持续的请求需要下载的图片,直到图片被成功下载为止,平均每张图片的下载耗时明显小于 curl 单线程下载的耗时。

为了验证上述结论,我们可以多执行几次,看一看二者的对比: 11

12

21

22

31

32

通过上面的几次执行结果的对比,我们可以很肯定的说,使用 curl 多线程下载图片,效率有很大的提升。

关于提升业务处理效率的更多想法

针对上面的需求,我们最终要完成的目标是在最短时间内把所需的图片全部下载下来,上面给出的 demo 都是通过在浏览器执行 php 脚本来进行图片下载的,但是如果一次性要下载的图片数量过多,网络存在延迟的情况下,很有可能会出现 php 脚本超时的问题。所以提升业务处理效率的一方面是:我们可以把 multi.php 放到 linux 服务器,通过 crontab 执行定时任务来下载图片。另一方面,为了进一步提升业务处理效率,缩短图片下载总耗时,我们可以使用 php 的 swoole 扩展。上文提到的多线程下载图片只是针对 curl 库的一种异步多线程模式,并非 php 的多线程,我们知道 php 是没有多线程的。而 swoole 扩展刚刚提供了解决了这个问题,使得 php 也能具备多线程事务处理能力。使用 swoole+crontab+curl_multi,我们就可以在短时间内批量下载所需的图片。

更多关于 swoole 与 curl_multi 的知识请自行查阅相关资料。本文所用 demo 源码下载请点此:download-picture