hoannc/spiderling

使用kohana或phantomjs爬取网页。

0.4.0 2020-02-17 10:01 UTC

README

Build Status Scrutinizer Quality Score Code Coverage Latest Stable Version

这是一个使用curl和PhantomJS爬取网页的库。深受Capybara的启发。它是phpunit-spiderling集成级别测试的主要组件。它可以轻松处理AJAX请求,并允许在不修改代码的情况下轻松地从仅PHP的快速驱动程序切换到支持JavaScript的如PhantomJS。

一个快速示例

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com');

$li = $page->find('ul.nav > li.test');

echo $li->text();

$page
  ->fill_in('Name', 'New Name')
  ->fill_in('Description', 'some description')
  ->click_button('Submit');

这将输出HTML节点li.test的文本内容,填写一些输入并提交表单。

Domain Specific Language (DSL)

Page对象具有丰富的DSL用于访问内容和填写表单

导航

  • visit($page, $query):将浏览器导向打开新的URL
  • current_url():获取当前URL——这将受重定向或脚本以任何方式更改浏览器URL的影响。
  • current_path():这将返回URL,不包括协议和主机部分,对于编写更通用的代码很有用。

获取器

每个节点代表页面上的一个HTML标签,你可以使用广泛的获取方法来探测其内容。所有这些获取器都是动态的,这意味着没有缓存涉及,每个方法都会向适当的驱动程序发送调用。

  • is_root():检查当前元素是否是根“节点”
  • id():获取当前节点的'id'——这个ID唯一标识当前页面上的节点。
  • html():获取当前节点的原始html——这就像在DOM元素上调用outerHTML一样。
  • tag_name():获取DOM元素的标签名。例如:DIV,SPAN,FORM,SELECT
  • attribute($name):获取当前标签的属性。如果标签为空,例如<div disabled />,则返回空字符串。如果没有属性,则返回NULL
  • text():获取HTML标签的文本内容——这类似于浏览器渲染HTML标签的方式,所有空白都会合并为单个空格。
  • is_visible():检查节点是否可见。如果项通过JS,CSS或内联样式隐藏,PhantomJS驱动程序将返回正确值。
  • is_selected():检查选项标签是否“选中”
  • is_checked():检查输入标签是否“选中”
  • value():获取输入表单标签的值

以下是一些获取器的使用示例,如果我们有这个页面

<html>
  <body>
    <ul>
      <li class="first"><span>LABEL</span> This is the first link</li>
      <li class="some class"><span>LABEL</span> Go <a href="http://example.com">somewhere</a></li>
    </ul>
  </body>
</html>

那么你可以编写以下PHP代码

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-page');

$li = $page->find('ul > li.first');

// Will output "LI"
echo $li->tag_name();

// Will output "first"
echo $li->attribute('class');

// Will output "LABEL This is the first link"
echo $li->text();

// Will output "<li class="first"><span>LABEL</span> This is the first link</li>"
echo $li->html();

设置器

Spiderling还提供了修改当前页面的能力,填写输入字段,点击按钮和链接,提交表单。这可以通过低级设置器完成

  • set($value):如果节点表示一个输入字段(input,textarea或甚至select),可以使用此方法设置其值。
  • append($text):如果您需要将一些文本附加到文本字段或输入,可以使用append()方法而不是set()——这允许用更少的往返到驱动程序来完成操作。
  • click():如果节点表示可以点击的东西(如链接或按钮),可以使用该节点的click()方法。它将执行所需的操作,就像人点击它一样,加载新页面并更新current_url / current_path获取器的结果。
  • select_option():如果节点是选项标签,则可以使用此方法选择它。这将取消选择SELECT标签中选中的任何其他选项,除非其标记为“multiple”
  • unselect_option():与select_option()相反
  • hover($x = NULL, $y = NULL):将鼠标悬停在当前节点上,触发javascript/css事件和状态。你可以选择性地传入x和y偏移量以便进行微调。
  • drop_files($files):这将触发所有与在dom元素上拖放文件相关的JavaScript事件(HTML5特性)。

例如,有一个这样的表单

