web-dev-qa-db-ja.com

PHPで簡単なクローラーを作成するにはどうすればよいですか?

リンクがたくさんあるWebページがあります。これらのリンクに含まれるすべてのデータをローカルファイルにダンプするスクリプトを作成したいと思います。

誰かがPHPでそれをやったことがありますか?答えとしては、一般的なガイドラインと落とし穴で十分です。

64

えー 正規表現でHTMLを解析 しないでください。

Tatuに触発されたDOMバージョンは次のとおりです。

<?php
function crawl_page($url, $depth = 5)
{
    static $seen = array();
    if (isset($seen[$url]) || $depth === 0) {
        return;
    }

    $seen[$url] = true;

    $dom = new DOMDocument('1.0');
    @$dom->loadHTMLFile($url);

    $anchors = $dom->getElementsByTagName('a');
    foreach ($anchors as $element) {
        $href = $element->getAttribute('href');
        if (0 !== strpos($href, 'http')) {
            $path = '/' . ltrim($href, '/');
            if (extension_loaded('http')) {
                $href = http_build_url($url, array('path' => $path));
            } else {
                $parts = parse_url($url);
                $href = $parts['scheme'] . '://';
                if (isset($parts['user']) && isset($parts['pass'])) {
                    $href .= $parts['user'] . ':' . $parts['pass'] . '@';
                }
                $href .= $parts['Host'];
                if (isset($parts['port'])) {
                    $href .= ':' . $parts['port'];
                }
                $href .= dirname($parts['path'], 1).$path;
            }
        }
        crawl_page($href, $depth - 1);
    }
    echo "URL:",$url,PHP_EOL,"CONTENT:",PHP_EOL,$dom->saveHTML(),PHP_EOL,PHP_EOL;
}
crawl_page("http://hobodave.com", 2);

編集:Tatuのバージョンのバグを修正しました(相対URLで動作するようになりました)。

編集:同じURLを2回たどらないようにする新しい機能を追加しました。

Edit:出力をSTDOUTにエコーして、必要なファイルにリダイレクトできるようにしました

編集:ジョージの答えで指摘されたバグを修正しました。相対URLはURLパスの末尾に追加されなくなりますが、上書きされます。ジョージに感謝します。 Georgeの答えは、https、user、pass、portのいずれにも対応していないことに注意してください。 http PECL拡張モジュールがロードされている場合、これは http_build_url を使用して簡単に実行できます。そうでない場合は、parse_urlを使用して手動で接着する必要があります。ジョージ、ありがとう。

89
hobodave

ここで、上記の例/回答に基づいて実装します。

  1. クラスベースです
  2. curlを使用します
  3. hTTP認証をサポート
  4. ベースドメインに属さないURLをスキップする
  5. 各ページのHttpヘッダーレスポンスコードを返す
  6. 各ページの戻り時間

クロールクラス:

class crawler
{
    protected $_url;
    protected $_depth;
    protected $_Host;
    protected $_useHttpAuth = false;
    protected $_user;
    protected $_pass;
    protected $_seen = array();
    protected $_filter = array();

    public function __construct($url, $depth = 5)
    {
        $this->_url = $url;
        $this->_depth = $depth;
        $parse = parse_url($url);
        $this->_Host = $parse['Host'];
    }

    protected function _processAnchors($content, $url, $depth)
    {
        $dom = new DOMDocument('1.0');
        @$dom->loadHTML($content);
        $anchors = $dom->getElementsByTagName('a');

        foreach ($anchors as $element) {
            $href = $element->getAttribute('href');
            if (0 !== strpos($href, 'http')) {
                $path = '/' . ltrim($href, '/');
                if (extension_loaded('http')) {
                    $href = http_build_url($url, array('path' => $path));
                } else {
                    $parts = parse_url($url);
                    $href = $parts['scheme'] . '://';
                    if (isset($parts['user']) && isset($parts['pass'])) {
                        $href .= $parts['user'] . ':' . $parts['pass'] . '@';
                    }
                    $href .= $parts['Host'];
                    if (isset($parts['port'])) {
                        $href .= ':' . $parts['port'];
                    }
                    $href .= $path;
                }
            }
            // Crawl only link that belongs to the start domain
            $this->crawl_page($href, $depth - 1);
        }
    }

