最近总监提出我们公司运营的一个网站运营数据有点差,亟待提升该网站的SEO(搜索引擎优化)体验。不然自然流量着实有点少,全靠氪金买百度付费流量,成本太高,显然不太现实。但是当时技术选型的时候并未考虑到SEO相关的问题,结果选了VUE框架。像vue、react这种SPA(单页应用)框架虽然开发效率高,性能和体验都得到市场认可,但是SEO却是天生的短板。项目开发运营几年了,现在重构成本太高,可SEO的问题不解决流量就起不来,没流量就不可能给公司创收,总不能看着大家辛苦几年打磨的项目就这样死掉。困难总是人去解决的不是,在领到任务之后,我开始各种查找资料、调研、反复验证,总算找到两个可落地,性价比又高的解决方案。tips: 欢迎关注作者微信公众号fever code
,获取最新技术分享。
Puppeteer无头浏览器
在分享方案之前先来聊聊什么是无头浏览器,因为我们今天的主角就是无头浏览器工具库(Puppeteer)。
无头浏览器(Headless Browser)是一种没有图形用户界面(GUI)的网络浏览器。所谓无头就是没有可视化的图形界面,因此不会占用屏幕空间,也不会在屏幕上显示任何内容,因此无头浏览器可以节省大量的资源和内存消耗。在功能设计上,无头浏览器提供了大量编程接口,使得开发者可以通过代码进行控制和操作浏览器以模拟用户行为,如点击、输入、提交表单等,这使得它在自动化测试、网页爬虫等场景中非常有用。常见的无头浏览器包括Chrome Headless、PhantomJS、Puppeteer等。简单点理解的话也可以理解为是没有用户界面的浏览器内核。
那什么是Puppeteer呢?Puppeteer 是一个由 Google 开发的 Node.js 库。它提供了一组高级 API,允许用户通过 DevTools 协议控制 Chromium 或 Chrome 浏览器。主要用途包括:自动化测试、网页爬取(网页抓取)、生成网页截图和 PDF、进行页面性能分析等任务。Puppeteer 允许用户以编程方式控制浏览器的行为,如模拟用户交互(点击按钮、填写表单)、导航到网页、修改页面内容、处理网络请求等。由于其深度集成 Chrome/Chromium 浏览器的能力,Puppeteer 使得开发者能够利用 Chrome 强大的网页渲染引擎和开发者工具功能,实现高度精确和复杂的自动化任务。
方案一:
使用nodejs启一个服务,通过Puppeteer请求目标网站,并获取经过Puppeteer渲染之后的html页面,返回给客户端,实现SSR(服务端渲染)。核心代码如下:
const puppeteer = require('puppeteer')
const express = require('express')
var app = express();
app.get('*', async (req, res) => {
let url = "https://www.demo.com" + req.originalUrl; // 目标网站URL
const browser = await puppeteer.launch(); // 启动Puppeteer浏览器
const page = await browser.newPage(); // 创建一个新页面
await page.goto(url, { waitUntil: 'networkidle2' }); // 跳转到目标网站并等待页面完全加载
const html = await page.content(); // 获取页面HTML代码
await browser.close(); // 关闭浏览器
res.send(html);
});
app.listen(3000, () => {
console.log('服务已启动在3000端口...');
});
启了nodejs服务之后还需要nginx服务器配合。具体架构为:用户向nginx服务器发起请求,nginx通过请求头信息判断是否为搜索引擎爬虫访问,如果为爬虫访问则将请求代理至nodejs服务,返回渲染好的html页面,供爬虫爬取页面内容,收录页面。如果为普通用户访问则正常访问目标网站,不走nodejs服务进行中转。
nginx配置如下:
location / {
proxy_set_header Host $host:$proxy_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
if ($http_user_agent ~* "Baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|bingbot|Sosospider|Sogou Pic Spider|Googlebot|360Spider") {
proxy_pass http://172.17.0.1:3000;
}
alias /usr/share/nginx/html/;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
方案二:
这种方案也可以采用java,php等服务端语言实现。这里使用php的puphpeteer库举例:
composer require nesk/puphpeteer
npm install @nesk/puphpeteer
public function index()
{
$url = 'http://cpservice.sipo.gov.cn/SecurityCode?timestamp='.time();
$ret = $this->ocr_code($url, 4, 1);
}
public function get_cookie(){
// @unlink('verify.png');
// @unlink('result.png');
$puppeteer = new Puppeteer;
$browser = $puppeteer->launch([
'headless'=>false
]);
try {
$page = $browser->newPage();
$page->goto('http://cpservice.sipo.gov.cn/index.jsp');
$img = $page->querySelector("#Verify");
$usernameInput = $page->querySelector("#username");
$usernameInput->focus(); //定位到用户名
$page->keyboard->type("91321102338928957N");
$passwordInput = $page->querySelector("#password");
$passwordInput->focus();
$page->keyboard->type("123123123");
$page->screenshot(['path' => 'verify.png',
'clip'=>[
'x'=>445,'y'=>513, 'width'=>70,'height'=>30
]
]);
$url = 'http://dp.cn/verify.png';
$ocr_url = 'http://dp.cn/index/index/ocr_code';
$debug_url = 'http://o2.cn/oxygen/user/front_log';
$js = <<<JS
var xmlhttp;
var ret = '1111';
if (window.XMLHttpRequest){
xmlhttp = new XMLHttpRequest();
}else{
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.open("POST","{$ocr_url}",false);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("url={$url}");
xmlhttp.onreadystatechange=function(){
if (xmlhttp.readyState==4 && xmlhttp.status==200){
ret = xmlhttp.responseText;
return;
}
}
if (xmlhttp.readyState==4 && xmlhttp.status==200){
ret = xmlhttp.responseText;
}else{
xmlhttp.open("GET","{$debug_url}?msg="+ JSON.stringify([xmlhttp.readyState, xmlhttp.status,xmlhttp.responseText, xmlhttp.responseXML]),true);
xmlhttp.send();
}
return ret;
JS;
$verifyFunction = JsFunction::create([], $js);
$dimensions = $page->evaluate($verifyFunction);
dump($dimensions);
if($dimensions != '1111' && $dimensions != ''){
$securityCodeInput = $page->querySelector("#securityCode");
$securityCodeInput->focus();
$page->keyboard->type($dimensions);
$page->screenshot(['path' => 'result.png']);
$loginFunction = JsFunction::create([], "
return login();
");
$dimensions2 = $page->evaluate($loginFunction);
$page->waitForNavigation(['timeout'=>5000]);
$jumpFunction = JsFunction::create([], "
return document.cookie;
");
$dimensions3 = $page->evaluate($jumpFunction);
$page->screenshot(['path' => 'result2.png']);
dump($dimensions3);
}else{
}
} catch (Node\Exception $exception) {
ptrace($e->getMessage().PHP_EOL.$e->getTraceAsString());
print($e->getMessage().PHP_EOL.$e->getTraceAsString());
}
$browser->close();
}
public function ocr_code($url, $length = 4, $debug =0){
if(false !== stripos($url, 'http')){
$img = file_get_contents($url);
}else{
$img = base64_decode($url);
}
// TODO md5 缓存识别结果
// halt($img);
$tmp = tempnam(sys_get_temp_dir(), 'code');
file_put_contents($tmp, $img);
// ptrace($tmp);
// return $tmp;
$ret = plugin_action('BaiduAi', 'Ocr', 'basicAccurate', [$img]);
ptrace($ret);
$words = $ret['words_result']['0']['words'];
$words = trim($words, ' ');
$words = str_ireplace(['.',' ', '\''], '', $words);
if($debug){
echo '<img src="'.base64EncodeImage($tmp).'">';
// dump($ret);
// dump($words);
}
return strlen($words) == $length? $words: '';
}
启了php服务之后同样要使用nginx服务器代理至该服务,操作同上,就是把nodejs服务替换成php服务。值得注意的是,puphpeteer只是一个桥接库,php通过这个桥接库操作puppeteer的API。puppeteer是基于nodejs环境的,所以要使用php的puphpeteer库,服务器环境还需要安装nodejs。
测试验证:
1.验证爬虫访问是否代理到“无头浏览器服务”方法:
postman访问域名时添加如下请求头:User-Agent:Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)
2.项目发布之后也可以去百度收录看一下爬取诊断:
项目未做任何处理之前的效果,请求回来的页面并没有经过服务端渲染(SSR),SEO体验极差
使用上述方案之后的效果,请求回来的是服务端渲染好的html页面
写在最后
上面分享的两个方案适合历史技术包袱较重,但是对SEO又有需求的项目,这两种方案是属于非侵入式改造,不该动原有项目代码,只在服务端做处理,从成本角度考量性价比是非常高的,只是多一点服务器性能开销,并无其他成本。如果新项目的话还是建议使用nuxt.js或者next.js这种全栈框架,毕竟这两个框架目前主流的完整的SSR解决方案,尽管槽点也很多。