<html>
  <body>
    <form action="/submit" method="post">
      <div class="row">
        <label for="text-input">Name:</label>
        <input type="text" id="text-input" name="name" />
      </div>
      <div class="row">
        <label for="description-input">Description:</label>
        <textarea name="description" id="description-input" cols="30" rows="10">
          Some text
        </textarea>
      </div>
      <input type="submit" value="Submit"/>
    </form>
  </body>
</html>

我们可以这样做脚本

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-form');

$page
  ->find('#text-input')
    ->set('New Name');

$page
  ->find('#description-input')
    ->append(' with some additions');

$page
  ->find('input[type="submit"]')
    ->click();

// This will return the submitted action of the form, e.g. http://example.com/submit
echo $page->current_url();

定位器

你可以通过CSS选择器(默认)以及输入元素、按钮和链接的特殊查找器来找到元素。这被称为“定位器类型”。

  • css - 默认
  • xpath - 使用XPath
  • link - 通过id、链接中的标题、文本或链接内图片的alt文本来查找链接。
  • field - 通过id、name、标签文本、指向此输入的文本、输入的占位符或没有值的select标签的选项文本来查找输入元素(TEXTAREA、INPUT、SELECT)。
  • label - 通过id、title、标签内的文本内容或标签内图片的alt文本来查找label标签。
  • button - 通过id、title、name、value、按钮内的文本或按钮内图片的alt文本来查找按钮。

所有这些定位器类型都允许你轻松扫描页面并选择你想要点击或填写的内容,而无需查看页面的html结构。任何可以输入选择器的位置,都可以输入array('{locator type}', '{selector}')来改变默认的定位器类型。

以下是一个使用之前HTML的示例

<html>
  <body>
    <form action="/submit" method="post">
      <div class="row">
        <label for="text-input">Name:</label>
        <input type="text" id="text-input" name="name" />
      </div>
      <div class="row">
        <label for="description-input">Description:</label>
        <textarea name="description" id="description-input" cols="30" rows="10">
          Some text
        </textarea>
      </div>
      <input type="submit" value="Submit"/>
    </form>
  </body>
</html>

PHP代码变得更加清晰和灵活 - 尽管底层HTML可以改变,但你的代码仍然可以按预期工作

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-form');

$page
  ->find(array('field', 'Name'))
    ->set('New Name');

$page
  ->find(array('field', 'Description'))
    ->set('some description');

$page
  ->find(array('button', 'Submit'))
    ->click();

// This will return the submitted action of the form, e.g. http://example.com/submit
echo $page->current_url();

过滤器

如果只用定位器还不够,你可以使用“过滤器”轻松地缩小搜索范围。它们遍历找到的候选者,过滤掉不符合条件的。要注意,它们会加载节点并逐个检查,这可能会影响性能,但在大多数情况下是可以接受的。

以下是可用的过滤器

  • visible:TRUE或FALSE - 过滤掉可见或不可见的节点
  • value:值的字符串 - 过滤掉没有匹配值的节点
  • text:文本的字符串 - 过滤掉没有给定文本的节点
  • attributes:数组 - 属性名称 > 属性值 - 过滤掉没有所有给定属性(名称和值)的节点
  • at:特别选择要返回的列表中的哪个节点,其他节点都被过滤掉。

以下是如何使用这些过滤器与HTML一起使用的一个示例

<html>
  <body>
    <ul>
      <li class="one">Row One</li>
      <li class="two">Row Two</li>
      <li style="display:none">Row Three</li>
    </ul>
    <select name="test">
      <option value="test">Option 1</option>
      <option value="test 2">Option 2</option>
    </select>
  </body>
</html>
use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-filters-page');

// Will output "Row One"
echo $page->find('li', array('text' => 'One'))->text();

// Will output "Row Three"
echo $page->find('li', array('visible' => FALSE))->text();

// Will output "Option 2"
echo $page->find('option', array('value' => 'test 2'))->text();

查找器

