如何在 PHP 中制作一个简单的爬虫? [关闭]

Posted

技术标签:

【中文标题】如何在 PHP 中制作一个简单的爬虫? [关闭]【英文标题】:How do I make a simple crawler in PHP? [closed] 【发布时间】:2011-01-19 18:20:54 【问题描述】:

我有一个包含大量链接的网页。我想编写一个脚本,将这些链接中包含的所有数据转储到本地文件中。

有人用 php 做过吗?一般准则和陷阱足以作为答案。

【问题讨论】:

【参考方案1】:

嗯。不要parse html with regexes。

这是一个受 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。

编辑:现在将输出回显到 STDOUT,以便您可以将其重定向到您想要的任何文件

编辑:修复了 George 在回答中指出的错误。相对 url 将不再附加到 url 路径的末尾,而是覆盖它。感谢乔治。请注意,乔治的回答不考虑以下任何一项:https、用户、通行证或端口。如果您加载了http PECL 扩展,则使用http_build_url 非常简单。否则,我必须使用 parse_url 手动粘合在一起。再次感谢乔治。

【讨论】:

我可以推荐使用 curl 来获取页面,然后使用 DOM 库进行操作/遍历。如果你经常这样做,那么 curl 是更好的选择。 我收到 SSL 错误:DOMDocument::loadHTMLFile(): SSL operation failed with code 1. DOMDocument::loadHTMLFile(): Failed to enable crypto in /var/www/7Cups.com/parser .php 在第 10 行。未能打开流:操作失败。 DOMDocument::loadHTMLFile(): I/O 警告:加载外部实体失败【参考方案2】:

这是我基于上述示例/答案的实现。

    它是基于类的 使用卷曲 支持 HTTP 认证 跳过不属于基域的 URL 返回每个页面的Http header Response Code 每页返回时间

抓取类:

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();

【讨论】:

只是我还是它算错了深度?【参考方案3】:

查看 PHP 爬虫

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

看看有没有帮助。

【讨论】:

提供参考链接最好作为 cmets。 看起来这不再维护了。最后更新:2013-04-15【参考方案4】:

最简单的形式:

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”。这些函数接受第二个参数depth,它定义了链接应该遵循多长时间。如果您只想解析来自给定页面的链接,请在此处传递 1。

【讨论】:

-1:不喜欢使用正则表达式。不适用于相对网址。在 file_put_contents() 中也使用了错误的 URL。 这是做什么的?我通过网站爬网,它给了我一堆废话。看起来它从其他地方获取内容,但现在来自我的网站。【参考方案5】:

为什么要为此使用 PHP,例如,当您可以使用 wget 时,

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

有关如何解析内容,请参阅Best Methods to parse HTML 并使用examples 的搜索功能。如何解析HTML之前已经回答过多次了。

【讨论】:

某些特定的字段必须被解析并取出。我需要写代码。 @Crimson 这是您应该在问题中注明的要求;) @Gordon:“我如何用 PHP 制作一个简单的爬虫?” :-P @hobodave 我的意思是关于必须解析和取出特定字段的部分:P 如果不是为了这个,使用 wget 是我能想象的最简单的事情这个目的。 @Tomalak 您可能确实在这里遗漏了一些明显的东西。是的,我没有回答如何使用 PHP 抓取页面。如果您查看我的答案,您会发现我实际上将其作为第一件事。我提供了一个我认为更实用的替代方案,我希望有人声称在“回答实际问题”和“为 OP 提供他实际需要的解决方案”之间取得平衡去理解。我还提供了两个关于如何解析 HTML 以获取数据的信息的链接。如果这对您来说还不够好,请保留您的 dv 和/或标记它。我不在乎。【参考方案6】:

对hobodave's 代码进行了一些小改动,这是一个可用于抓取页面的代码n-p。这需要在您的服务器中启用 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);
?>

我已经在crawler script tutorial中解释了这个教程

【讨论】:

