nggiahao/spiderling

使用 kohana 或 phantomjs 爬取网页。

0.4.1 2020-06-08 09:28 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元素(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,这使得您可以轻松地进行链式调用。考虑一下“查找器”部分的先前的示例——您可以像这样重写它

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图像放置在“$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驱动程序而不是默认的“简单”驱动程序,则需要进行以下操作

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服务器。您需要在您的PATH中安装PhantomJS并使其可访问。它会随机选择一个新的端口,因此可以同时打开多个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 file”参数,允许驱动程序将phantomjs服务器进程的pid保存到该文件中,然后在再次启动时尝试清理服务器,从而确保您不会到处都有正在运行的PhantomJS进程。

许可证

版权所有(c)2012-2013,OpenBuildings Ltd。由Ivan Kerin在clippings.com项目中开发。

根据BSD-3-Clause许可,请阅读LICENSE文件。