大多数定位器类型都有一个用于查找特定类型的自定义方法。还有一些其他有用的自定义查找器。

  • find($selector, array $filters = array()) - 默认,使用CSS选择器
  • find_field($selector, array $filters = array()) - 使用'field'定位器类型来查找输入元素
  • find_link($selector, array $filters = array()) - 使用'link'定位器类型来查找锚点标签
  • find_button($selector, array $filters = array()) - 使用'button'定位器类型
  • not_present($selector, array $filters = array()) - “find”的对立面,确保页面中没有元素
  • all($selector, array $filters = array()) - 返回一个Nodelist - 一个可迭代和可计数的类似数组对象,你可以轻松地使用'foreach'和'count'。请注意,它具有懒加载功能,所以只有当访问节点时,它才会由驱动程序加载。count()不会加载任何节点。

前面的表单示例可以重写为如下

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-form');

$page
  ->find_field('Name')
    ->set('New Name');

$page
  ->find_field('Description')
    ->set('some description');

$page
  ->find_button('Submit')
    ->click();

// This will return the submitted action of the form, e.g. http://example.com/submit
echo $page->current_url();

操作

您可以在页面上执行的一些常用操作(如修改输入、点击链接和按钮等)都有快捷方法,这可以使您的代码更加易于阅读和健壮。

以下都是这些操作

  • click_on($selector, array $filters = array()):通过CSS选择器找到节点并点击它
  • click_link($selector, array $filters = array()):通过“链接”定位器类型找到节点并点击它
  • click_button($selector, array $filters = array()):通过“按钮”定位器类型找到节点并点击它
  • fill_in($selector, $with, array $filters = array()):通过“字段”定位器类型找到节点,并使用"$with"设置其值。
  • choose($selector, array $filters = array()):通过“字段”定位器类型找到特定的单选输入标签。
  • check($selector, array $filters = array()):通过“字段”定位器和“勾选”找到复选框输入标签。
  • uncheck($selector, array $filters = array()):通过“字段”定位器和“取消勾选”找到复选框输入标签。
  • attach_file($selector, $file, array $filters = array()):通过“字段”定位器找到文件输入标签并将文件设置到它上面。
  • select($select, $option_filters, array $filters = array()):通过“字段”定位器找到select标签,并标记其一个或多个选项为“选中”。如果$option_filters是一个字符串,则设置具有该字符串值的选项,否则设置所有匹配筛选条件的选项。这允许按值、文本甚至位置进行选择。
  • unselect($select, $option_filters, array $filters = array()):与“select”相同,但匹配的选项将被“取消选中”
  • hover_on($select, array $filters = array()):将鼠标悬停在通过CSS选择器找到的元素上
  • hover_link($select, array $filters = array()):将鼠标悬停在通过链接定位器类型找到的元素上
  • hover_field($select, array $filters = array()):将鼠标悬停在通过“字段”定位器类型找到的元素上
  • hover_button($select, array $filters = array()):将鼠标悬停在通过“按钮”定位器类型找到的元素上

使用这些方法可以使您的代码非常易于阅读。此外,所有这些操作都返回$this,这使得您可以轻松地链式调用它们。考虑前面的示例在《查找器》部分 - 您可以将其重写如下

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-form');

$page
  ->fill_in('Name', 'New Name')
  ->fill_in('Description', 'some description')
  ->click_button('Submit');

// This will return the submited action of the form, e.g. http://example.com/submit
echo $page->current_url();

需要更复杂的示例。我们将使用以下HTML

