使用PHP进行网页抓取是入门抓取的最简单方法。抓取网站有很多好处,比如你可以监控竞争对手和供应商,检查价格变化,并通过观察数据趋势生成洞察。但首先你需要收集这些信息。
根据W3Techs的数据,WordPress支持了43%的网站。这意味着全球至少43%的网站依赖于PHP。因此,与其学习新语言、设置新环境、使用不同的服务器并从零开始解决各种问题,不如直接在你的网站旁运行一个PHP抓取器,这样要容易得多。
但即使使用PHP,也有不少挑战。首先,最大的挑战是正确渲染页面(包括JS代码)。其次,你需要找到避免被封锁的方法。最后,如何提取数据和与页面交互也存在一些挑战。
今天我们的目标是学习使用PHP进行网页抓取。你将学到以下内容:
- 网页抓取的基础知识
- 如何选择合适的PHP网页抓取工具
- 如何在不被封锁的情况下进行抓取
- 如何从页面中提取数据
- 使用PHP抓取器与页面交互
- 常见技术问题的FAQ
让我们开始吧!
PHP适合做网页抓取吗?
PHP并不是进行网页抓取的完美解决方案,但它非常容易学习和上手。对于大多数应用来说,它已经足够用了,而且入门门槛非常低。你只需要一个PHP服务器,这几乎可以从任何共享虚拟主机提供商那里获得。
不过要记住,它并不完美。PHP可能有点慢,而且你无法像在NodeJS或Python中那样运行大量并发的抓取任务。无论如何,如果你只是刚开始或者只是为自己的业务运行抓取任务,PHP已经绰绰有余了。
步骤一 – 选择合适的PHP抓取工具
如果选择了合适的工具,你99%的问题都能解决。想象一下,如果你在喝汤,你会选择叉子还是勺子?开发工具也是一样。经常会有一些工具可以用来完成任务,但它们的功能可能非常有限,使用这些工具可能不值得花费精力。
有很多工具可以选择,比如Guzzle、PHP-PhantomJS、Mink、Regex、Symfony/Panther、Chrome PHP等。幸运的是,这些工具大致可以分为四类,这使得分析它们变得更简单。以下是这四类工具:
1. 使用原生PHP函数(如Regex)从HTML代码中提取数据
经常有人建议获取页面的HTML代码,然后使用正则表达式或其他函数从中提取数据。通过这些函数,你可以查找特定的标签,然后读取数据。这听起来不错,对吧?
正则表达式在某些方面很出色,但网页抓取并不是其中之一。正则表达式就像是喝汤时用的叉子。在非常特定的场景下它能工作,但在大多数情况下会出问题。任何未闭合的标签、自闭合标签、动态元素,甚至有时一个点的位置不对,都会导致它失效。
如果你想学习正则表达式,那当然可以。但如果你想抓取一些页面,我们需要一个更强大的解决方案。
2. HTML解析器
这也是一个常见的建议。一些库会获取HTML代码并模拟渲染页面。它比正则表达式好一些,但仍然非常有限。你受限于解析器的能力。如果解析器没有实现特定的浏览器功能,你就无法获得所需的结果。很难证明投资时间在一个一开始就过时的解决方案上是合理的。
3. 加载非HTML请求(API、XHR)
这是一种聪明的方法,可以在不使用网页抓取器的情况下抓取元素。在这种方法中,你检查目标网站是否有API,或者通过查看XHR请求来确定它是否动态加载数据。
听起来很复杂,但其实很简单。打开目标网站,然后打开开发者工具并检查网络标签。只选择XHR请求(你需要重新加载页面以查看所有请求):
这里是reddit.com。但请注意“今日趋势”选项卡实际上是如何作为JSON对象加载的。因此,如果希望抓取特定的数据,只需要重复这个XHR请求。所以,如果你浏览https://reddit.com/api/trending_searches_v1.json?withAds=1&subplacement=tile&raw_json=1&gilding_detail=1
将获得针对当前日期更新的相同JSON对象。这同样适用于大多数网站,甚至是你意想不到的网站。任何WordPress站点都默认启用了JSON API。因此,如果想要抓取它,只需要访问它的端点。
例如,你可以在这里看到TechCrunch的最新帖子: https://techcrunch.com/wp-json/wp/v2/posts
大多数自定义文章类型(如页面和产品)也是如此。所以,如果你想监控一个WooCommerce商店的价格,你可以直接从目标的API中加载这些信息。
说到实际使用PHP加载这些数据,只需要一个函数:cURL。不过我们不会在这里深入探讨cURL,因为我们有一个更好的工具推荐给你——而且你还可以用它来加载XHR请求。
现在是时候介绍你进行PHP网页抓取的最佳伙伴了:无头浏览器。
4. 无头浏览器
归根结底,抓取基本上是通过代码模拟用户行为,而无头浏览器可以让你做到这一点。使用无头浏览器,你可以通过代码打开并控制浏览器的操作。
这是因为像Firefox和Chrome这样的浏览器提供了一个API,允许开发者与它们进行交互。因此,你可以做任何你想做的事情。你可以填写表单、按键、移动鼠标光标、点击、截屏(包括全页或裁剪后的截图)。你甚至可以运行任意的JS代码,这为实现你能想象到的任何操作打开了大门。
在本教程中,我们使用的是Chrome PHP,这是一个维护得非常好的库,用于控制基于Chromium的浏览器。如果你想跟着一起操作,可以使用Composer来安装它:
composer require chrome-php/chrome
如果你从未使用过Composer,它可以帮助你管理代码库,加载所有的依赖项。因此,当你下载chrome-php/chrome时,会创建多个文件夹,包括Symfony、evenement、monolog等。
如果你完全不想使用Composer,你可以使用https://php-download.com/或者https://github.com/Wilkins/composer-file-loader。一旦你安装了ChromePHP,你可以从命令行运行它,或者创建一个类似这样的文件:
<?php use HeadlessChromium\BrowserFactory; //don't forget to load the library and dependencies require_once 'vendor/autoload.php'; $browserFactory = new BrowserFactory(); // starts headless chrome $browser = $browserFactory->createBrowser(); try { // creates a new page and navigates to an URL $page = $browser->createPage(); $page->navigate('http://ipv4.icanhazip.com')->waitForNavigation(); // get the IP from a tag $el = $page->dom()->querySelector('pre'); //get the text from the element $text = $el->getText(); //output the result echo "Your IP Address is $text"; } finally { // close the connection $browser->close(); } ?>
然后在浏览器中打开这个文件,一切都设置好了。
步骤二- 如何避免PHP抓取被封锁
检测网站抓取的方法有很多种。无论你是加载XHR请求、使用API,还是使用无头浏览器,如果不小心,你都会被封锁。因此,你需要使用代理来混淆身份,避免被发现。
在抓取过程中,最简单的实现方法是使用住宅代理服务。通过它们,你可以使用住宅IP地址加载网站,每次都会轮换地址。
这样,当你加载第一个页面时,网站会认为你来自美国,而在下一个页面,目标网站会认为你在西班牙。对他们来说,这就像是两个不同的用户在查看两个不同的页面。因此,他们无法封锁你。
比如我们以IPRoyal为例,如果你已经购买了它的住宅IP,就可以访问客户端区域,在那里你可以看到你的凭证:
代理认证怎么办?
现在是时候介绍第一个大问题了。Chrome不允许你使用代理认证头。这意味着,当你启动浏览器时,你可以设置代理服务器的IP地址,像这样:
$browser = $browserFactory->createBrowser([ 'customFlags' => [ '--proxy-server=http://geo.iproyal.com:12321' ] ]);
但你无法将用户名/密码信息传递给代理服务器。但有两种解决方案——一种是简单快捷的方法,另一种是比较繁琐的方法。
简单快捷的方法是将你的IP地址列入IPRoyal的白名单。点击白名单标签,然后在那里添加你的IP地址。来自这个IP地址的所有连接都不需要凭证。
如果你有固定IP或者可以在开始抓取之前手动更新这些信息,这种方法效果很好。如果这不可能,你可以使用mitmproxy。通过它,你基本上可以设置一个“桥接”代理。你不会直接连接到IPRoyal,而是连接到这个桥接代理,然后它会带着你的凭证连接到IPRoyal。
你甚至可以在自己的电脑上安装mitmproxy,这样所有连接到你本地主机特定端口的请求都会转发到IPRoyal。因此,你的PHP代码会像这样:
$browser = $browserFactory->createBrowser([ 'customFlags' => [ '--proxy-server=http://localhost:3128' ] ]);
这个3128端口是使用您的凭证连接到http://geo.iproyal.com:12321的端口。
步骤三 – 使用PHP提取抓取的数据
现在你可以安全地连接到任何网站,是时候开始抓取了!你可以使用多种方法来提取数据,但有三种方法能够涵盖大部分需求:
1. 截图 / 生成PDF
一旦你连接到一个页面,你可以使用以下代码进行截图:
$page->screenshot()->saveToFile('/foo/bar.png');
你可以用下面的代码保存pdf文件:
$page->pdf(['printBackground' => false])->saveToFile('/foo/bar.pdf');
每一种方法都有很多选择。以下是截图选项的总结:
- 在启动时调整浏览器窗口的大小:
$browser = $browserFactory->createBrowser([ 'windowSize' => [1920, 1000] ]);
- 截图格式(默认为png):
$screenshot = $page->screenshot([ 'format' => 'jpeg' ]);
- 图像质量:
$screenshot = $page->screenshot([ 'quality' => 90 ]);
- 裁剪截图区域:
$clip = new Clip($x, $y, $width, $height); $screenshot = $page->screenshot([ 'clip' => $clip ]);
- 全页截图:
$screenshot = $page->screenshot([ 'captureBeyondViewport' => true, 'clip' => $page->getFullPageClip() ]);
这些是PDF选项:
$options = [ 'landscape' => true, // default to false 'printBackground' => true, // default to false 'displayHeaderFooter' => true, // default to false 'preferCSSPageSize' => true, // default to false (reads parameters directly from @page) 'marginTop' => 0.0, // defaults to ~0.4 (must be a float, value in inches) 'marginBottom' => 1.4, // defaults to ~0.4 (must be a float, value in inches) 'marginLeft' => 5.0, // defaults to ~0.4 (must be a float, value in inches) 'marginRight' => 1.0, // defaults to ~0.4 (must be a float, value in inches) 'paperWidth' => 6.0, // defaults to 8.5 (must be a float, value in inches) 'paperHeight' => 6.0, // defaults to 8.5 (must be a float, value in inches) 'headerTemplate' => '<div>foo</div>', // valid HTML code 'footerTemplate' => '<div>foo</div>', // valid HTML code 'scale' => 1.2, // defaults to 1.0 (must be a float) ];
2. 输出 / 保存到变量
另一种处理变量的方法是输出或保存它们。你可以使用方法来查询CSS选择器或xPath,然后获取该标签的文本内容。下面是一个示例:
//CSS selector $el = $page->dom()->querySelector('pre'); //get the text from the element $text = $el->getText(); //output the result echo "Your IP Address is $text"; You can do the same with xPath: $elem = $page->dom()->search('//div/*/a'); And even get attributes instead of the text contents: $attr = $elem->getAttribute('class');
3. 执行JavaScript函数
你可以将PHP抓取与JavaScript函数结合使用。这非常方便!这是因为无头浏览器会执行一段JS代码,然后将结果返回,就像你在检查控制台日志一样。下面是一个简单的示例:
//connecting to a URL page = $browser->createPage(); $page->navigate('https://en.wikipedia.org/wiki/Operation_Sandwedge')->waitForNavigation(); //run JS code to get a tag by ID $evaluation = $page->evaluate('document.getElementById("firstHeading").textContent'); $value = $evaluation->getReturnValue(); // output it echo $value;
这段代码加载了一个维基页面,然后返回一个ID的内容。如果你想预处理数据,可以使用类似的方法。
例如,你可以在页面上运行一段JS代码来获取价格列表中的最低价格。这可以节省后续的处理时间,并且如果价格高于某个阈值,你可以忽略该页面。
就是这样。通过这三个步骤,你基本上已经准备好设置一个无头浏览器,安全地连接到目标页面并提取数据。现在让我们来看一些额外内容。
如何与目标页面进行交互
有时候,仅仅加载一个页面不足以成功使用PHP抓取页面。也许你需要填写一个表单。有时你需要向下滚动以触发懒加载脚本。无论你需要什么,有一些Chrome PHP方法可以帮助你。
你可以使用精确的命令与鼠标进行交互:
$page->mouse() ->move(20, 40) // Moves mouse to position x=20; y=40 ->click() // left-click on position set above ->click(['button' => Mouse::BUTTON_RIGHT]; // right-click on position set above ->scrollUp(100); // scroll up 100px
或者你可以选择一个特定的元素并点击它:
$page->mouse()->find('#myID')->click(); // find and click on an element with id "myID" $page->mouse()->find('.myclass', 5); // find and click on the 5th (or last) element with class "myclass"
也有使用键盘的方法:
$page->keyboard() ->typeRawKey('Tab') // type a key, such as Tab ->typeText('test'); // type the text "test"
你可以通过点击表单字段,然后输入:
$elem->click(); $elem->sendKeys('[email protected]');
常见问题解答 (FAQ)
致命错误:未捕获的 HeadlessChromium\Exception\OperationTimedOut
有时Chromium应用加载时间过长。如果每次执行抓取程序时都发生这种情况,尝试增加其时间限制。你可以在浏览器调用时这样设置:
$browser = $browserFactory->createBrowser([ 'startupTimeout' => 60 // time in seconds ]);
致命错误:在 vendor\chrome-php\chrome\src\Browser\BrowserProcess.php 中的浏览器进程
这通常发生在应用未安装或你使用了不同的应用名称时。你可以在启动浏览器工厂时提供正确的应用名称来解决这个问题:
$browserFactory = new BrowserFactory( chromeBinary: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' );
未捕获的 RuntimeException:Chrome 进程在启动完成前停止
这可能是上述相同的问题。也许应用名称错误。此外,值得检查你当前的Chrome版本是否与Chrome PHP兼容。他们声称支持Chrome 65+。
致命错误:未找到 HeadlessChromium\BrowserFactory
不要忘记在项目中包含ChromePHP文件。你可以使用以下代码行来实现:
require_once 'vendor/autoload.php';
错误的对象类型或未捕获的错误:在空对象上调用成员函数 getText()
如果你看到关于对象类型的致命错误或奇怪的消息,检查网页抓取过程是否正常工作。并检查选择器是否返回有效元素。有时数据为空,这意味着在加载页面或元素时出现了问题。
结 论
现在,你学会了如何使用PHP进行网页抓取。你从选择合适的工具开始,逐步了解了复杂页面交互的过程。同时,你也学会了使用住宅代理的重要性,以避免被封锁。
到今天结束时,你应该能够使用PHP从任何网站提取数据。此外,由于PHP在Web开发中被广泛使用,你可以将这些数据拉取到自己的服务器上。你还可以通过将Node.js抓取工具与其他PHP库(如WordPress插件)结合使用,来增加趣味性。
希望你喜欢这次学习,我们下次再见!