    protected function _getContent($url)
    {
        $handle = curl_init($url);
        if ($this->_useHttpAuth) {
            curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
            curl_setopt($handle, CURLOPT_USERPWD, $this->_user . ":" . $this->_pass);
        }
        // follows 302 redirect, creates problem wiht authentication
//        curl_setopt($handle, CURLOPT_FOLLOWLOCATION, TRUE);
        // return the content
        curl_setopt($handle, CURLOPT_RETURNTRANSFER, TRUE);

        /* Get the HTML or whatever is linked in $url. */
        $response = curl_exec($handle);
        // response total time
        $time = curl_getinfo($handle, CURLINFO_TOTAL_TIME);
        /* Check for 404 (file not found). */
        $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);

        curl_close($handle);
        return array($response, $httpCode, $time);
    }

    protected function _printResult($url, $depth, $httpcode, $time)
    {
        ob_end_flush();
        $currentDepth = $this->_depth - $depth;
        $count = count($this->_seen);
        echo "N::$count,CODE::$httpcode,TIME::$time,DEPTH::$currentDepth URL::$url <br>";
        ob_start();
        flush();
    }

    protected function isValid($url, $depth)
    {
        if (strpos($url, $this->_Host) === false
            || $depth === 0
            || isset($this->_seen[$url])
        ) {
            return false;
        }
        foreach ($this->_filter as $excludePath) {
            if (strpos($url, $excludePath) !== false) {
                return false;
            }
        }
        return true;
    }

    public function crawl_page($url, $depth)
    {
        if (!$this->isValid($url, $depth)) {
            return;
        }
        // add to the seen URL
        $this->_seen[$url] = true;
        // get Content and Return Code
        list($content, $httpcode, $time) = $this->_getContent($url);
        // print Result for current Page
        $this->_printResult($url, $depth, $httpcode, $time);
        // process subPages
        $this->_processAnchors($content, $url, $depth);
    }

    public function setHttpAuth($user, $pass)
    {
        $this->_useHttpAuth = true;
        $this->_user = $user;
        $this->_pass = $pass;
    }

    public function addFilterPath($path)
    {
        $this->_filter[] = $path;
    }

    public function run()
    {
        $this->crawl_page($this->_url, $this->_depth);
    }
}

使用法:

// USAGE
$startURL = 'http://YOUR_URL/';
$depth = 6;
$username = 'YOURUSER';
$password = 'YOURPASS';
$crawler = new crawler($startURL, $depth);
$crawler->setHttpAuth($username, $password);
// Exclude path with the following structure to be processed 
$crawler->addFilterPath('customer/account/login/referer');
$crawler->run();
16
WonderLand

PHPクローラーをご覧ください

http://sourceforge.net/projects/php-crawler/

それが役立つかどうかを確認してください。

11
GeekTantra

最も簡単な形式では:

function crawl_page($url, $depth = 5) {
    if($depth > 0) {
        $html = file_get_contents($url);

        preg_match_all('~<a.*?href="(.*?)".*?>~', $html, $matches);

        foreach($matches[1] as $newurl) {
            crawl_page($newurl, $depth - 1);
        }

        file_put_contents('results.txt', $newurl."\n\n".$html."\n\n", FILE_APPEND);
    }
}

crawl_page('http://www.domain.com/index.php', 5);

この関数はページからコンテンツを取得し、見つかったすべてのリンクをクロールし、そのコンテンツを「results.txt」に保存します。関数は、リンクが続く時間を定義する2番目のパラメーターdepthを受け入れます。特定のページからのリンクのみを解析する場合は、1を渡します。

9
Tatu Ulmanen

hobodave's コードを少し変更すると、ページのクロールに使用できるコードスニペットがあります。これには、サーバーでcurl拡張機能を有効にする必要があります。

