如何构建一个简单的网站分析服务(如 Google Analytics)






4.52/5 (7投票s)
网站分析服务(例如 Google Analytics)背后的原理以及如何构建一个。
大家好!
这是我为 CodeProject 撰写的第二篇文章,它是对第一篇关于具有自动刷新功能的实时访客计数器开发的自然延续。这次,我想解释一下像 Google Analytics 这样的分析服务是如何工作的,以便提供关于网站上发生的事情的实时信息。此外,我将解释如何构建一个。(极其简化版)
正如我之前所说,我的英语非常非常差,但我希望您能帮助我改进文本质量。一如既往,我使用 .NET 和 VB 来加快开发时间,但本文中描述的所有方法都可以用任何开发语言实现。
如果您使用 Google Analytics,您可以看到一些惊人的功能:活跃访客数量、当前正在浏览的页面、用户行为等等。(所有这些都是实时的!)
但是有人可能会问:这是如何工作的?它是如何实现的?我们来试着回答这些问题。
首先,我们可以观察到许多(Google 并非唯一)分析服务需要在您所有页面中添加 Javascript 代码。本质上,为了正常工作,以下 HTML 模板将被实现在您所有页面中:
<html>
<head>
<script language="javascript" src="SERVICE_URL/file.js"></script>
</head>
... page contents ...
<body>
</body>
</html>
当您在页眉中添加 <script> 标签时,您的浏览器将下载第三方代码来执行。该代码的作用将在下一节中报告。
您可能会观察到,要下载一个文件,您的浏览器必须向服务器创建一个 Http 请求:在服务器端,可以了解有关请求来源的许多信息:例如,服务器可以访问您的 IP 地址、您的远程主机名和您的浏览器名称;仅此一点,就可以提供足够的信息来构建一个小型跟踪系统。
分析服务器的简单示意图如下所示。
正如您所见,对于您浏览器中加载的每个页面,都会从分析服务器下载 file.js 的副本。这意味着您允许服务器为您做一些事情(无害,javascript 在沙箱中运行)。
那么,第二个问题可能是:“服务器从我的浏览器读取哪些信息?它是如何读取的?因此,我的浏览器如何将信息发送到服务器”。
下图回答了这些问题。

