前段时间,Google Analytics 给我发了封邮件,说二月份我的博客有 9.3K 的 UV。点进去看了看热门的文章,访问量最多的是「Twitter 账号被锁定是种怎样的体验」(17 年的博文了……),其他比较高的还有以前写的教程「Windows Terminal 安装与配置指南」「WSL 配置指北:打造 Windows 最强命令行」,以及后起之秀「记一次 HNS 币空投薅 $500 羊毛的经历」,很真实。

看着报表上的访问量排名,我突然一寻思,这白花花的数据都在眼前,那能不能利用 GA 里的这些数据,给博客加个访问计数、阅读量统计功能呢?

当然可以。

我 Google 了一下,事实上早就已经有人这么干过了:

既然 GA 有官方提供的 API,那事情就好办了。

TL;DR

先说成果。我写了两个版本的程序,分别使用 Node.js 和 PHP 实现,都可以通过页面 URI 查询 PV 访问量,提供的 API 也是一样的。源码放在 GitHub 上:

部署之后,前端通过其提供的 API 获取 PV,具体效果可以参考本博客。

程序的具体使用方法这里就不多说了,README 上都有写。下面具体讲讲怎么使用 Google Analytics API 以及可能遇到的坑,希望对各位有所帮助。

启用 API 与获取凭据

首先根据官方的教程启用 Analytics Reporting API v4。

登录 Google 账号后打开 setup tool

google-developer-console-enable-api

点击创建项目后(也可以使用现有项目),会自动跳转至凭据页面。

页面会引导你回答一些问题以确定要创建的凭据的类型(API 密钥、OAuth 或者是服务账号),在我们这个场景下(服务器端访问 Google API 而非用户端访问)应该使用服务账号类型的凭据。

google-api-add-credential

角色选择 Service Account User,密钥类型选择 JSON,点继续后浏览器会下载一个 JSON 文件,里面就是服务账号的密钥了。记得妥善保存这个密钥文件,下面的操作都要用到它。

安装相应语言的 API 库

Google 提供了很多编程语言的 API 支持库,封装简化了认证与请求的流程。具体支持的语言可以查看官方文档:Client Libraries | Analytics Reporting API v4

Node.js

npm install googleapis

Python

pip install --upgrade google-api-python-client

PHP

composer require google/apiclient

通过 API 查询 PV 数据

下面以 Node.js 为例进行说明(也有官方样例)。

使用凭据创建认证 client:

// 省略了外层 async 和其他 import
import { google } from 'googleapis'

// 如果使用了服务账号以外的认证方式这里也要改
const auth = new google.auth.GoogleAuth({
  // 上面下载的密钥文件。除了直接给文件路径,也可以把
  // 文件里的内容通过 credentials, projectId 等参数传进来
  keyFile: path.join(__dirname, 'key.json'),
  // API 作用域,我们只需要访问 Analytics
  scopes: 'https://www.googleapis.com/auth/analytics.readonly',
})

const client = await auth.getClient()
const analyticsreporting = google.analyticsreporting({
  version: 'v4',
  auth: client,
});

构建请求查询 API:

const res = await analyticsreporting.reports.batchGet({
  requestBody: {
    reportRequests: [{
      // 数据视图 ID,可以在 GA 的「管理 > 数据视图设置」内查看
      viewId: '213856884',
      dateRanges: [{
        // 查询起始日期,设置一个足够久远的时间以统计所有数据
        startDate: '2010-01-01',
        // 查询结束日期
        endDate: 'today',
      }],
      // Metric,官方翻译是指标,可以理解为要获取的数据字段
      // 可以指定多个 metric,比如 'ga:avgTimeOnPage'
      metrics: [{
        expression: 'ga:pageviews'
      }],
      // Dimension,官方翻译是维度,我们用这个来过滤数据
      // 这里使用页面路径,也可以通过 'ga:pageTitle' 按标题过滤
      dimensions: [{
        name: 'ga:pagePath'
      }],
      // 维度过滤器,只返回页面 URI 以所给字符串开头的数据
      dimensionFilterClauses: [{
        filters: [{
          'dimensionName': 'ga:pagePath',
          'operator': 'BEGINS_WITH',
          'expressions': ['/foo'],
        }, {
          'dimensionName': 'ga:pagePath',
          'operator': 'BEGINS_WITH',
          'expressions': ['/bar'],
        }]
      }],
      // 按照页面浏览量降序排序
      orderBys: [{
        fieldName: 'ga:pageviews',
        sortOrder: 'DESCENDING'
      }],
    }]
  }
})