<?php
//set_time_limit (0);
function crawl_page($url, $depth = 5){
$seen = array();
if(($depth == 0) or (in_array($url, $seen))){
    return;
}   
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
$result = curl_exec ($ch);
curl_close ($ch);
if( $result ){
    $stripped_file = strip_tags($result, "<a>");
    preg_match_all("/<a[\s]+[^>]*?href[\s]?=[\s\"\']+"."(.*?)[\"\']+.*?>"."([^<]+|.*?)?<\/a>/", $stripped_file, $matches, PREG_SET_ORDER ); 
    foreach($matches as $match){
        $href = $match[1];
            if (0 !== strpos($href, 'http')) {
                $path = '/' . ltrim($href, '/');
                if (extension_loaded('http')) {
                    $href = http_build_url($href , array('path' => $path));
                } else {
                    $parts = parse_url($href);
                    $href = $parts['scheme'] . '://';
                    if (isset($parts['user']) && isset($parts['pass'])) {
                        $href .= $parts['user'] . ':' . $parts['pass'] . '@';
                    }
                    $href .= $parts['Host'];
                    if (isset($parts['port'])) {
                        $href .= ':' . $parts['port'];
                    }
                    $href .= $path;
                }
            }
            crawl_page($href, $depth - 1);
        }
}   
echo "Crawled {$href}";
}   
crawl_page("http://www.sitename.com/",3);
?>

これでこのチュートリアルを説明しました クローラースクリプトチュートリアル

5
Team Webgalli

wget を使用できる場合、これにPHPを使用する理由.

wget -r -l 1 http://www.example.com

コンテンツを解析する方法については、 HTMLを解析するための最良の方法 を参照し、 examples の検索機能を使用してください。 HTMLを解析する方法は、以前に複数回回答されています。

5
Gordon

Hobodaveあなたは非常に近かった。変更したのは、見つかったアンカータグのhref属性が「http」で始まるかどうかを確認するifステートメント内だけです。渡されたページを含む$ url変数を単に追加する代わりに、最初にparse_url php関数を使用して実行できるホストにそれを削除する必要があります。

<?php
function crawl_page($url, $depth = 5)
{
  static $seen = array();
  if (isset($seen[$url]) || $depth === 0) {
    return;
  }

  $seen[$url] = true;

  $dom = new DOMDocument('1.0');
  @$dom->loadHTMLFile($url);

  $anchors = $dom->getElementsByTagName('a');
  foreach ($anchors as $element) {
    $href = $element->getAttribute('href');
    if (0 !== strpos($href, 'http')) {
       /* this is where I changed hobodave's code */
        $Host = "http://".parse_url($url,PHP_URL_Host);
        $href = $Host. '/' . ltrim($href, '/');
    }
    crawl_page($href, $depth - 1);
  }

  echo "New Page:<br /> ";
  echo "URL:",$url,PHP_EOL,"<br />","CONTENT:",PHP_EOL,$dom->saveHTML(),PHP_EOL,PHP_EOL,"  <br /><br />";
}

crawl_page("http://hobodave.com/", 5);
?>
3
George

前述のように、クローラーフレームワークはすべてカスタマイズできますが、実行していることが簡単である場合は、最初から簡単に作成できます。

リンクのスクレイピング: http://www.phpro.org/examples/Get-Links-With-DOM.html

結果をファイルにダンプする: http://www.tizag.com/phpT/filewrite.php

2
Jens Roland

@hobodaveありがとう。

ただし、コードに2つの弱点が見つかりました。 「ホスト」セグメントを取得するための元のURLの解析は、最初の単一のスラッシュで停止します。これは、すべての相対リンクがルートディレクトリで始まることを前提としています。これは時々真実です。

original url   :  http://example.com/game/index.html
href in <a> tag:  highscore.html
author's intent:  http://example.com/game/highscore.html  <-200->
crawler result :  http://example.com/highscore.html       <-404->

これを修正するには、最初ではなく最後の単一のスラッシュで区切ります

2番目の無関係なバグは、$depthは実際に再帰の深さを追跡せず、再帰の最初のレベルのを追跡することです。

このページがアクティブに使用されていると信じた場合、この2番目の問題をデバッグするかもしれませんが、この問題は6年前であり、十分ではないので、今書いているテキストはだれでも、人間でもロボットでも決して読まれないと思います+ hobodaveにコードについてコメントすることでこれらの欠陥について直接通知する評判。とにかくhobodaveに感謝します。

1
Dov Jacobson

@hobodaveのコードを使用し、この小さな調整を使用して、同じURLのすべてのフラグメントバリアントを再クロールしないようにしました。

