我如何在PHP中创建一个简单的网络爬虫?

64

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

有人用PHP完成了这个功能吗?一般的指导方针和注意事项将足以作为答案。

15个回答

89
嗯。不要用正则表达式解析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。

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

编辑:修复了George在他的答案中指出的一个错误。相对URL将不再附加到URL路径的末尾,而是覆盖它。感谢George的指正。请注意,George的答案不考虑https、用户、密码或端口。如果你加载了http PECL扩展,可以使用http_build_url轻松完成此操作。否则,我必须手动使用parse_url进行拼接。再次感谢George。


1
我可以建议使用curl来获取页面,然后使用DOM库进行操作/遍历。如果您经常这样做,我认为curl是更好的选择。 - Ben Shelock
我遇到了SSL错误:DOMDocument :: loadHTMLFile():SSL操作失败,代码为1。 DOMDocument :: loadHTMLFile():在/var/www/7Cups.com/parser.php的第10行中无法启用加密。打开流失败:操作失败。 DOMDocument :: loadHTMLFile():I / O警告:无法加载外部实体。 - Zoka

15

这是我基于上面的例子/答案实现的代码:

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

是我自己的问题还是它计算深度的方式有误? - TheCrazyProfessor

11

提供参考链接最好作为注释。 - mickmackusa
看起来这个已经不再维护了。最后更新:2013年4月15日。 - PiTheNumber

9
在最简单的形式中:
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:使用正则表达式感觉一般。不能处理相对URL。而且在file_put_contents()中使用了错误的URL。 - hobodave
这个东西应该是做什么的?我爬了一下我的网站,但给了我一大堆垃圾。它看起来像是从其他地方获取内容,而不是从我的网站上获取。 - erdomester

5

在对hobodave的代码进行了一些小修改后,这里是一个可以用来爬网页的代码片段。需要在服务器上启用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
为什么要使用PHP来完成这个任务,而不是使用wget呢?例如:wget
wget -r -l 1 http://www.example.com

关于如何解析内容,请参见最佳HTML解析方法,并使用搜索功能查找示例。如何解析HTML已经被多次回答过。


@Crimson 那是你应该在问题中注明的要求 ;) - Gordon
7
@Gordon:「我该如何用PHP制作一个简单的网络爬虫?」:-P - hobodave
@Tomalak 如果您认为我的回答不是一个答案,请标记它。如果OP要求如何解析HTML,则这是重复的,不应该回答,而应该关闭投票。我已经回答了如何爬取页面,这就是标题中所要求的。我不会再次回答如何解析。 - Gordon
@Tomalak,说真的,你先评论(引用)“这可能是实现爬行的最佳方法”,然后告诉我“你没有回答如何在php中爬行页面”。在惹恼别人之前,请先下定决心。这可行吗?好了,拜拜了您嘞。 - Gordon
1
@Tomalak 你可能确实忽略了一些显而易见的东西。是的,我没有回答如何使用PHP爬取网页。如果你看看我的回答,你会发现我把这个作为了第一件事情说了出来。我给出了一个我认为更实际的替代方案,这是我希望那些声称要在“回答实际问题”和“给予OP他实际需要的解决方案之间取得平衡”的人能够理解的东西。我还提供了两个关于如何解析HTML数据的链接。如果这对你来说不够好,请继续downvote或者flag它。我不介意。 - Gordon
显示剩余7条评论

3

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

2
谢谢你指出我的错误,George!你的解决方案忽略了https、用户、密码和端口。我已经更新了我的答案来解决你发现的错误,以及你引入的错误。再次感谢! - hobodave

2

1
你可以尝试这个,它可能对你有帮助。
$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

谢谢 @hobodave。

然而,我在你的代码中发现了两个弱点。你解析原始url以获取“host”段的方法在第一个单斜杠处停止。这假定所有相对链接都从根目录开始。但这只有时候是正确的。

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。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接