首先,为了唯一地跟踪用户,会生成一个 UUID:该 UUID 存储在 Cookie 中,并将用作用户标识符(因为 IP 地址在同一用户两次连接之间可能会发生变化,如果用户使用 NAT 或其他协议);随后,当前位置(URL)、浏览器名称、操作系统名称和版本以及其他信息可以被 file.js 中的代码读取并发送到服务器。
每个“track”数据包的格式为 [UUID],{User_DATA},服务器只需要管理一组 [UUID,User_DATA] 即可提供分析功能:该集合是一个以 UUID 作为键,以 ArrayList 作为值的哈希表。
因此,我们的下一个目标是构建一个提供以下功能的原型(这些功能在 Google Analytics 中也可用):
- 活跃访客数量
- 在线访客数量
- 在线访客当前浏览的页面
- 每个在线访客的页面历史记录
下图显示了一个服务器端可访问的“控制台”示例,该控制台显示当前的网站状态。(我每天都在我的几个电子商务网站上使用我自制的分析服务。显然,图片已更改以隐藏用户 IP。)
背景
为了本文的目的,您需要了解我们对 UUID、Hashtable (在 .net 中称为 Dictionary)和 Arraylist 的含义。我们只需要一个 Microsoft Web Developer Express 的副本(可以从 Microsoft 网站免费下载)。
由于我们使用 Microsoft IIS,因此我们利用存在于应用程序池中直到未被回收的 Application 对象:这个简单的实现不将数据保存在 DBMS 中,因此每次 IIS 重启所有数据都将丢失;但是,如果您愿意,可以利用 App_End 和 App_Start 事件来在 DBMS 和应用程序内存之间保存和恢复数据。
使用代码
现在我们来分析如何构建项目以跟踪网站上的用户活动:在我之前的文章中,我谈到了一个简单的哈希表,用于管理访客,以构建一个实时计数器。现在我想扩展之前的代码以处理我们想要的其他信息。
客户端
首先,我们需要在 file.js 中实现逻辑,或者更确切地说,创建 UUID、读取信息并将信息发送到服务器。特别是我们需要:
- 读取 Cookie 并设置 Cookie (getCookie 和 setCookie 函数)
- 创建 Ajax 异步调用(getXmlReq 函数)
- 生成 UUID(file.js 的主体)
- 读取浏览器位置 (__as__ping 函数)
// Address of track server. This address is communicated by server when browser download this file.
var NETSELL_STAT = 'https://:82';
function getCookie(c_name, remote) {
// get normal cookies
if (document.cookie.length > 0) {
c_start = document.cookie.indexOf(c_name + "=");
if (c_start != -1) {
c_start = c_start + c_name.length + 1;
c_end = document.cookie.indexOf(";", c_start);
if (c_end == -1) c_end = document.cookie.length;
return unescape(document.cookie.substring(c_start, c_end));
}
}
return "";
}
function setCookie(c_name, value, expiredays, remote) {
var cookiebody;
var exdate = new Date();
exdate.setSeconds(exdate.getSeconds() + expiredays);
//exdate.setDate(exdate.getDate() + expiredays);
cookiebody = c_name + "=" + escape(value) +
((expiredays == null) ? "" : ";expires=" + exdate.toUTCString());
if (remote != null) {
// remote cookie// send cookies to LogonServ
}
else // normal cookie
document.cookie = cookiebody;
}
function getXMLReq() {
var xmlhttp;
if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp = new XMLHttpRequest();
}
else {// code for IE6, IE5
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
return xmlhttp;
}
// Check for UUID of this user (if not exist create one)
var uuid = getCookie("site_uuid");
if (uuid == "") {
var d = new Date();
var rnd = Math.floor((Math.random() * 100000) + 1);
uuid = d.getDay() + '_' + d.getMonth() + '_' + d.getYear() + '_' + rnd + '_' + d.getSeconds() + '_' + d.getMilliseconds() + '_' + d.getMinutes() + '_' + d.getHours();
setCookie("site_uuid", uuid);
}
// send uuid to server (the ping)
function __as_ping() {
var ping = getXMLReq();
ping.open("GET", NETSELL_STAT + "/srv/serverside.aspx?TYPE=PING&UUID=" + uuid + '&L=' + location.href.toString().replace('&', '::'), true);
ping.send();
}
__as_ping();
当所有数据都已读取并发送后,客户端无需执行任何操作。
服务器端
另一方面,服务器必须管理有关用户的所有信息。之前,我曾谈到过哈希表(Dictionary),您可以在下面看到一个简单的实现。
首先,我们需要初始化要维护数据的内存空间。
在 global.asax 文件中,我们编写:
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' When application start
Application.Add("LastReset", Date.Now)
' We make sure that 'memory' is available
SyncLock Application
Dim ActiveUser As Dictionary(Of String, decorablePosition)
ActiveUser = CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))
If IsNothing(ActiveUser) Then
ActiveUser = New Dictionary(Of String, decorablePosition)
Application.Add("ActiveUser", ActiveUser)
End If
Application.Add("ActiveUser", ActiveUser)
End SyncLock
End Sub
随后,我们只需要存储来自客户端的“track”数据包。我们可以创建一个 aspx 页面(file.js 发送数据的页面),名为 serverside.aspx,内容如下:
<%@ Page Language="VB" AutoEventWireup="false" CodeFile="serverside.aspx.vb" Inherits="srv_serverside" %>
<%@ Import Namespace="System.collections.generic" %>
<%@Import Namespace="DIBIASI.CALCE_Min.ABSTRACT.TDA.UTILS" %>
<%
' on PING receive we check if UUID is known
' then save last action date and time (and location, and ip)
If Request("TYPE") = "PING" Then
Dim UUID As String = Request("UUID")
SyncLock CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))
If Not CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition)).ContainsKey(UUID) Then
CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition)).Add(UUID, New decorablePosition)
CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))(UUID).setValueOf("LOCATION_STORY", New ArrayList)
End If
CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))(UUID).setValueOf("DATE", Date.Now)
CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))(UUID).setValueOf("LOCATION", Request("L"))
CType(CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))(UUID).getValueOf("LOCATION_STORY"), ArrayList).Add(Date.Now & "|" & Request("L"))
CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))(UUID).setValueOf("IPADDR", Request.UserHostAddress)
End SyncLock
End If
%>
最后,我们只需要使用存储的数据,例如,按如下方式进行。
首先,我们需要计算总用户数,即字典中的条目数,因为每个用户都有一个 UUID。其次,我们想计算在线用户,我们可以遍历字典的所有条目,只计算 last-action-date 小于 240 秒的条目。
活跃用户字段可以通过相同的方式确定(last-action 小于 60 秒)。最后,我们可以通过读取“LOCATION”字段来访问用户正在浏览的当前页面
您可以在下面阅读一个使用存储数据的页面的示例。
<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="stats.aspx.vb" Inherits="Analysis.stats" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<div style="font-family:Tahoma;background-color:#f6f6f6;float:left;border:1px solid #e6e6e6;width:20%;height:180px;text-align:center;vertical-align:middle;">
<%
Dim ConnectedUser As Integer = 0
Dim actu As Integer = 0
Dim visitorFromLastReset As Integer = 0
Dim visitorToday As Integer = 0
Dim ActiveKart As Integer = 0
dim euroinKart as double=0
For Each it As KeyValuePair(Of String, decorablePosition) In CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))
'# count visit from last reset
visitorFromLastReset += 1
'# count visit today
If Format(CDate(it.Value.getValueOf("DATE")), "yyyyMMdd") = Format(Date.Now, "yyyyMMdd") Then
visitorToday += 1
End If
'# count connected users
If Math.Abs(DateDiff(DateInterval.Second, CDate(it.Value.getValueOf("DATE")), Date.Now)) <= 240 Then
ConnectedUser += 1
'# count active users
If Math.Abs(DateDiff(DateInterval.Second, CDate(it.Value.getValueOf("DATE")), Date.Now)) <= 60 Then
actu += 1
End If
End If
Next it
%>
<table width="100%">
<tr>
<td><%=Format(Application("LastReset"),"dd/MM/yy HHHH.mm") %></td>
<td>Today Visitors</td>
</tr>
<tr>
<td><span style="font-size:1.3em;"><%=visitorFromLastReset%></span></td>
<td><span style="font-size:1.3em;color:Blue;"><%=visitorToday%></span></td>
</tr>
</table>
<table width="100%">
<tr>
<td>Connected Now</td>
<td></td>
</tr>
<tr>
<td><span style="font-size:1.3em;"><%=ConnectedUser%></span></td>
<td></td>
</tr>
</table>
Active Now
<br />
<span style="font-size:2em;color:blue;"><%=actu%></span>
</div>
<!-- show active page for each user -->
<br />
<div style="font-family:tahoma;font-size:0.8em;display:block;float:left;border:1px solid #e6e6e6;width:99%;height:200px;overflow:auto;text-align:center;vertical-align:middle;">
<table border="0" cellspacing="0" cellpadding="0">
<%
Dim foreColor As String = "#000"
Dim LOCATION As String = ""
Dim RASCL As String = ""
For Each it As KeyValuePair(Of String, decorablePosition) In CType(Application("ActiveUser"), Dictionary(Of String, decorablePosition))
If Math.Abs(DateDiff(DateInterval.Second, CDate(it.Value.getValueOf("DATE")), Date.Now)) <= 240 Then
foreColor="#000"
If Math.Abs(DateDiff(DateInterval.Second, CDate(it.Value.getValueOf("DATE")), Date.Now)) <= 60 Then
foreColor = "#33CC33"
End If
LOCATION = it.Value.getValueOf("LOCATION").ToString.Split("/")(it.Value.getValueOf("LOCATION").ToString.Split("/").Length - 1)
RASCL = " <strong>" & mid(it.Value.getValueOf("RASCL"),1,20) & "</strong>"
%>
<tr style="color:<%=foreColor%>">
<td style="width:35%;padding:1px;" align="left"><span><a href="followUserAction.aspx?IPADDR=<%=it.Value.getValueOf("IPADDR") %>" target="_blank"><%=it.Value.getValueOf("IPADDR") %> <%=RASCL %></a></span></td>
<td align="left"><span><%=LOCATION%></span></td>
</tr>
<%
End If
Next it
%>
</table>
</div>
</body>
</html>
在本文附带的 zip 文件中,您可以找到一个可以在 Web Developer Express 中运行的完整原型。(请记住在端口 82 上开始调试或更改 file.js 中的路径)
历史
14/01/2014:草稿发布
16/01/2014:首次发布
16/01/2014:由 Bruno Interlandi 完成英文修订
17/01/2014:重新上传 zip 文件