<?php
function crawl_page($url, $depth = 5)
{
  $parts = parse_url($url);
  if(array_key_exists('fragment', $parts)){
    unset($parts['fragment']);
    $url = http_build_url($parts);
  }

  static $seen = array();
  ...

その後、forループ内で$parts = parse_url($url);行を省略することもできます。

1
pasqal

あなたはこれを試すことができます、それはあなたに役立つかもしれません

$search_string = 'american golf News: Fowler beats stellar field in Abu Dhabi';
$html = file_get_contents(url of the site);
$dom = new DOMDocument;
$titalDom = new DOMDocument;
$tmpTitalDom = new DOMDocument;
libxml_use_internal_errors(true);
@$dom->loadHTML($html);
libxml_use_internal_errors(false);
$xpath = new DOMXPath($dom);
$videos = $xpath->query('//div[@class="primary-content"]');
foreach ($videos as $key => $video) {
$newdomaindom = new DOMDocument;    
$newnode = $newdomaindom->importNode($video, true);
$newdomaindom->appendChild($newnode);
@$titalDom->loadHTML($newdomaindom->saveHTML());
$xpath1 = new DOMXPath($titalDom);
$titles = $xpath1->query('//div[@class="listingcontainer"]/div[@class="list"]');
if(strcmp(preg_replace('!\s+!',' ',  $titles->item(0)->nodeValue),$search_string)){     
    $tmpNode = $tmpTitalDom->importNode($video, true);
    $tmpTitalDom->appendChild($tmpNode);
    break;
}
}
echo $tmpTitalDom->saveHTML();
1
Niraj patel

次のスパイダーコードを思い付きました。私はそれを以下から少し適応させました: PHP-深い再帰を実行する安全な方法はありますか? それはかなり速いようです...

    <?php
function  spider( $base_url , $search_urls=array() ) {
    $queue[] = $base_url;
    $done           =   array();
    $found_urls     =   array();
    while($queue) {
            $link = array_shift($queue);
            if(!is_array($link)) {
                $done[] = $link;
                foreach( $search_urls as $s) { if (strstr( $link , $s )) { $found_urls[] = $link; } }
                if( empty($search_urls)) { $found_urls[] = $link; }
                if(!empty($link )) {
echo 'LINK:::'.$link;
                      $content =    file_get_contents( $link );
//echo 'P:::'.$content;
                    preg_match_all('~<a.*?href="(.*?)".*?>~', $content, $sublink);
                    if (!in_array($sublink , $done) && !in_array($sublink , $queue)  ) {
                           $queue[] = $sublink;
                    }
                }
            } else {
                    $result=array();
                    $return = array();
                    // flatten multi dimensional array of URLs to one dimensional.
                    while(count($link)) {
                         $value = array_shift($link);
                         if(is_array($value))
                             foreach($value as $sub)
                                $link[] = $sub;
                         else
                               $return[] = $value;
                     }
                     // now loop over one dimensional array.
                     foreach($return as $link) {
                                // echo 'L::'.$link;
                                // url may be in form <a href.. so extract what's in the href bit.
                                preg_match_all('/<a[^>]+href=([\'"])(?<href>.+?)\1[^>]*>/i', $link, $result);
                                if ( isset( $result['href'][0] )) { $link = $result['href'][0]; }
                                // add the new URL to the queue.
                                if( (!strstr( $link , "http")) && (!in_array($base_url.$link , $done)) && (!in_array($base_url.$link , $queue)) ) {
                                     $queue[]=$base_url.$link;
                                } else {
                                    if ( (strstr( $link , $base_url  ))  && (!in_array($base_url.$link , $done)) && (!in_array($base_url.$link , $queue)) ) {
                                         $queue[] = $link;
                                    }
                                }
                      }
            }
    }


    return $found_urls;
}    


    $base_url       =   'https://www.houseofcheese.co.uk/';
    $search_urls    =   array(  $base_url.'acatalog/' );
    $done = spider( $base_url  , $search_urls  );

    //
    // RESULT
    //
    //
    echo '<br /><br />';
    echo 'RESULT:::';
    foreach(  $done as $r )  {
        echo 'URL:::'.$r.'<br />';
    }
0
Ian

古い質問です。それ以来、多くの良いことが起こりました。このトピックに関する私の2セントは次のとおりです。

  1. 訪問したページを正確に追跡するには、最初にURIを正規化する必要があります。正規化アルゴリズムには複数のステップが含まれます。

    • クエリパラメータを並べ替えます。たとえば、次のURIは正規化後は同等です:GET http://www.example.com/query?id=111&cat=222 GET http://www.example.com/query?cat=222&id=111
    • 空のパスを変換します。例:http://example.org → http://example.org/

    • パーセントエンコーディングを大文字にします。パーセントエンコードトリプレット内のすべての文字(「%3A」など)は大文字と小文字を区別しません。例:http://example.org/a%c2%B1b → http://example.org/a%C2%B1b

    • 不要なドットセグメントを削除します。例:http://example.org/../a/b/../c/./d.html → http://example.org/a/c/d.html

    • おそらく他のいくつかの正規化ルール

  2. <a>タグにhref属性があるだけでなく、<area>タグにもあります https://html.com/tags/area/ 。何も見逃したくない場合は、<area>タグもスクレイピングする必要があります。

  3. クロールの進行状況を追跡します。 Webサイトが小さい場合、問題はありません。逆に、サイトの半分をクロールして失敗した場合、非常にイライラするかもしれません。データベースまたはファイルシステムを使用して進行状況を保存することを検討してください。

  4. サイトの所有者に親切にしてください。ウェブサイト以外でクローラーを使用する場合は、遅延を使用する必要があります。遅延がなければ、スクリプトは速すぎて、いくつかの小さなサイトを著しく遅くする可能性があります。システム管理者の観点からは、DoS攻撃のように見えます。リクエスト間の静的遅延がトリックを行います。

それに対処したくない場合は、 Crawlzone を試して、フィードバックをお知らせください。また、私がしばらく前に書いた記事をチェックしてください https://www.codementor.io/zstate/this-is-how-i-crawl-n98s6myxm

0
zstate

提供されたURLからデータを取得する小さなクラスを作成し、選択したHTML要素を抽出します。このクラスは、CURLとDOMDocumentを使用します。

phpクラス:

class crawler {


   public static $timeout = 2;
   public static $agent   = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';


   public static function http_request($url) {
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL,            $url);
      curl_setopt($ch, CURLOPT_USERAGENT,      self::$agent);
      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::$timeout);
      curl_setopt($ch, CURLOPT_TIMEOUT,        self::$timeout);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      $response = curl_exec($ch);
      curl_close($ch);
      return $response;
   }


   public static function strip_whitespace($data) {
      $data = preg_replace('/\s+/', ' ', $data);
      return trim($data);
   }


   public static function extract_elements($tag, $data) {
      $response = array();
      $dom      = new DOMDocument;
      @$dom->loadHTML($data);
      foreach ( $dom->getElementsByTagName($tag) as $index => $element ) {
         $response[$index]['text'] = self::strip_whitespace($element->nodeValue);
         foreach ( $element->attributes as $attribute ) {
            $response[$index]['attributes'][strtolower($attribute->nodeName)] = self::strip_whitespace($attribute->nodeValue);
         }
      }
      return $response;
   }


}

