openbuildings/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 的文本内容,填写一些输入并提交表单。

领域特定语言(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() 方法。它将执行所需操作,就像有人点击它一样,加载新页面并更新当前_url / current_path 获取器的结果。
  • select_option():如果节点是选项标签,则可以使用此方法选择它。这将取消选择 SELECT 标签中任何其他选中的选项,除非其标记为 "multiple"。
  • unselect_option():与 select_option() 相反。
  • hover($x = NULL, $y = NULL):将鼠标悬停在当前节点上,触发 JavaScript / CSS 事件和状态。您可以可选地传递 x 和 y 偏移量,以便可以微调。
  • drop_files($files):这将触发与在 DOM 元素(HTML5 功能)上放置文件相关的所有 JavaScript 事件。

例如,有一个这样的示例表单

<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、标题、内容文本或标签内图像的 alt 文本查找 label 标签。
  • button - 通过 id、标题、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()):使用“字段”定位类型查找文件输入标签并将其设置为“$file”。
  • select($select, $option_filters, array $filters = array()):使用“字段”定位类型查找选择标签,并标记其中一个或多个选项为“已选中”。如果$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,允许你轻松地链式调用。考虑 Finders 部分的上一个示例 - 你可以重写如下:

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序列化返回方法。可选地,您可以提供回调,结果将是回调的第一个参数(第二个是节点本身)。
  • screenshot($file):捕获页面的当前状态,将其作为PNG图像放置在文件$中。

处理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作为其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文件。