在第一篇文章中,重点介绍了脚本需要搜集的数据,而本篇主要介绍的是服务器端如何处理客户端发送过来的请求和参数。

一、设备信息检测

  通过分析User-Agent请求首部(如下图红线框出的部分),可以得到相关的设备信息。

  Piwik系统专门有一套代码用来分析代理信息,还独立了出来,叫做DeviceDetector。它有一个专门的demo页面,可以展示其功能,点进去后可以看到下图中的内容。

  它能检测出浏览器名称、浏览器的渲染引擎、浏览器的版本、设备品牌(例如HTC、Apple、HP等)、设备型号(例如iPad、Nexus 5、Galaxy S5等)、设备类别(例如desktop、smartphone、tablet等),这6类数据中的可供选择的关键字,可以参考“List of segments”或插件的“readme”。顺便说一下,Piwik还能获取到访客的定位信息,在“List of segments”中,列举出了城市、经纬度等信息,其原理暂时还没研究。
  Piwik为大部分设备信息的关键字配备了一个icon图标,所有的icon图标被放置在“plugins\Morpheus\icons”中,包括浏览器、设备、国旗、操作系统等,下图截取的是浏览器中的部分图标。

二、IP地址

  在Piwik系统的后台设置中,可以选择IP地址的获取方式(如下图所示)。在官方博客的一篇《Geo Locate your visitors》博文中提到,3.5版本后可以在系统中嵌入MaxMind公司提供的IP地理定位服务(GeoIP2)。

  下面是一张看官方的产品介绍表,从描述中可看出这是一项非常厉害的服务。不过需要注意的是,这是一项付费服务。

三、日志数据和归档数据

  在官方发布的说明文档《How Matomo (formerly Piwik) Works》中提到,在Piwik中有两种数据类型:日志数据和归档数据。日志数据(Log Data)是一种原始分析数据,从客户端发送过来的参数就是日志数据,刚刚设备检测到的信息也是日志数据,还有其它的一些日志数据的来源,暂时还没细究。由于日志数据非常巨大,因此不能直接生成最终用户可看的报告,得使用归档数据来生成报告。归档数据(Archive Data)是以日志数据为基础而构建出来的,它是一种被缓存并且可用于生成报告的聚合分析数据。
  日志数据会通过“core\Piwik\Tracker\Visit.php”中的方法保存到数据库中,其中核心的方法如下所示,注释中也强调了该方法中的内容是处理请求的主要逻辑。该方法涉及到了很多对象,以及对象的方法,错综复杂,我自己也没有研究透,只是利用PHPStorm编辑器自动索引,查找出了一些关联,具体细节还有待考证。

/**
 *  Main algorithm to handle the visit.
 *
 *  Once we have the visitor information, we have to determine if the visit is a new or a known visit.
 *
 * 1) When the last action was done more than 30min ago,
 *      or if the visitor is new, then this is a new visit.
 *
 * 2) If the last action is less than 30min ago, then the same visit is going on.
 *    Because the visit goes on, we can get the time spent during the last action.
 *
 * NB:
 *  - In the case of a new visit, then the time spent
 *    during the last action of the previous visit is unknown.
 *
 *    - In the case of a new visit but with a known visitor,
 *    we can set the 'returning visitor' flag.
 *
 * In all the cases we set a cookie to the visitor with the new information.
 */
public function handle() {
    foreach ($this->requestProcessors as $processor) {
        Common::printDebug("Executing " . get_class($processor) . "::manipulateRequest()...");
        $processor->manipulateRequest($this->request);
    }
    $this->visitProperties = new VisitProperties();
    foreach ($this->requestProcessors as $processor) {
        Common::printDebug("Executing " . get_class($processor) . "::processRequestParams()...");
        $abort = $processor->processRequestParams($this->visitProperties, $this->request);
        if ($abort) {
            Common::printDebug("-> aborting due to processRequestParams method");
            return;
        }
    }
    $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit');
    if (!$isNewVisit) {
        $isNewVisit = $this->triggerPredicateHookOnDimensions($this->getAllVisitDimensions() , 'shouldForceNewVisit');
        $this->request->setMetadata('CoreHome', 'isNewVisit', $isNewVisit);
    }
    foreach ($this->requestProcessors as $processor) {
        Common::printDebug("Executing " . get_class($processor) . "::afterRequestProcessed()...");
        $abort = $processor->afterRequestProcessed($this->visitProperties, $this->request);
        if ($abort) {
            Common::printDebug("-> aborting due to afterRequestProcessed method");
            return;
        }
    }
    $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit');
    // Known visit when:
    // ( - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor
    //   OR
    //   - the visitor doesn't have the Piwik cookie but could be match using heuristics @see recognizeTheVisitor()
    // )
    // AND
    // - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit()
    if (!$isNewVisit) {
        try {
            $this->handleExistingVisit($this->request->getMetadata('Goals', 'visitIsConverted'));
        }
        catch(VisitorNotFoundInDb $e) {
            $this->request->setMetadata('CoreHome', 'visitorNotFoundInDb', true); // TODO: perhaps we should just abort here?
            
        }
    }
    // New visit when:
    // - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit()
    // - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor()
    // - the visitor does have the Piwik cookie but the idcookie and idvisit found in the cookie didn't match to any existing visit in the DB
    if ($isNewVisit) {
        $this->handleNewVisit($this->request->getMetadata('Goals', 'visitIsConverted'));
    }
    // update the cookie with the new visit information
    $this->request->setThirdPartyCookie($this->request->getVisitorIdForThirdPartyCookie());
    foreach ($this->requestProcessors as $processor) {
        Common::printDebug("Executing " . get_class($processor) . "::recordLogs()...");
        $processor->recordLogs($this->visitProperties, $this->request);
    }
    $this->markArchivedReportsAsInvalidIfArchiveAlreadyFinished();
}

   最后了解一下Piwik的数据库设计,此处只分析与日志数据和归档数据有关的数据表。官方的说明文档曾介绍,日志数据有5张相关的数据表,我对于表的内在含义还比较模糊,因此下面所列的描述还不是很清晰。
(1)log_visit:每次访问都会生成一条访问者记录,表中的字段可参考“Visits”。
(2)log_action:网站上的访问和操作类型(例如特定URL、网页标题),可分析出访问者感兴趣的页面,表中的字段可参考“Action Types”。

(3)log_link_visit_action:访问者在浏览期间执行的操作,表中的字段可参考“Visit Actions”。
(4)log_conversion:访问期间发生的转化(与目标相符的操作),表中的字段可参考“Conversions”。
(5)log_conversion_item:与电子商务相关的信息,表中的字段可参考“Ecommerce items”。
  归档数据的表有两种前缀,分别是“archive_numeric_”和“archive_blob_”,表的字段可参考“Archive data”。通过对字段的观察可知,两种最大的不同就是value字段的数据类型。archive_numeric_* 表中的value能储存数值(数据类型是Double),而archive_blob_* 表中的value能储存出数字以外的其他任何数据(数据类型是Blob)。
  两种表都是动态生成的,因此前缀的后面都用“*”表示。生成规则可按年、月、周、天或自定义日期范围,不设置的话,默认是按月计算,例如archive_numeric_2018_09、archive_blob_2018_09。

参考资料:
开源网站分析软件Piwik的数据库表结构
Piwik运转原理
How Matomo (formerly Piwik) Works
Database schema
数据分析技术白皮书
What data does Matomo track?
Segmentation in the API
device-detector
Device Detector demo page
Geo Locate your visitors

Last modification:February 10th, 2020 at 10:01 pm