
リンク切れチェックスクリプトを curl_multi で作りなおしているのですが、うまく使えておらず質問します。
現在使用しているスクリプトは、curlで作っており、http code を拾い、それを評価し、必要であれば中身も入手し、リンク切れを確認しています。
このスクリプトを、大量URL対応版にしたいと思い改変しているのですが、curl_multi の使い方がよく理解できず、アドバイスいただければと思います。
挙動を全部追えていないのですが、curl_multi の CURLOPT_URL に相当数の URL を突っ込むと、動きがおかしくなります。
手持ちのサーバでやると2件目以降ステータス499を返してしまって、よくわからない状況ですが、100件程度であればリクエストを同時に投げているようですが、10000件になるとリクエストを投げていないようです。
実はリクエストは全部同時ではなくて、ある程度数量で上限が決まっていて、勝手に順次アクセスとなるのではないかと思ってましたが、そうではなかったようです。
実現したいのは、同時接続数を制限して、順次アクセスさせたいのですが、CURLOPT にそれらしき項目が見つかりませんでした。
スクリプト側で制限する方法として、一旦10程度のURLを切り出し、それらが全部終了すると次の10件に取り掛かるといった対応を考えていますが、できれば、切れ目なくアクセスさせたいです。
実装方法に関して、ヒントがあれば教えていただけないでしょうか?
よろしくお願いします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答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総合スコア5223
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&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
総合スコア5223
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。


あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2016/06/19 12:06
退会済みユーザー
2016/06/19 14:52
2016/06/19 15:12 編集
退会済みユーザー
2016/06/19 15:35
2016/06/19 15:37
退会済みユーザー
2016/06/19 15:43