cacing69 / cquery

PHP爬虫,具有语言表达式,可用于从使用javascript或ajax的网站抓取数据

v1.4.0 2023-09-14 12:52 UTC

This package is auto-updated.

Last update: 2024-09-17 16:30:09 UTC


README

Cquery(爬取查询)

Latest Version on Packagist Software License PRs Welcome StyleCI

关于Cquery

想要为像这样的网站创建查询以进行网络爬取,我认为这将使我能够从任何地方生成爬取查询。

from (.item)
define
    span > a.title as title
    attr(href, div > h1 > span > a) as url
filter
    span > a.title has 'history'
limit 1

变更日志

请参阅变更日志以获取更多关于最近更改的信息。

目前正在试验

尝试从网页提取数据,我认为这变得更加有趣,我创建这个的初衷是使能够利用js/ajax进行内容加载的网站的网页爬取变得更容易。

要使用js/ajax加载的页面进行网页爬取,您需要在这个包之外使用适配器,这是使用 symfony/panther 开发的。我不想将它作为cquery核心中的默认包,因为这个特性对某些人来说是可选的。请在此处检查并理解其用法。我将其称为 cacing69/cquery-panther-loader。有关symfony/panther的更多信息,您将在那里发现安装和其他信息。

此处提供的方法和用法说明都是根据我的需求设计的。如果您有任何建议或反馈以改进它们,我将不胜感激。

我希望有善良和有同情心的人能够构建一个像为cquery定制的工具一样的Web App/UI应用程序,其中包含一个文本区域(用于查询输入)和一个表格容器以显示结果,就像cquery游乐场一样,用于运行原始cquery。如果有人准备好了,我将为它创建一个API,并开始在 解析器 类上开发更多逻辑。

这是什么

Cquery是爬取查询的缩写,用于使用PHP从HTML元素中提取文本,简单来说,它是用于爬取/抓取网页的工具。它被称为查询,因为它采用SQL查询中存在的结构,因此您可以将其类比为您的DOM/HTML文档是一个您将查询的表。

让我们暂时玩一会儿,看看如何使网站爬取更容易,就像为数据库创建查询一样。

请注意,我还没有达到这个库的beta/稳定版本,所以可用的功能仍然非常有限。

我会非常欢迎每个人的任何支持/贡献。请参阅CONTRIBUTING.md以获取入门帮助。

我列出了一些利用高级功能的示例

快速安装

composer require cacing69/cquery

例如,您有一个如下所示的简单HTML元素。

点击显示HTML: src/Samples/sample.html
<!DOCTYPE HTML>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>Href Attribute Example</title>
</head>
<body>
  <span id="lorem">
    <div class="link valid">
      <h1 data-link-from-source="/url-1" id="title-id-1" class="class-title-1">Title 1</h1>
      <a class="ini vip class-1" data-custom-attr-id="12" href="http://ini-url-1.com">Href Attribute Example 1
      </a>
    </div>
    <div class="link">
      <h1 ref-id="23" id="title-id-2" class="class-title-1">Title 2</h1>
      <a class="vip class-2 nih tenied" data-custom-attr-id="212" href="http://ini-url-2.com">
        Href Attribute Example 2
        <p>Lorem pilsum</p>
      </a>
    </div>
    <div class="link">
      <h1 id="title-id-3" class="class-title-1">Title 3</h1>
      <a class="premium class-3" data-custom-attr-id="122" regex-test="a-abc-ab" href="http://ini-url-3.com">Href Attribute Example 4</a>
    </div>
    <div class="link">
      <h1>Title 11</h1>
      <a class="vip class-1 super blocked" data-custom-attr-id="132" regex-test="a-192-ab" href="http://ini-url-11.com">Href Attribute Example 78</a>
      </div>
    <div class="link">
      <h1>Title 22</h1>
      <a class="preview itu class-2 vip blocked" data-custom-attr-id="712" regex-test="b-12-ac" href="http://ini-url-22.com">Href Attribute Example 90</a>
    </div>
    <div class="link">
      <h1>Title 323</h1>
      <a class="nied premium class-3 blocked" data-custom-attr-id="132" href="http://ini-url-33-1.com">Href Attribute Example 5</a>
    </div>
    <div class="link pending">
      <h1>Title 331</h1>
      <a class="premium class-31 ended" data-custom-attr-id="121" regex-test="zx-1223-ac" customer-id="18" href="http://ini-url-33-2.com">Href Attribute Example 51</a>
    </div>
    <div class="link pending">
      <h1>Title 331</h1>
      <a class="test-1-item" data-custom-attr-id="121" customer-id="16" href="http://ini-url-33-2.com">Href Attribute Example 51</a>
    </div>
    <div class="link pending">
      <h1>12345</h1>
      <a class="premium class-32 denied" data-custom-attr-id="1652" customer-id="17" href="http://ini-url-33-0.com">Href Attribute Example 52</a>
    </div>
  </span>
  <p>
    <a href="https://www.freecodecamp.org/contribute/">The freeCodeCamp Contribution Page
  </p>
  <footer>
    <p>Copyright 2023</p>
  </footer>