<html>
  <body>
    <form action="/submit" method="post">
      <div class="row">
        <label for="text-input">Name:</label>
        <input type="text" id="text-input" name="name" />
      </div>
      <div class="row">
        <label>Features:</label>
        <ul>
          <li>
            <input type="checkbox" id="feature-input-1" name="feature_1" checked />
            <label for="feature-input-1">Feature One</label>
          </li>
          <li>
            <input type="checkbox" id="feature-input-2" name="feature_2" />
            <label for="feature-input-2">Feature Two</label>
          </li>
        </ul>
      </div>
      <div class="row">
        <label>State:</label>
        <ul>
          <li>
            <input type="radio" id="state-input-1" name="state" checked />
            <label for="state-input-1">Open</label>
          </li>
          <li>
            <input type="radio" id="state-input-2" name="state" />
            <label for="state-input-2">Closed</label>
          </li>
        </ul>
      </div>
      <div class="row">
        <label for="type-input">Type:</label>
        <select name="type" id="type-input">
          <option>Select an Option</option>
          <option value="big">Type Big</option>
          <option value="small">Type Small</option>
        </select>
      </div>
      <div class="row">
        <label for="text-input">Name:</label>
        <input type="text" id="text-input" name="name" />
      </div>
      <div class="row">
        <label for="description-input">Description:</label>
        <textarea name="description" id="description-input" cols="30" rows="10">
          Some text
        </textarea>
      </div>
      <button>
        <img src="/img/submit-form.png" alt="Submit" />
      </button>
    </form>
  </body>
</html>
use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-big-form');

$page
  ->fill_in('Name', 'New Name')
  ->uncheck('Feature One')
  ->check('Feature Two')
  ->choose('Closed')
  ->select('Type', array('text' => 'Type Small'))
  ->fill_in('Description', 'some description')
  ->click_button('Submit');

// This will return the submited action of the form,
// e.g. http://example.com/submit
echo $page->current_url();

嵌套

当页面上有多个元素时,您可能希望更具体一些,Spiderling允许您通过嵌套节点来实现这一点 - 您可以在节点的“内部”调用所有操作和查找器,这样查找器将只在节点的子元素中搜索。

例如

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-big-form');

$page
  ->fill_in('Name', 'New Name')
  ->find('.row', array('text' => 'Type'))
    ->choose('Closed')
  ->end()
  ->click_button('Submit');

请注意“end()”方法,这允许您返回到上一个级别并从那里继续工作。您还可以多次嵌套而不会出现问题(您将需要多次使用“end()”来“退出”嵌套)

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-big-form');

$page
  ->fill_in('Name', 'New Name')
  ->find('.row', array('text' => 'Type'))
    ->find('ul')
      ->choose('Closed')
    ->end()
  ->end()
  ->click_button('Submit');

其他

作为DSL的一部分,还有一些其他额外的方法

  • confirm($confirm):如果页面上打开了一个警告或确认对话框,您可以使用此方法来关闭它(通过提供FALSE)或批准它(对于确认对话框,提供TRUE)
  • execute($script, $callback = NULL):在页面上执行任意JavaScript,在给定节点的上下文中。您将能够将其作为回调的第一个参数访问,例如arguments[0]。JavaScript执行的结果将通过方法返回(通过JSON序列化传递)。可选地,您可以提供回调,结果将是回调的第一个参数(第二个是节点本身)
  • 截图($file):截取页面当前状态,将其作为PNG图片保存到$file中。

处理AJAX

Spiderling遵循与Capybara相同的理念,即它不明确支持或等待AJAX调用完成,然而每个查找器在元素未加载时不会立即得出失败结论,而是在抛出异常之前稍作等待(默认2秒)。在编写爬虫时,如果您有AJAX请求,需要搜索AJAX将要执行的改变。

例如

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->visit('http://example.com/the-big-form');

$page
  ->click_button('Edit')
  // This will wait for the appearance of the "edit" form, loaded via AJAX
  ->find('h1', array('text' => 'Edit Form'))
    // Enter a new name inside the form
    ->fill_in('Name', 'New Name');
    ->click_button('Save')
  ->end();
  // We wait a bit to make sure the form is closed,
  // also as it might take longer than normal,
  // we extend the wait time from 2 to 4 seconds.
$page
  ->next_wait_time(4000)
  ->find('.notification', array('text' => 'Saved Successfully'));

驱动程序

Spiderling的一个强大之处在于能够使用不同的驱动程序来执行代码。这允许您在不需要修改代码的情况下,从仅使用PHP curl解析页面切换到使用PhantomJS。例如,如果我们想使用PhantomJS驱动程序而不是默认的“Simple”驱动程序,那么我们需要这样做

use Openbuildings\Spiderling\Page;

