- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
我一直在研究如何根据谷歌的 instructions 使 SPA 可被谷歌抓取。 .尽管有很多一般性解释,但我在任何地方都找不到带有实际示例的更详尽的分步教程。完成此操作后,我想分享我的解决方案,以便其他人也可以使用它并可能进一步改进它。
我正在使用 MVC
与 Webapi
Controller 和 Phantomjs在服务器端,和 Durandal在客户端使用 push-state
启用;我也用 Breezejs对于客户端-服务器数据交互,我强烈推荐所有这些,但我将尝试给出足够通用的解释,这也将有助于使用其他平台的人。
最佳答案
在开始之前,请确保您了解google requires ,尤其是使用漂亮和丑陋的 URL。现在让我们看看实现:
客户端
在客户端,您只有一个 html 页面,它通过 AJAX 调用与服务器动态交互。这就是SPA的意义所在。所有a
客户端中的标签是在我的应用程序中动态创建的,稍后我们将看到如何使这些链接对服务器中的谷歌机器人可见。每个这样的a
标签需要能够有 pretty URL
在 href
标记,以便谷歌的机器人抓取它。你不想要 href
当客户端点击它时使用的部分(即使您确实希望服务器能够解析它,我们稍后会看到),因为我们可能不希望加载新页面,只是为了进行 AJAX 调用让一些数据显示在页面的一部分并通过javascript更改URL(例如使用HTML5 pushstate
或使用 Durandaljs
)。所以,我们有一个 href
google 和 onclick
上的属性当用户点击链接时,它会完成这项工作。现在,因为我使用 push-state
我不要任何 #
在 URL 上,所以一个典型的 a
标签可能如下所示:<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
'category' 和 'subCategory' 可能是其他短语,例如电器商店的“communications”和“phones”或“computers”和“laptops”。显然会有许多不同的类别和子类别。如您所见,该链接直接指向类别、子类别和产品,而不是作为特定“商店”页面(例如 http://www.xyz.com/store/category/subCategory/product111
)的额外参数。 .这是因为我更喜欢更短更简单的链接。这意味着我不会有与我的“页面”之一同名的类别,即“关于”。
如何通过AJAX加载数据(onclick
部分)我就不讲了,在google上搜索,有很多很好的解释。这里我要提到的唯一重要的事情是,当用户单击此链接时,我希望浏览器中的 URL 如下所示:http://www.xyz.com/category/subCategory/product111
.而且这个 URL 不会发送到服务器!请记住,这是一个 SPA,客户端和服务器之间的所有交互都是通过 AJAX 完成的,根本没有链接!所有“页面”都在客户端实现,并且不同的 URL 不会调用服务器(服务器确实需要知道如何处理这些 URL,以防它们被用作从另一个站点到您站点的外部链接,我们稍后会在服务器端部分看到这一点)。现在,这被 Durandal 巧妙地处理了。我强烈推荐它,但如果你更喜欢其他技术,你也可以跳过这部分。如果您选择它,并且您也像我一样使用 MS Visual Studio Express 2012 for Web,则可以安装 Durandal Starter Kit ,然后在 shell.js
中,使用这样的东西:
define(['plugins/router', 'durandal/app'], function (router, app) {
return {
router: router,
activate: function () {
router.map([
{ route: '', title: 'Store', moduleId: 'viewmodels/store', nav: true },
{ route: 'about', moduleId: 'viewmodels/about', nav: true }
])
.buildNavigationModel()
.mapUnknownRoutes(function (instruction) {
instruction.config.moduleId = 'viewmodels/store';
instruction.fragment = instruction.fragment.replace("!/", ""); // for pretty-URLs, '#' already removed because of push-state, only ! remains
return instruction;
});
return router.activate({ pushState: true });
}
};
});
route:''
)用于其中没有额外数据的 URL,即 http://www.xyz.com
.在此页面中,您使用 AJAX 加载一般数据。可能实际上没有 a
此页面中的所有标签。您需要添加以下标签,以便 google 的机器人知道如何处理它:<meta name="fragment" content="!">
.此标签将使谷歌的机器人将 URL 转换为 www.xyz.com?_escaped_fragment_=
我们稍后会看到。 mapUnknownRoutes
进来了。它将这些未知路由映射到“商店”路由并删除任何“!”来自 URL,以防它是 pretty URL
由谷歌搜索引擎生成。 'store' 路由获取 'fragment' 属性中的信息,并进行 AJAX 调用以获取数据、显示数据并在本地更改 URL。在我的应用程序中,我不会为每个此类调用加载不同的页面;我只更改与此数据相关的页面部分,并在本地更改 URL。 pushState:true
它指示 Durandal 使用推送状态 URL。 pushState:true
即可)。更复杂的部分(至少对我来说......)是服务器部分:
MVC 4.5
在服务器端使用
WebAPI
Controller 。服务器实际上需要处理 3 种类型的 URL:由 google 生成的 URL - 两者都是
pretty
和
ugly
以及与客户端浏览器中显示的格式相同的“简单”URL。让我们看看如何做到这一点:
http://www.xyz.com/category/subCategory/product111
的内容并查找名为“类别”的 Controller 。所以在
web.config
我添加以下行以将它们重定向到特定的错误处理 Controller :
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
.我希望将 URL 发送到将通过 AJAX 加载数据的客户端,所以这里的技巧是调用默认的“索引” Controller ,就像不引用任何 Controller 一样;我通过在所有 'category' 和 'subCategory' 参数之前向 URL 添加一个散列来做到这一点;除了默认的“索引” Controller 之外,散列的 URL 不需要任何特殊的 Controller ,数据被发送到客户端,然后删除散列并使用散列后的信息通过 AJAX 加载数据。这是错误处理程序 Controller 代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Routing;
namespace eShop.Controllers
{
public class ErrorController : ApiController
{
[HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
public HttpResponseMessage Handle404()
{
string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
var response = Request.CreateResponse(HttpStatusCode.Redirect);
response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
return response;
}
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace eShop.App_Start
{
public class AjaxCrawlableAttribute : ActionFilterAttribute
{
private const string Fragment = "_escaped_fragment_";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.RequestContext.HttpContext.Request;
if (request.QueryString[Fragment] != null)
{
var url = request.Url.ToString().Replace("?_escaped_fragment_=", "#");
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
}
return;
}
}
}
using System.Web.Mvc;
using eShop.App_Start;
namespace eShop
{
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new AjaxCrawlableAttribute());
}
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace eShop.Controllers
{
public class HtmlSnapshotController : Controller
{
public ActionResult returnHTML(string url)
{
string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
var startInfo = new ProcessStartInfo
{
Arguments = String.Format("{0} {1}", Path.Combine(appRoot, "seo\\createSnapshot.js"), url),
FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
StandardOutputEncoding = System.Text.Encoding.UTF8
};
var p = new Process();
p.StartInfo = startInfo;
p.Start();
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
ViewData["result"] = output;
return View();
}
}
}
view
很简单,就一行代码:
@Html.Raw( ViewBag.result )
正如你在 Controller 中看到的,phantom 加载了一个名为
createSnapshot.js
的 javascript 文件。在我创建的名为
seo
的文件夹下.这是这个 javascript 文件:
var page = require('webpage').create();
var system = require('system');
var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();
page.onResourceReceived = function (response) {
if (requestIds.indexOf(response.id) !== -1) {
lastReceived = new Date().getTime();
responseCount++;
requestIds[requestIds.indexOf(response.id)] = null;
}
};
page.onResourceRequested = function (request) {
if (requestIds.indexOf(request.id) === -1) {
requestIds.push(request.id);
requestCount++;
}
};
function checkLoaded() {
return page.evaluate(function () {
return document.all["compositionComplete"];
}) != null;
}
// Open the page
page.open(system.args[1], function () { });
var checkComplete = function () {
// We don't allow it to take longer than 5 seconds but
// don't return until all requests are finished
if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
clearInterval(checkCompleteInterval);
var result = page.content;
//result = result.substring(0, 10000);
console.log(result);
//console.log(results);
phantom.exit();
}
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);
checkLoaded()
函数返回真。为什么?这是因为我的特定 SPA 进行了几次 AJAX 调用以获取所有数据并将其放置在我页面上的 DOM 中,并且在将 DOM 的 HTML 反射返回给我之前,幻影无法知道所有调用何时完成。我在这里所做的是在最后一次 AJAX 调用之后我添加了一个
<span id='compositionComplete'></span>
,这样如果这个标签存在,我就知道 DOM 已经完成。我这样做是为了回应 Durandal 的
compositionComplete
事件,见
here更多。如果这在 10 秒内没有发生,我放弃(最多只需要一秒钟)。返回的 HTML 包含用户在浏览器中看到的所有链接。该脚本将无法正常工作,因为
<script>
HTML 快照中确实存在的标签没有引用正确的 URL。这也可以在 javascript 幻像文件中更改,但我认为这不是必要的,因为 HTML snapshort 仅由 google 用于获取
a
链接而不是运行 javascript;这些链接确实引用了一个漂亮的 URL,事实上,如果您尝试在浏览器中查看 HTML 快照,您将收到 javascript 错误,但所有链接都可以正常工作,并且这次使用漂亮的 URL 再次将您引导至服务器获得完全工作的页面。
http://www.xyz.com/store/category/subCategory/product111
.这将避免我的解决方案中的所有无效 URL 都被视为实际上是对“索引” Controller 的调用的问题,并且我认为这些可以在“存储” Controller 中处理,而无需添加
web.config
我上面显示了。
关于ajax - 如何使 SPA SEO 可抓取?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18530258/
如本answer所述,如果浏览器不支持 e,可以设置后备游标。 G。 光标:抓取;。我现在的问题是获取这些图像。在我的驱动器上本地搜索“.cur”只给了我系统光标,其中 grab.cur 和 grab
以下代码在计算机上运行以从 Instagram 帐户中抓取数据。当我尝试在 VPS 服务器上使用它时,我被重定向到 Instagram 登录页面,因此脚本不起作用。 为什么当我在电脑上或服务器上时,I
我在使用 Ruby 和 Mechanize 将 POST 查询传递到站点的网站上。访问站点的查询基于 firebug,如下所示 param.PrdNo=-1¶m.Type=Prop¶m
我正在尝试抓取一个具有多个页面结果的网站,例如“1、2、3、4、5...”。 每个分页号都是到另一个页面的链接,我需要抓取每个页面。 到目前为止,我想出了这个: while lien = page.l
我正在使用 HtmlAgilityPack 在 C# Asp.Net 中执行 Scraping,到目前为止,我在从多个 Web 执行 Scratch 时没有遇到问题,但是,尝试弹出以下代码时出现错误
如果我有一个 css 文件做这样的事情 #foo:after{content:"bar;} ,有没有办法用 javascript 获取 :after 的内容?获取父元素的内容只返回 #foo 元素的内
问题是这样的: 我有一个 Web 应用程序 - 一个经常更改的通知系统 - 在一系列本地计算机上运行。该应用程序每隔几秒刷新一次以显示新信息。计算机仅显示信息,没有键盘或任何输入设备。 问题是,如果与
我想制作一个程序来模拟用户浏览网站和点击链接。必须启用 Cookie 和 javascript。我已经在 python 中成功地做到了这一点,但我想把它写成一种可编译的语言(python ide 不会
我制作了这个小机器人,它通过搜索参数列表进行处理。它工作正常,直到页面上有几个结果: product_prices_euros 给出了一半为空的项目列表。因此,当我与 product_prices_c
我需要找到一个单词的匹配项,例如: 在网上找到所有单词“学习”https://www.georgetown.edu/(结果:4个字)(您可以看到它按CTRL + F并搜索) 我有我的 Python 代
有一个站点\资源提供一些一般统计信息以及搜索工具的界面。这种搜索操作成本高昂,因此我想限制频繁且连续(即自动)的搜索请求(来自人,而不是来自搜索引擎)。 我相信有很多现有的技术和框架可以执行一些情报抓
这并不是真正的抓取,我只是想在网页中找到类具有特定值的 URL。例如: 我想获取 href 值。关于如何做到这一点的任何想法?也许正则表达式?你能发布一些示例代码吗?我猜 html 抓取库,比如 B
我正在使用 scrapy。 我正在使用的网站具有无限滚动功能。 该网站有很多帖子,但我只抓取了 13 个。 如何抓取剩余的帖子? 这是我的代码: class exampleSpider(scrapy.
我正在尝试从这个 website 中抓取图像和新闻 url .我定义的标签是 root_tag=["div", {"class":"ngp_col ngp_col-bottom-gutter-2 ng
关闭。这个问题需要更多focused .它目前不接受答案。 想改进这个问题吗? 更新问题,使其只关注一个问题 editing this post . 关闭上个月。 Improve this ques
我在几个文件夹中有数千个 html 文件,我想从评论中提取数据并将其放入 csv 文件中。这将允许我为项目格式化和清理它。例如,我在这个文件夹中有 640 个 html 文件: D:\My Web S
我在编写用于抓取网页的实用程序时遇到了一个问题。 我正在发送 POST 请求来检索数据,我模仿我正在抓取的网络行为(根据使用 fiddler 收集的信息)。 我已经能够自动替换我的 POST 中除 V
对于 Googlebot 的 AJAX 抓取,我在我的网站中使用“_escaped_fragment_”参数。 现在我查看了 Yandex 对我网站的搜索结果。 我看到搜索结果中不存在 AJAX 响应
我正在尝试抓取网站的所有结果页面,它可以工作,但有时脚本会停止并显示此错误: 502 => Net::HTTPBadGateway for https://website.com/id/12/ --
我是一个学习网络爬虫的初学者,由于某种原因我无法爬网this地点。当我在 Chrome 中检查它时,代码看起来不错,但是当我用 BeautifulSoup 阅读它时,它不再是可刮的。汤提到“谷歌分析”
我是一名优秀的程序员,十分优秀!