</body>
</html>

可用的列表定义表达式

以下是您可以使用的表达式,它们可能随着时间的推移而更改。

别名规则列表

以下是您可以使用的功能,它们可能随着时间的推移而更改。
注意:已支持嵌套函数。

如何使用过滤器

注意:尚不支持嵌套过滤器。

因此,让我们开始爬取这个网站。

require_once 'vendor/autoload.php';

$html = file_get_contents("src/Samples/sample.html");
$data = new Cacing69\Cquery\Cquery($html);

$result = $query
        ->from("#lorem .link") // next will be from("(#lorem .link) as el")
        ->define(
            "h1 as title",
            "a as description",
            "attr(href, a) as url", // get href attribute from all element at #lorem .link a
            "attr(class, a) as class"
        )
        // just imagine this is your table, and every element as your column
        ->filter("attr(class, a)", "has", "vip") // add some filter here
        // ->orFilter("attr(class, a)", "has", "super") // add another condition its has OR condition SQL
        // ->filter("attr(class, a)", "has", "blocked") // add another condition its has AND condition SQL
        ->get(); // -> return type is \Doctrine\Common\Collections\ArrayCollection

或者您可以使用原始方法

require_once 'vendor/autoload.php';

$html = file_get_contents("src/Samples/sample.html");
$data = new Cacing69\Cquery\Cquery($html);