【参考方案7】:

Hobodave,你非常接近。我唯一更改的是在 if 语句中检查找到的锚标记的 href 属性是否以“http”开头。不要简单地添加包含传入页面的 $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);
?>

【讨论】:

感谢乔治指出我的错误!您的解决方案忽略了处理 https、user、pass 和 port。我已经更新了我的答案以解决您发现的错误以及您引入的错误。再次感谢!【参考方案8】:

如前所述,那里有所有准备好定制的爬虫框架,但如果您所做的事情像您提到的那样简单,那么您可以很容易地从头开始。

抓取链接:http://www.phpro.org/examples/Get-Links-With-DOM.html

将结果转储到文件:http://www.tizag.com/phpT/filewrite.php

【讨论】:

【参考方案9】:

我使用了@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);这一行。

【讨论】:

【参考方案10】:

你可以试试这个可能对你有帮助

$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();

【讨论】:

【参考方案11】:

谢谢@hobodave。

但是,我在您的代码中发现了两个弱点。 您对原始网址的解析以获取“主机”段在第一个单斜杠处停止。这假定所有相对链接都从根目录开始。这只在某些时候是正确的。

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->

通过在最后一个单斜杠而不是第一个斜杠处中断来解决此问题

第二个不相关的错误是$depth 并没有真正跟踪递归深度,它跟踪第一级递归的广度

如果我相信这个页面正在被积极使用,我可能会调试第二个问题,但我怀疑我现在写的文本将永远不会被任何人阅读,无论是人类还是机器人,因为这个问题已经存在六年了,而我没有甚至有足够的声誉通过评论他的代码来直接通知 +hobodave 这些缺陷。无论如何谢谢hobodave。

【讨论】:

【参考方案12】:

我想出了以下蜘蛛代码。 我从以下内容对其进行了一些调整: PHP - Is the there a safe way to perform deep recursion? 它似乎相当迅速......

    <?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 />';
    

【讨论】:

【参考方案13】:

值得记住的是,在抓取外部链接时(我很欣赏 OP 与用户自己的页面相关),您应该注意 robots.txt。我发现了以下内容,希望对 http://www.the-art-of-web.com/php/parse-robots/ 有所帮助。

【讨论】:

【参考方案14】:

我创建了一个小类来从提供的 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://***.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:\/\/***.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)"
        
    
]

【讨论】:

【参考方案15】:

这是一个老问题。从那以后发生了很多好事。这是我在这个主题上的两分钱:

    要准确跟踪访问过的页面,您必须首先规范化 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

    可能还有其他一些规范化规则

    不仅&lt;a&gt;标签有href属性,&lt;area&gt;标签也有https://html.com/tags/area/。如果你不想错过任何东西,你也必须刮&lt;area&gt;标签。

    跟踪抓取进度。如果网站很小,这不是问题。相反,如果您爬取了一半的站点并且它失败了,这可能会非常令人沮丧。考虑使用数据库或文件系统来存储进度。

    善待网站所有者。 如果您打算在网站之外使用爬虫,则必须使用延迟。没有延迟,脚本太快了,可能会显着减慢一些小型站点。从系统管理员的角度来看,它看起来像是 DoS 攻击。请求之间的静态延迟可以解决问题。

如果您不想处理这个问题,请尝试 Crawlzone 并让我知道您的反馈。另外,请查看我不久前写的文章https://www.codementor.io/zstate/this-is-how-i-crawl-n98s6myxm

【讨论】:

以上是关于如何在 PHP 中制作一个简单的爬虫? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

如何在 PHP 中实现网络爬虫? [关闭]

如何用 PHP 制作一个简单的登录系统? [关闭]

如何使用 PHP 制作机器人以登录并在网站上执行操作 [关闭]

求一个简易的php爬虫提取网页的title

如何在本地主机上使用 htaccess 制作 seo url? [关闭]

如何在 PHP 中实现分页? [关闭]