如何在PHP中制作蜘蛛机器人?
在本文中,我们将看到如何在PHP中制作一个简单且相对高级的网络爬虫(或蜘蛛机器人)。更简单的将简单地输出它在网页中找到的所有链接,而高级的会将标题、关键字和描述添加到概念数据库(概念意味着本文中没有使用 SQL 数据库)!与谷歌相比,即使我们的高级网络爬虫实际上只是一个简单的网络爬虫,因为我们的爬虫不使用任何人工智能代理!在结束本文之前,我们将经历总共3 次迭代——每一次迭代都有一个解释。
注意:在整篇文章中,我将交替使用蜘蛛机器人和网络爬虫这两个词。有些人可能会在不同的上下文中使用它们,但在本文中,这两个词的含义基本相同。
您可以做很多事情来改进这个蜘蛛机器人并使其更先进——添加诸如维护流行指数之类的功能,并实施一些反垃圾邮件功能,例如惩罚没有内容的网站或使用“点击诱饵”的网站添加与页面内容无关的关键字等策略!此外,您可以尝试从页面生成关键字和描述,这是 GoogleBot 一直在做的事情。如果您想改进此蜘蛛机器人,可以查看以下相关文章列表。
一个简单的 SpiderBot:简单版本将是非递归的,并且将简单地打印它在网页中找到的所有链接。请注意,我们所有的主要逻辑都将发生在followLinks
函数中!
- 程序:
array( 'method' => "GET", 'user-agent' => "gfgBot/0.1\n" ) ); // Create context for communication $context=stream_context_create( $options ); // Create a new HTML DomDocument for web-scraping $doc = new DomDocument(); @$doc -> loadHTML( file_get_contents($url, false, $context) ); // Get all the anchor nodes in the DOM $links = $doc -> getElementsByTagName('a'); // Iterate through all the anchor nodes // found in the document foreach ($links as $i) echo $i->getAttribute('href') . '
'; } followLink("http://example.com"); ?>- 输出:现在,这不好——我们只得到一个链接——那是因为我们在站点
example.com
中只有一个链接,而且由于我们不是递归的,所以我们不遵循我们得到的链接。如果您想查看完整的操作,可以运行followLink("http://apple.com")
。但是,如果您使用 geeksforgeeks.com,那么您可能会遇到一些错误,因为 GeeksforGeeks 会阻止我们的请求(当然是出于安全原因)。https://www.iana.org/domains/example
解释:
- 第 3 行:我们正在创建一个
$options
数组。除了在上下文创建中需要它之外,您不必了解太多。请注意,user-agent
名称是 gfgBot——您可以将其更改为您喜欢的名称。您甚至可以使用 GoogleBot 来欺骗网站,使其认为您的爬虫是 Google 的蜘蛛机器人,只要它使用这种方法来找出机器人。 - 第 10 行:我们正在为交流创建上下文。对于任何你需要背景的东西——讲一个故事你需要一个背景。要在 OpenGL 中创建一个窗口,您需要一个上下文 - 对于 HTML5 Canvas 和PHP网络通信相同!对不起,如果我脱离了“背景”,但我必须这样做。
- 第 13 行:创建一个DomDocument ,它基本上是一种用于处理 DOM 的数据结构,通常用于 HTML 和 XML 文件。
- 第 14 行:我们通过提供文档的内容来加载 HTML!这个过程可能会产生一些警告(因为它已被弃用)所以我们禁止所有警告。
- 第 17 行:我们基本上创建了一个包含在 DOM 中找到的所有锚节点的数组。
- 第 21 行:我们打印这些锚节点引用的所有链接。
- 第 24 行:我们获取网站example.com中的所有链接!它只有一个输出的链接。
稍微复杂一点的蜘蛛机器人:在前面的代码中,我们有一个基本的蜘蛛机器人,它很好,但它更像是一个爬虫而不是爬虫(关于爬虫和爬虫之间的区别,请参阅这篇文章)。我们没有递归——我们没有“跟踪”我们得到的链接。所以在这个迭代中,我们将这样做,并且我们还将假设我们有一个数据库,我们将在其中插入链接(用于索引)。任何链接都将通过
insertIntoDatabase
函数插入到数据库中!- 程序:
5){ echo "
The Crawler is giving up!"; return; } $options = array( 'http' => array( 'method' => "GET", 'user-agent' => "gfgBot/0.1\n" ) ); $context = stream_context_create($options); $doc = new DomDocument(); @$doc -> loadHTML(file_get_contents($url, false, $context)); $links = $doc->getElementsByTagName('a'); foreach ($links as $i){ $link = $i->getAttribute('href'); if (ignoreLink($link)) continue; $link = convertLink($url, $link); if (!in_array($link, $crawledLinks)){ $crawledLinks[] = $link; $crawling[] = $link; insertIntoDatabase($link, $depth); } } foreach ($crawling as $crawlURL){ echo ("". "[+] Crawling $crawlURL
"); followLink($crawlURL, $depth+1); } if (count($crawling)==0) echo ("". "[!] Didn't Find any Links in $url!
"); } // Converts Relative URL to Absolute URL // No conversion is done if it is already in Absolute URL function convertLink($site, $path){ if (substr_compare($path, "//", 0, 2) == 0) return parse_url($site)['scheme'].$path; elseif (substr_compare($path, "http://", 0, 7) == 0 or substr_compare($path, "https://", 0, 8) == 0 or substr_compare($path, "www.", 0, 4) == 0) return $path; // Absolutely an Absolute URL!! else return $site.'/'.$path; } // Whether or not we want to ignore the link function ignoreLink($url){ return $url[0]=="#" or substr($url, 0, 11) == "javascript:"; } // Print a message and insert into the array/database! function insertIntoDatabase($link, $depth){ echo ( "". "Inserting new Link:- $link". "
" ); $crawledLinks[]=$link; } followLink("http://guimp.com/") ?> - 输出:
Inserting new Link:- http://guimp.com//home.html [+] Crawling http://guimp.com//home.html Inserting new Link:- http://www.guimp.com Inserting new Link:- http://guimp.com//home.html/pong.html Inserting new Link:- http://guimp.com//home.html/blog.html [+] Crawling http://www.guimp.com Inserting new Link:- http://www.guimp.com/home.html [+] Crawling http://www.guimp.com/home.html Inserting new Link:- http://www.guimp.com/home.html/pong.html Inserting new Link:- http://www.guimp.com/home.html/blog.html [+] Crawling http://www.guimp.com/home.html/pong.html [!] Didn't Find any Links in http://www.guimp.com/home.html/pong.html! [+] Crawling http://www.guimp.com/home.html/blog.html [!] Didn't Find any Links in http://www.guimp.com/home.html/blog.html! [+] Crawling http://guimp.com//home.html/pong.html [!] Didn't Find any Links in http://guimp.com//home.html/pong.html! [+] Crawling http://guimp.com//home.html/blog.html [!] Didn't Find any Links in http://guimp.com//home.html/blog.html!
解释:
- 第 3 行:创建一个全局数组 -
$crawledLinks
,其中包含我们在会话中捕获的所有链接。我们将使用它来查找链接是否已经在数据库中!在数组中查找比在哈希表中查找效率低。我们可以使用哈希表,但它不会比数组高效,因为键是很长的字符串(URL)所以我相信使用数组会更快。 - 第 8 行:我们告诉解释器我们正在使用我们刚刚创建的全局数组
$crawledLinks
!在下一行中,我们创建了一个新数组$crawling
,它将简单地包含我们当前正在爬过的所有链接。 - 第 31 行:我们忽略所有未链接到外部页面的链接!链接可以是内部链接、深层链接或系统链接!这个函数不会检查每一种情况(这会使它变得很长),而是检查两种最常见的情况——链接是内部链接和链接引用 javascript 代码时。
- 第 33 行:我们将链接从相对链接转换为绝对链接,并进行一些其他转换(如//wikipedia.org到http://wikipedia.org或https://wikipedia.org ,具体取决于原始 URL 的方案)。
- 第 35 行:我们只是检查我们正在迭代的
$link
是否不在数据库中。如果是,那么我们忽略它——如果不是,我们将它添加到数据库以及$crawling
数组中,以便我们也可以跟踪该 URL 中的链接。 - 第 43 行:爬虫在这里递归。它遵循它必须遵循的所有链接(在
$crawling
数组中添加的链接)。 - 第 83 行:我们调用
followLink("http://guimp.com/"),
我们使用 URL http://guimp.com/作为起点,因为它恰好是(或声称是)最小的世界上的网站。
更高级的蜘蛛机器人:在之前的迭代中,我们递归地跟踪我们在页面上获得的所有链接并将它们添加到数据库中(这只是一个数组)。但是我们只将 URL 添加到数据库中,然而,搜索引擎对每个页面都有很多字段——缩略图、作者信息、日期和时间,最重要的是页面标题和关键字。有些甚至有页面的缓存副本以加快搜索速度。然而,我们将——为了简单起见,只从页面中删除标题、描述和关键字。
注意:由您决定使用哪个数据库——PostgreSQL、MariaDB 等,我们只会输出Inserting URL/Text 等,因为处理外部数据库超出了本文的范围!
描述和关键字出现在元标记中。一些搜索引擎搜索(几乎)完全基于元数据信息,而一些搜索引擎并没有赋予它们太多相关性。谷歌甚至不考虑它们,它们的搜索完全基于页面的流行度和相关性(使用 PageRank 算法),并且生成关键字和描述,而不是从元标记中提取它们。谷歌不会惩罚没有任何描述或关键字的网站。但它确实会惩罚没有标题的网站。我们的概念搜索引擎(将使用这个“高级”蜘蛛机器人构建)会做相反的事情,它会惩罚没有描述和关键字的网站(即使它会将它们添加到数据库中但它会给它们带来较低的排名) 并且它不会惩罚没有标题的网站。它将网站的 URL 设置为标题。
- 程序:
MAX_DEPTH){ echo "
The Crawler is giving up!"; return; } $options=array( 'http'=>array( 'method'=>"GET", 'user-agent'=>"gfgBot/0.1\n" ) ); $context=stream_context_create($options); $doc=new DomDocument(); @$doc->loadHTML(file_get_contents($url, false, $context)); $links=$doc->getElementsByTagName('a'); $pageTitle=getDocTitle($doc, $url); $metaData=getDocMetaData($doc); foreach ($links as $i){ $link=$i->getAttribute('href'); if (ignoreLink($link)) continue; $link=convertLink($url, $link); if (!in_array($link, $crawledLinks)){ $crawledLinks[]=$link; $crawling[]=$link; insertIntoDatabase($link, $pageTitle, $metaData, $depth); } } foreach ($crawling as $crawlURL) followLink($crawlURL, $depth+1); } function convertLink($site, $path){ if (substr_compare($path, "//", 0, 2)==0) return parse_url($site)['scheme'].$path; elseif (substr_compare($path, "http://", 0, 7)==0 or substr_compare($path, "https://", 0, 8)==0 or substr_compare($path, "www.", 0, 4)==0) return $path; else return $site.'/'.$path; } function ignoreLink($url){ return $url[0]=="#" or substr($url, 0, 11) == "javascript:"; } function insertIntoDatabase($link, $title, &$metaData, $depth){ echo ( "Inserting new record {URL= $link". ", Title = '$title'". ", Description = '".$metaData['description']. "', Keywords = ' ".$metaData['keywords']. "'}
" ); $crawledLinks[]=$link; } function getDocTitle(&$doc, $url){ $titleNodes=$doc->getElementsByTagName('title'); if (count($titleNodes)==0 or !isset($titleNodes[0]->nodeValue)) return $url; $title=str_replace('', '\n', $titleNodes[0]->nodeValue); return (strlen($title)<1)?$url:$title; } function getDocMetaData(&$doc){ $metaData=array(); $metaNodes=$doc->getElementsByTagName('meta'); foreach ($metaNodes as $node) $metaData[$node->getAttribute("name")] = $node->getAttribute("content"); if (!isset($metaData['description'])) $metaData['description']='No Description Available'; if (!isset($metaData['keywords'])) $metaData['keywords']=''; return array( 'keywords'=>str_replace('', '\n', $metaData['keywords']), 'description'=>str_replace('', '\n', $metaData['description']) ); } followLink("http://example.com/") ?> - 输出:
Inserting new record {URL= https://www.iana.org/domains/example, Title = 'Example Domain', Description = 'No Description Available', Keywords = ' '}
说明:没有什么突破性的变化,但我想解释一些东西:
- 第 3 行:我们正在创建一个新的全局常量
MAX_DEPTH
。以前我们只是使用 5 作为最大深度,但这次我们使用MAX_DEPTH
常量代替它。 - 第 22行和第 23 行:我们基本上是在
$pageTitle
中获取页面的标题以及将存储在$metaData
变量(关联数组)中的描述和关键字。您可以参考第 64 行和第 72 行来了解抽象出来的信息。 - 第 31 行:我们将一些额外的参数传递给
insertIntoDatabase
函数。
我们的网络爬虫的问题:我们创建这个网络爬虫只是为了学习。将其部署到生产代码中(例如用它制作搜索引擎)可能会产生一些严重的问题。以下是我们的网络爬虫的一些问题:
- 它不可扩展我们的网络爬虫无法像 GoogleBot 一样爬取数十亿个网页。
- 它不太符合爬虫与网站通信的标准。它不遵循站点的
robots.txt
,即使站点管理员请求不这样做,它也会抓取站点。 - 它不是自动的。当然,它会“自动”获取当前页面的所有 URL 并抓取每个 URL,但它并不完全是自动的。它没有任何爬行频率的概念。
- 它不是分布式的。如果两个蜘蛛机器人正在运行,那么它们目前无法相互通信(查看另一个蜘蛛机器人是否没有爬取同一页面)
- 解析太简单了。我们的蜘蛛机器人不会处理编码标记(甚至编码 URL)
- 输出:现在,这不好——我们只得到一个链接——那是因为我们在站点
在评论中写代码?请使用 ide.geeksforgeeks.org,生成链接并在此处分享链接。