$result = $query
        ->raw("
            from (#lorem .link)
            define
              h1 as title,
              a as description,
              attr(href, a) as url,
              attr(class, a) as class
            filter
              attr(class, a) has 'vip'
        ");

这里是一些结果

Alt text

另一个使用匿名函数的示例

require_once 'vendor/autoload.php';

use Cacing69\Cquery\Definer;
$html = file_get_contents("src/Samples/sample.html");
$data = new Cacing69\Cquery\Cquery($html);

$result_1 = $data
          ->from("#lorem .link")
          ->define(
              "upper(h1) as title_upper",
              new Definer( "a", "col_2", function($value) use ($date) {
                  return "{$value} fetched on: {$date}";
              })
          )
          ->filter("attr(class, a)", "has", "vip")
          ->limit(2)
          ->get() // -> return type is \Doctrine\Common\Collections\ArrayCollection
          ->toArray();
点击显示输出:$result_1

Alt text

// another example, filter with closure
$result_2 = $data
            ->from("#lorem .link")
            ->define("reverse(h1) as title", "attr(href, a) as url")
            ->filter("h1", function ($e) {
                return $e->text() === "Title 3";
            })
            ->get() // -> return type is \Doctrine\Common\Collections\ArrayCollection
            ->toArray();
点击显示输出:$result_2

Alt text

如何从URL加载源页面

// another example, to load data from url used browserkit

$url = "https://free-proxy-list.net/";
$data = new Cquery($url);

$result_3 = $data
    ->from(".fpl-list")
    ->pick(
        "td:nth-child(1) as ip_address",
        "td:nth-child(4) as country",
        "td:nth-child(7) as https",
    )->filter('td:nth-child(7)', "=", "no")
    ->limit(1)
    ->get() // -> return type is \Doctrine\Common\Collections\ArrayCollection
    ->toArray();
点击显示输出:$result_3

Alt text

如何使用 append_node(a, b)

// another example, to load data from url used browserkit

$url = "http://quotes.toscrape.com/";
$data = new Cquery($url);

$result_4 = $data
              ->from(".col-md-8 > .quote")
              ->define(
                  "span.text as text",
                  "span:nth-child(2) > small as author",
                  "append_node(div > .tags, a)  as tags",
              )
              ->get() // -> return type is \Doctrine\Common\Collections\ArrayCollection
              ->toArray();
点击显示输出:$result_4

Alt text

另一个示例,如何使用 append_child() 并为每个项指定自定义键

// another example, to load data from url used browserkit

$url = "http://quotes.toscrape.com/";
$data = new Cquery($url);

$result_5 = $data
              ->from(".col-md-8 > .quote")
              ->define(
                  "span.text as text",
                  "append_node(div > .tags, a) as tags.key", // grab child `a` on element `div > .tags` and place it into tags['key']
              )
              ->get() // -> return type is \Doctrine\Common\Collections\ArrayCollection
              ->toArray();
点击显示输出:$result_5

Alt text

// another example, to load data from url used browserkit

$url = "http://quotes.toscrape.com/";
$data = new Cquery($url);

$result_6 = $data
              ->from(".col-md-8 > .quote")
              ->define(
                  "span.text as text",
                  "append_node(div > .tags, a) as _tags",
                  "append_node(div > .tags, a) as tags.*.text",
                  "append_node(div > .tags, attr(href, a)) as tags.*.url", // [*] means each index, for now ots limitd only one level
              )
              ->get() // -> return type is \Doctrine\Common\Collections\ArrayCollection
              ->toArray();
点击显示输出:$result_6

Alt text

如何使用 replace

  // how to use replace with single string
  $content = file_get_contents(SAMPLE_HTML);

  $data = new Cquery($content);

  $result = $data
      ->from(".col-md-8 > .quote")
      ->define(
          "replace('The', 'Lorem', span.text) as text",
      )
      ->get();

  // how to use replace with array arguments
  $data_2 = new Cquery($content);

  $result = $data_2
      ->from(".col-md-8 > .quote")
      ->define(
          "replace(['The', 'are'], ['Please ', 'son'], span.text) as text",
          // "replace(['The', 'are'], ['Please'], span.text) as text", // or you can do this if just want to use single replacement
      )
      ->get();

  // how to use replace with array arguments and single replacement
  $data_3 = new Cquery($simpleHtml);

  $result = $data_3
      ->from("#lorem .link")
      ->define("replace(['Title', '331'], 'LOREM', h1)  as title")
      ->get();

查询结果的操作方法

在CQuery中有两种方法可以操作查询结果。
  1. 每个项目闭包 ...->eachItem(function ($el, $i){})...->eachItem(function ($el){}) 示例
  ...->eachItem(function ($item, $i){
    $item["price"] = $i == 2 ? 1000 : $resultDetail["price"];

    return $item;
  })

基本上,您可以对每个项执行任何操作。在给定的示例中,它将插入一个新键“价格”到每个项中,如果索引等于2(第三项),则将价格设置为1000。

  1. 获取结果闭包 ...->onObtainedResults(function ($results){}) 示例
  ...->onObtainedResults(function ($results){
    // u can do any operation here

    return  array_map(function ($_item) use ($results) {
        $_item["sub"] = [
            "foo" => "bar"
        ];

        return $_item;
    }, $results);
  })

基本上,这是查询结果的数组,您可以对它们进行任何操作。为了另一个示例,我包括了一个示例,特别是对于您需要为每个条目从另一个页面加载不同详细信息的场景,您可以在这里检查检查异步多次请求

如何处理每个元素的多重请求

如果存在这种情况,您需要加载详细信息,而这些详细信息在另一个URL上,这意味着您必须加载每个页面。

您应该使用能够执行非阻塞请求的客户端,例如 amphp/http-clientguzzlephpreact/http 或以面向对象的方式使用 curl_multi_init,您应该检查 php-curl-class

我建议使用 phpreact 通过异步请求。

  use Cacing69\Cquery\Cquery;
  use React\EventLoop\Loop;
  use React\Http\Browser;
  use Psr\Http\Message\ResponseInterface;

  $url = "http://www.classiccardatabase.com/postwar-models/Cadillac.php";

  $data = new Cquery($url);

  $loop = Loop::get();
  $client = new Browser($loop);

  // detail is on another page
  $result = $data
            ->from(".content")
            ->define(
                ".car-model-link > a as name",
                "replace('../', 'http://www.classiccardatabase.com/', attr(href, .car-model-link > a)) as url",
            )
            ->filter("attr(href, .car-model-link > a)", "!=", "#")
            ->onObtainedResults(function ($results) use ($loop, $client){
                // I've come across a maximum threshold of 25 chunk, when I input 30, there is some null data.
                $results = array_chunk($results, 25);

                foreach ($results as $key => $_chunks) {
                    foreach ($_chunks as $_key => $_result) {
                        $client
                        ->get($_result["url"])
                        ->then(function (ResponseInterface $response) use (&$results, $key, $_key) {
                            $detail = new Cquery((string) $response->getBody());

                            $resultDetail = $detail
                                ->from(".spec")
                                ->define(
                                    ".specleft tr:nth-child(1) > td.data as price"
                                )
                                ->first();

                            $results[$key][$_key]["price"] = $resultDetail["price"];
                        });
                    }
                    $loop->run();
                }

                return $results;
            })
            ->get();

以下是使用 phpreact 的比较。

没有 phpreact

Alt text

使用 phpreact

Alt text

在这种情况下,有320行数据,并且每个细节都将被加载,这意味着将会有很多HTTP请求被发出以获取单个细节。

如何在页面加载后执行操作(点击链接/提交表单)

1. 提交表单

如果您需要提交数据以检索用于爬取的其他数据,您将需要处理这种情况。

  • 案例1:没有爬虫对象
$url = "https://user-agents.net/random";
$data = new Cquery($url);

$result = $data
  ->onContentLoaded(function (HttpBrowser $browser) {
      $browser->submitForm("Generate random list", [
          "limit" => 5,
      ]);

      return $browser;
  })
  ->from("section > article")
  ->define(
      "ol > li > a as user_agent",
  )
  ->get();

使用上面的代码,您将在数据中将限制(根据输入名称)设置为5时执行表单提交。

  • 案例2:有爬虫对象

让我们在维基百科上进行模拟,然后使用短语'sambas'进行搜索,看看结果是否与手动搜索匹配。

$url = "https://id.wikipedia.org/wiki/Halaman_Utama";
$data = new Cquery($url);

$result = $data
    ->onContentLoaded(function (HttpBrowser $browser, Crawler $crawler) {
        // This is a native function available in the dom-crawler.
        $form = new Form($crawler->filter("#searchform")->getNode(0), $url);

        $browser->submit($form, [
            "search" => "sambas",
        ]);
        return $browser;
    })
    ->from("html")
    ->define(
        "title as title",
    )
    ->get();

结果

Alt text

网页

Alt text

页面源代码

Alt text

  1. 点击链接如果您想在加载的页面上点击链接,请观察下面的代码。

Alt text

在开始爬取之前点击该链接

$url = "https://semver.org/";
$data = new Cquery($url);

$result = $data
    ->onContentLoaded(function (HttpBrowser $browser, Crawler $crawler) {
        $browser->clickLink("Bahasa Indonesia (id)");
        return $browser;
    })
    ->from("#spec")
    ->define(
        "h2 as text",
    )
    ->get();

点击链接的结果

Alt text

如何使用PHP爬取由js/ajax加载的网站

如果要爬取的网页使用JavaScript和AJAX处理其数据,那么您需要为cquery添加Panther-loader。

安装 composer-panther-loader

composer require cacing69/cquery-panther-loader

其他示例

可以在tests中找到包含示例代码的方法列表。

注意

我最近开始构建这个项目,如果有人感兴趣,我非常欢迎所有阅读/看过我这个小项目的每个人的反馈,任何形式(问题、拉取请求或其他)。然而,目前我正在考虑使其更加灵活和用户友好,以便进行网站抓取。

这只是开始,只要我能继续,我将继续开发它。

许可证

MIT许可证(MIT)。请参阅许可证文件以获取更多信息。