其中 Metric 和 Dimension 的概念可能有点不好理解,可以参考官方 API 文档:

以查询我博客上最新两篇文章的 PV 为例,API 响应是这样的:

{
  "reports": [
    {
      "columnHeader": {
        "dimensions": [
          "ga:pagePath"
        ],
        "metricHeader": {
          "metricHeaderEntries": [{
            "name": "ga:pageviews",
            "type": "INTEGER"
          }]
        }
      },
      "data": {
        "rows": [
          {
            "dimensions": [
              "/short-domain-name-for-blog/"
            ],
            "metrics": [{
              "values": [
                "135"
              ]
            }]
          },
          {
            "dimensions": [
              "/reverse-proxy-for-126-mail-pop-server/"
            ],
            "metrics": [{
              "values": [
                "76"
              ]
            }]
          }
        ],
        "totals": [{ "values": ["211"] }],
        "rowCount": 2,
        "minimums": [{ "values": ["76"] }],
        "maximums": [{ "values": ["135"] }]
      }
    }
  ]
}

不得不说很复杂、很强大,更高级的用法有待各位挖掘。

踩坑:Serverless 部署的可行性

其实最开始,我是没打算部署到自己的服务器上的,想着要是能直接用 Serverless 解决(白嫖免费版)最好。然而折腾了一天,在 CloudFlare WorkersZEIT Now 上的尝试均以失败告终。

CloudFlare Workers 上遇到的问题是 CPU 执行时间超时(script exceeded time limit)。我排查了很长时间,甚至怀疑是 Google 的 Node.js 库没有用 fetch API 的问题,还把 Analytics API 请求的部分用原生 fetch 重写了一遍。结果最后发现是因为 Google 服务账号的鉴权用的是 JWT,HS256 的签名算法。你妈的,难怪啊!

如果在 API 请求中手动指定 HTTP 请求头 Authorization: Bearer <access token> 认证的话屁事没有一切正常,但要签发 token 的话就不行了,sad。

不过我还不死心,又去 ZEIT Now 平台写了个 lambda 试了一下。这次运行是可以正常运行了,我却发现了一个挺要命的问题:缓存。在我们这个场景下,缓存肯定是必要的。API Client 认证获得了 access token 要缓存,从 API 那查询拿到的数据也要缓存,不然总不能每次请求都重新签发 token、查询数据吧。

然而我翻遍了文档,也没找到适用于这些 Serveless 平台的缓存解决方案(内存缓存?生命周期不允许;文件缓存?不允许读写文件;NoSQL?我靠,那还得去别的地方弄个远程服务来),或许是应用场景不合适吧。

无奈之下,只好部署到自己的乞丐版 VPS 上了。白嫖计划泡汤,遗憾。

写在后面

我捣鼓博客这几年以来,在阅读量统计上也花过一些心思:

直接使用 GA 的数据作为阅读量展示,有其优势也有其局限性。优势是不用自己维护统计的后端了,一个 GA 统计代码就可以通吃,数据也比较一致。

不足之处我目前想到就是数据更新不及时(GA 本身的延迟和缓存),以及统计代码容易被浏览器屏蔽吧(可以使用 SukkaW/cloudflare-workers-async-google-analytics 这类异步 GA 解决方案)。不过我的博客浏览量统计对时效性和准确度并不敏感,所以目前看来,直接使用 GA 的数据对我来说应该是最好的方案了。

另外这也是我第一次在实际项目中使用 TypeScript,体验确实很不错,类型提示和自动补全不要太爽。不过就这么一个小 API 常驻一个 Node 服务有点太占资源了,顶不住,所以我又拿 Lumen 写了个 PHP 的版本。

两边实现的 API 都是一样的,各位有需要的话可以自行选择。