使用例:

$data  = crawler::http_request('https://stackoverflow.com/questions/2313107/how-do-i-make-a-simple-crawler-in-php');
$links = crawler::extract_elements('a', $data);
if ( count($links) > 0 ) {
   file_put_contents('links.json', json_encode($links, JSON_PRETTY_PRINT));
}

応答例:

[
    {
        "text": "Stack Overflow",
        "attributes": {
            "href": "https:\/\/stackoverflow.com",
            "class": "-logo js-gps-track",
            "data-gps-track": "top_nav.click({is_current:false, location:2, destination:8})"
        }
    },
    {
        "text": "Questions",
        "attributes": {
            "id": "nav-questions",
            "href": "\/questions",
            "class": "-link js-gps-track",
            "data-gps-track": "top_nav.click({is_current:true, location:2, destination:1})"
        }
    },
    {
        "text": "Developer Jobs",
        "attributes": {
            "id": "nav-jobs",
            "href": "\/jobs?med=site-ui&ref=jobs-tab",
            "class": "-link js-gps-track",
            "data-gps-track": "top_nav.click({is_current:false, location:2, destination:6})"
        }
    }
]
0
Jake

外部リンクをクロールするとき(OPがユーザー自身のページに関連していることを感謝します)、robots.txtに注意する必要があることを思い出してください。うまくいけば http://www.the-art-of-web.com/php/parse-robots/ に役立つ次のものを見つけました。

0
Antony