質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.47%
cURL

cURLはHTTP, FTPやTelnetなど複数のプロトコルを用いてデータを転送するライブラリとコマンドラインツールを提供します。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

Q&A

解決済

2回答

9731閲覧

curl_multi の接続上限設定

退会済みユーザー

退会済みユーザー

総合スコア0

cURL

cURLはHTTP, FTPやTelnetなど複数のプロトコルを用いてデータを転送するライブラリとコマンドラインツールを提供します。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

2グッド

2クリップ

投稿2016/06/19 09:09

リンク切れチェックスクリプトを curl_multi で作りなおしているのですが、うまく使えておらず質問します。

現在使用しているスクリプトは、curlで作っており、http code を拾い、それを評価し、必要であれば中身も入手し、リンク切れを確認しています。

このスクリプトを、大量URL対応版にしたいと思い改変しているのですが、curl_multi の使い方がよく理解できず、アドバイスいただければと思います。

挙動を全部追えていないのですが、curl_multi の CURLOPT_URL に相当数の URL を突っ込むと、動きがおかしくなります。
手持ちのサーバでやると2件目以降ステータス499を返してしまって、よくわからない状況ですが、100件程度であればリクエストを同時に投げているようですが、10000件になるとリクエストを投げていないようです。
実はリクエストは全部同時ではなくて、ある程度数量で上限が決まっていて、勝手に順次アクセスとなるのではないかと思ってましたが、そうではなかったようです。

実現したいのは、同時接続数を制限して、順次アクセスさせたいのですが、CURLOPT にそれらしき項目が見つかりませんでした。

スクリプト側で制限する方法として、一旦10程度のURLを切り出し、それらが全部終了すると次の10件に取り掛かるといった対応を考えていますが、できれば、切れ目なくアクセスさせたいです。

実装方法に関して、ヒントがあれば教えていただけないでしょうか?

よろしくお願いします。

ikuwow👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

curl_multiの扱い面倒くさいですよね.接続数上限を設けようとすると,以下のように書くしかないです.

php