$page = new Page(new Driver_Phantomjs);

$page->visit('http://example.com/the-big-form');

$page
  ->fill_in('Name', 'New Name')
  ->find('.row', array('text' => 'Type'))
    ->choose('Closed')
  ->end()
  ->click_button('Submit');

目前有4个驱动程序

  • Driver_Simple:使用PHP curl加载页面。不支持JavaScript或浏览器警告对话框
  • Driver_Kohana:使用Kohana框架的原生Internal Request类,完全不打开互联网连接 - 如果您的代码已经使用Kohana框架,那么它将非常高效。
  • Driver_Phantomjs:启动PhantomJS服务器。您需要安装PhantomJS并将其路径添加到PATH中。它会在随机端口上选择一个新端口,因此可以同时打开多个PhantomJS浏览器。

您可以通过扩展Driver类并实现自己的方法轻松编写自己的驱动程序。一些驱动程序可能不支持所有功能,因此不需要实现每个方法。

现在让我们详细了解一下每个驱动程序

Driver_Simple

使用curl加载HTML页面,然后使用PHP的本地DOM和XPath对其进行解析。所有查找器都非常快,因此如果您不依赖JavaScript或其他浏览器特定功能,这是您最好的选择。它也非常容易扩展,以便为特定Web框架创建“原生”版本 - 您需要实现的只是加载部分,例如,您可以通过查看“Driver_Kohana”类了解示例。

在每个请求之前,$_GET、$_POST和$_FILES被保存,填充适当的值,并在之后恢复,模拟真实的PHP请求。

除了通过curl加载HTML之外,您还可以直接设置内容,如果您已经通过其他方式加载了它。

以下是其外观

use Openbuildings\Spiderling\Page;

$page = new Page();

$big_form_content = file_get_contents('big_content.html');

$page->content($big_form_content);

$page
  ->fill_in('Name', 'New Name')
  ->find('.row', array('text' => 'Type'))
    ->choose('Closed')
  ->end()
  ->click_button('Submit');

通常不建议自己执行POST请求,因为并非所有驱动程序都支持它们。但使用Driver_Simple,您可以执行任意请求,例如测试API调用。这是通过驱动程序直接完成的,如下所示

use Openbuildings\Spiderling\Page;

$page = new Page();

$page->driver()->post('http://example.com/api/endpoint', array(), array('name' => 'some post value'));

Driver_Kohana

使用Kohana框架的原生Internal Request(稍作修改以欺骗框架认为它是一个初始请求)。它扩展了Driver_Simple

此外,它处理重定向,限制最大重定向次数为8(可配置)并使用Request::$user_agent作为其用户代理。

示例使用

use Openbuildings\Spiderling\Page;

$page = new Page(new Driver_Kohana);

Driver_Phantomjs

使用此驱动程序,您可以使用PhantomJS执行所有查找和操作,使用真实的WebKit引擎和JavaScript,而无需任何图形环境(无头)。您需要将其安装到PATH中,可以通过调用“phantomjs”来访问。

您可以从这里下载: http://phantomjs.org/download.html

默认情况下,它会在4445和5000之间的随机端口上启动一个新的服务器。

如果已安装PhantomJS,则应该可以正常工作。

use Openbuildings\Spiderling\Page;

$page = new Page(new Driver_Phantomjs);

如果您想独立启动服务器,可以修改PhantomJS连接,您还可以将其设置为将消息输出到日志文件,以及调整其他参数。

use Openbuildings\Spiderling\Page;

$connection = new Driver_Phantomjs_Connection;
$connection->port(5500);
$connection->start('pid_file', 'log_file');

$driver = new Driver_Phantomjs($connection);

$page = new Page();

在启动时设置“pid文件”参数,允许驱动程序将phantomjs服务器进程的pid保存到该文件,并在再次启动时尝试清理服务器,从而确保您不会在各个地方都有运行的PhantomJS进程。

许可证

版权所有(c)2012-2013,OpenBuildings Ltd。由Ivan Kerin作为clippings.com的一部分开发。

在BSD-3-Clause许可证下,请阅读LICENSE文件。