1<?php 2 3/** 4 * @param array $curls cURLリソースの配列 5 * @param int $limit 接続上限 6 * @return array $curlsの要素値をエラーコードに入れ替えたもの 7 * コンテンツはcurl_multi_getcontentで取得可能 8 * エラー内容はcurl_errorで取得可能 9 * 但しcurl_errnoは使えないため,この返り値は重要となる 10 */ 11function parallel_curl_exec(array $curls, $limit = 10) 12{ 13 $mh = curl_multi_init(); 14 $queue = []; // キュー 15 $errors = array_fill_keys(array_keys($curls), null); // 順番が揃うように最初にキーを埋めておく 16 $count = 0; // 現在稼働中のリクエスト数 17 18 // 出来る限りリクエストをプールに追加し,オーバーしたぶんはキューに入れる 19 foreach ($curls as $i => $ch) { 20 if ($count < $limit) { 21 if (CURLM_OK === curl_multi_add_handle($mh, $ch)) { 22 ++$count; 23 } 24 } else { 25 $queue[] = $ch; 26 } 27 } 28 29 // リクエスト実行開始 30 curl_multi_exec($mh, $active); 31 32 do { 33 34 // 最大0.5秒間の間監視し,結果をプールに反映する 35 curl_multi_select($mh, 0.5); 36 curl_multi_exec($mh, $active); 37 38 // 一度すべての結果を取り出す 39 // このときにエラーコードの配列を埋めておく 40 // (ここでプールから除去もしてしまうとエラーになるので注意!一旦すべて取り出す必要がある) 41 $entries = []; 42 do if ($entry = curl_multi_info_read($mh, $remains)) { 43 $errors[array_search($entry['handle'], $curls)] = $entry['result']; 44 $entries[] = $entry; 45 } while ($remains); 46 47 // 取り出された数だけリクエストをプールから除去し,キューからまだのぶんを追加する 48 foreach ($entries as $entry) { 49 curl_multi_remove_handle($mh, $entry['handle']); 50 --$count; 51 if ($ch = array_shift($queue) and CURLM_OK === curl_multi_add_handle($mh, $ch)) { 52 ++$count; 53 } 54 } 55 56 } while ($count > 0 || $queue); // 稼働中のリクエストとキューが無くなるまでループする 57 58 assert($active === 0); // これは必ず成立する 59 60 return $errors; 61}

ホントいうと,1つリクエストが終わるごとに直ちに確認処理をすべきところなのですが,あまりにもコードが複雑になるので設計面は妥協しています.

一応,mpyw/Coというものを作ったことがあるのですが,ソースが汚すぎるのとバグだらけなのがあって,まともにまだ使えないですね…

投稿2016/06/19 09:57

編集2016/06/19 10:08
mpyw

総合スコア5223

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2016/06/19 12:06

いつも色々参考にしてます!回答ありがとうございます。 multi_remove_handle、curl_multi_add_handle して、再度、curl_multi_exec すればよかったのですね。 まだちゃんと動かせていないので、取り急ぎお礼まで。
退会済みユーザー

退会済みユーザー

2016/06/19 14:52

試してみているのですが、ちょっとうまくいっていません。 CURLM_OK === curl_multi_add_handle($mh, $ch) の判定がうまく動作せず、 var_dump(curl_multi_add_handle($mh, $ch)); を確認したところ、NULLが返ってました。 ドキュメントを見る限り、curl_multi_add_handle の返り値は、0かCURLM_XXXを取るようなのですが、NULLが返ってる理由が分かりませんでした。 なにか切り分けのポイント、あったりしますか? $curls に10個ほど同じURLを入れて実験しています。 よろしくお願いします。
mpyw

2016/06/19 15:12 編集

汎用性を高くするため,$curls に渡るのはURLの文字列ではなくcURLリソースを想定しています…(ちょっと微妙な設計だったかな…) $curls = []; foreach ($urls as $i => $url) {     $ch = curl_init();     curl_setopt_array($ch, [         CURLOPT_URL => $url,         CURLOPT_RETURNTRANSFER => true,         CURLOPT_ENCODING => 'gzip',     ]);     $curls[$i] = $ch; } $errors = parallel_curl_exec($curls, 10); foreach ($errors as $i => $error) { // ここで $error が CURLE_OK ならTCPレベルの通信は成功している // HTTPステータスは curl_getinfo($curls[$i], CURLINFO_EFFECTIVE_CODE)  // コンテンツは curl_multi_getcontent($curls[$i]) } 使いづらいようでしたら,使いやすいように組み替えてみてください.ただ,curl_errno関数で後からエラーコードを取得することはできない,ということだけご注意ください.(curl_getinfo関数は使えます)
退会済みユーザー

退会済みユーザー

2016/06/19 15:35

無事動作しました!ありがとうございます。 非常に快適です。 話が横にそれてしまうのですが、cURLリソースを投入した結果 var_dump(curl_multi_add_handle($mh, $ch)); は、0となっており、マニュアル通りなのですが CURLM_OK === curl_multi_add_handle($mh, $ch) が TURE になるのは、なぜですか? もし時間があれば、追加で教えていただけると嬉しいです。
mpyw

2016/06/19 15:37

CURLM_OKはゼロを表す定数ですね… var_dump(CURLM_OK); // int(0)
退会済みユーザー

退会済みユーザー

2016/06/19 15:43

そうなんですね^^; 勉強になりました。 中身確認してみれば良かった。 お手数をお掛けしました。 色々ありがとうございます!
guest

0

mpyw/coがひとまず完成したので宣伝も兼ねて再回答させていただきます.

php

1function curl_init_with($url, array $options = [CURLOPT_RETURNTRANSFER => true]) 2{ 3 $ch = curl_init($url); 4 curl_setopt_array($ch, $options); 5 return $ch; 6} 7function get_xpath_async($url) 8{ 9 $dom = new \DOMDocument; 10 @$dom->loadHTML(yield curl_init_with($url)); 11 return new \DOMXPath($dom); 12} 13 14var_dump(Co::wait([ 15 16 'Delay 5 secs' => function () { 17 echo "[Delay] I start to have a pseudo-sleep in this coroutine for about 5 secs\n"; 18 for ($i = 0; $i < 5; ++$i) { 19 yield Co::DELAY => 1; 20 if ($i < 4) { 21 printf("[Delay] %s\n", str_repeat('.', $i + 1)); 22 } 23 } 24 echo "[Delay] Done!\n"; 25 }, 26 27 "google.com HTML" => curl_init_with("https://google.com"), 28 29 "Content-Length of github.com" => function () { 30 echo "[GitHub] I start to request for github.com to calculate Content-Length\n"; 31 $content = yield curl_init_with("https://github.com"); 32 echo "[GitHub] Done! Now I calculate length of contents\n"; 33 return strlen($content); 34 }, 35 36 "Save mpyw's Gravatar Image URL to local" => function () { 37 echo "[Gravatar] I start to request for github.com to get Gravatar URL\n"; 38 $src = (yield get_xpath_async('https://github.com/mpyw')) 39 ->evaluate('string(//img[contains(@class,"avatar")]/@src)'); 40 echo "[Gravatar] Done! Now I download its data\n"; 41 yield curl_init_with($src, [CURLOPT_FILE => fopen('/tmp/mpyw.png', 'w')]); 42 echo "[Gravatar] Done! Saved as /tmp/mpyw.png\n"; 43 } 44 45]));

こんなに複雑なフローでも,ジェネレータのyield句を活用して効率よく処理できます.

[Delay] I start to have a pseudo-sleep in this coroutine for about 5 secs [GitHub] I start to request for github.com to calculate Content-Length [Gravatar] I start to request for github.com to get Gravatar URL [Delay] . [Delay] .. [GitHub] Done! Now I calculate length of contents [Gravatar] Done! Now I download its data [Delay] ... [Gravatar] Done! Saved as /tmp/mpyw.png [Delay] .... [Delay] Done! array(4) { ["Delay 5 secs"]=> NULL ["google.com HTML"]=> string(262) "<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8"> <TITLE>302 Moved</TITLE></HEAD><BODY> <H1>302 Moved</H1> The document has moved <A HREF="https://www.google.co.jp/?gfe_rd=cr&amp;ei=XXXXXX">here</A>. </BODY></HTML> " ["Content-Length of github.com"]=> int(25534) ["Save mpyw's Gravatar Image URL to local"]=> NULL }

ある処理の待ち時間の間に別の処理が積極的に割り込んでいるのに注目です.しかも1プロセス・1スレッドしか使わないので無駄がありません.スクレイピングでは負荷を考慮し,相手サーバごとにスリープをかけたい場合もあると思うので,コルーチンごとに擬似的なスリープを行えるようにもしています.

投稿2016/07/20 18:26

mpyw

総合スコア5223

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2016/07/20 19:31

> テスト100%通りました! Qiitaの記事で読みましたw 今まで、curl_multi で実装しようとも思わなかった処理が、割り当てられそうです。 この質問の処理は、単純なモノだったので、次に構想しているアプリで試してみます。 また詰まったら、相談させてもらいます。 ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問