airflow源码分析之BashOperator

BashOperator主要的功能是执行shell命令或者shell脚本。负责具体的执行过程的是BashOperator.execute()函数。
airflow的bash_operator.py文件:

from builtins import bytes
import os
import signal
from subprocess import Popen, STDOUT, PIPE
from tempfile import gettempdir, NamedTemporaryFile

from airflow.exceptions import AirflowException
from airflow.models import BaseOperator
from airflow.utils.decorators import apply_defaults
from airflow.utils.file import TemporaryDirectory


class BashOperator(BaseOperator):
    """
    :param xcom_push: If xcom_push is True, the last line written to stdout
        will also be pushed to an XCom when the bash command completes.
    :type xcom_push: bool
    :param env: If env is not None, it must be a mapping that defines the
        environment variables for the new process; these are used instead
        of inheriting the current process environment, which is the default
        behavior. (templated)
    :type env: dict
    :type output_encoding: output encoding of bash command
    """
    template_fields = ('bash_command', 'env')
    template_ext = ('.sh', '.bash',)
    ui_color = '#f0ede4'

    @apply_defaults    # 处理默认的参数
    def __init__(
            self,
            bash_command, # string 可以是单独的命令,或者是命令集,或者是.sh文件
            xcom_push=False,  # 如果两个operator有依赖关系时,值为True
            env=None,
            output_encoding='utf-8',   
            *args, **kwargs):

        super(BashOperator, self).__init__(*args, **kwargs)
        self.bash_command = bash_command
        self.env = env
        self.xcom_push_flag = xcom_push
        self.output_encoding = output_encoding

    def execute(self, context):
        """
        Execute the bash command in a temporary directory
        which will be cleaned afterwards
        """
        bash_command = self.bash_command
        self.log.info("Tmp dir root location: \n %s", gettempdir()) # 基类继承了处理log的mixin类
        with TemporaryDirectory(prefix='airflowtmp') as tmp_dir:
            with NamedTemporaryFile(dir=tmp_dir, prefix=self.task_id) as f:

                f.write(bytes(bash_command, 'utf_8'))
                f.flush()   # 将缓冲区的数据写入到磁盘中
                fname = f.name
                script_location = tmp_dir + "/" + fname
                self.log.info(
                    "Temporary script location: %s",
                    script_location
                )
                def pre_exec():
                    # Restore default signal disposition and invoke setsid
                    for sig in ('SIGPIPE', 'SIGXFZ', 'SIGXFSZ'):
                        if hasattr(signal, sig):
                            signal.signal(getattr(signal, sig), signal.SIG_DFL)
                    os.setsid()
                self.log.info("Running command: %s", bash_command)
                sp = Popen(
                    ['bash', fname],
                    stdout=PIPE, stderr=STDOUT,
                    cwd=tmp_dir, env=self.env,
                    preexec_fn=pre_exec)

                self.sp = sp

                self.log.info("Output:")
                line = ''
                for line in iter(sp.stdout.readline, b''):
                    line = line.decode(self.output_encoding).strip()
                    self.log.info(line)
                sp.wait()
                self.log.info(
                    "Command exited with return code %s",
                    sp.returncode
                )

                if sp.returncode:
                    raise AirflowException("Bash command failed")

        if self.xcom_push_flag:
            return line

    def on_kill(self):
        self.log.info('Sending SIGTERM signal to bash process group')
        os.killpg(os.getpgid(self.sp.pid), signal.SIGTERM)

TemporaryDirectory:创建一个临时的目录,它使用了@contextmanager,生成了一个上下文管理器,因此它能用在with环境里。用contextmanager装饰的函数,要返回一个生成器,并且只能返回一个值

@contextmanager  # 生成一个上下文管理器
def TemporaryDirectory(suffix='', prefix=None, dir=None):
    name = mkdtemp(suffix=suffix, prefix=prefix, dir=dir)  # suffix:后缀  prefix: 前缀
    try:
        yield name    # yield 生成器 仅返回一个值
    finally:
        try:
            shutil.rmtree(name)   # 当with结束之后,删除临时目录
        except OSError as e:
            # ENOENT - no such file or directory
            if e.errno != errno.ENOENT:
                raise e                                              

NamedTemporaryFile: 创建一个临时的文件,它继承了一个类,这个类实现了__enter__, __exit__ 方法,因此能用with
pre_exec: 捕捉信号,并进行信号处理
subprocess.Popen: 具体执行command或者shell脚本

airflow的安装和配置

1.安装
virtualenv airflow
export AIRFLOW_HOME=~/airflow
source airflow/bin/activate
pip install airflow
这个过程时间有点长,airflow安装了很多依赖包,数据库同步工具alembic, orm工具sqlalchemy, flask等
2.初始化数据库
airflow默认的数据库是sqlite,如果你想具体测试airflow的功能的话,你需要指定一个真实的数据库,mysql或者postgresql
airflow initdb

3.启动服务
airflow webserver -p 8080
启动服务之后,你就可以访问127.0.0.1来访问airflow。这时整个网站是没有登录入口的,需要在配置文件里配置才可以看到用户登录界面
4.配置登录界面
airflow配置文件在主目录下,airflow.cfg
找到[webserver]这一项
authenticate = True
auth_backend = airflow.contrib.auth.backends.password_auth
把这两项改完之后,保存配置文件
cd /airflow python
Python 2.7.9 (default, Feb 10 2015, 03:28:08)
Type “help”, “copyright”, “credits” or “license” for more information.

>>> import airflow
>>> from airflow import models, settings
>>> from airflow.contrib.auth.backends.password_auth import PasswordUser
>>> user = PasswordUser(models.User())
>>> user.username = 'new_user_name'
>>> user.email = 'new_user_email@example.com'
>>> user.password = 'set_the_password'
>>> session = settings.Session()
>>> session.add(user)
>>> session.commit()
>>> session.close()
>>> exit()

重启服务 airflow webserver -p 8080
5.设置一个后端
修改airflow.cfg:
executor = LocalExecutor
sql_alchemy_conn = mysql://username:password@localhost:3306/dbname
初始化数据库:
airflow initdb
6.测试airflow的scheduler
启动scheduler服务: airflow scheduler 如果定时任务还没有运行的话,重启一下服务 airflow webserver -p 8080

airflow之DAGs详解

airflow是一个描述,执行,监控工作流的平台。airflow自带了一些dags,当你启动airflow之后,就可以在网页端看到这些dags,我们也可以自己定以dag。

1.什么是DAGs
DAG是一个有向无环图,它是一个task的集合,并且定义了这些task之间的执行顺序和依赖关系。比如,一个DAG包含A,B,C,D四个任务,A先执行,只有A运行成功后B才能执行,C只有在A,B都成功的基础上才能执行,D不受约束,随时都可以执行。DAG并不关心它的组成任务所做的事情,它的任务是确保他们所做的一切都在适当的时间,或以正确的顺序进行,或者正确处理任何意外的问题。

2.什么是operators
DAG定义了一个工作流,operators定义了工作流中的每一task具体做什么事情。一个operator定义工作流中一个task,每个operator是独立执行的,不需要和其他的operator共享信息。它们可以分别在不同的机器上执行。
如果你真的需要在两个operator之间共享信息,可以使用airflow提供的Xcom功能。

airflow目前有一下几种operator:
BashOperator – executes a bash command
PythonOperator – calls an arbitrary Python function
EmailOperator – sends an email
HTTPOperator – sends an HTTP request
SqlOperator – executes a SQL command
Sensor – waits for a certain time, file, database row, S3 key, etc…

3.写一个简单的DAG
first_dag.py

# -*- coding:utf-8 -*-
import airflow
import datetime
from builtins import range
from airflow.operators.bash_operator import BashOperator
from airflow.models import DAG

args = {
    'owner': 'test',
    'start_date': airflow.utils.dates.days_ago(1)
}   # 默认参数

dag = DAG(
    dag_id='first_dag',
    default_args=args,
    schedule_interval='*/1 * * * *',
    dagrun_timeout=datetime.timedelta(minutes=60)
)  # 创建一个DAG实例
dag.sync_to_db()     # 将dag写到数据库中

run_last = BashOperator(task_id='run_last', bash_command='echo 1', dag=dag)   # 定义一个operator,这个operator不做任何操作

for i in range(3):
    op = BashOperator(task_id='task_run_%s'%i, bash_command='ls -l', dag=dag)  # 执行 ls -l 命令
    op.set_downstream(run_last) # set_downstream set_upstream 定义operator的关系

if __name__ == "__main__":
    dag.cli()

基于大数据的用户画像

什么是用户画像?
简而言之,用户画像是根据用户社会属性、生活习惯和行为等信息而抽象出的一个标签化的用户模型。构建用户画像的核心工作即是给用户贴“标签”,而标签是通过对用户信息分析而来的高度精炼的特征标识。用户画像作为“大数据”的核心组成部分,在众多互联网公司中一直有其独特的地位。
举例来说,如果你经常购买一些玩偶玩具,那么电商网站即可根据玩具购买的情况替你打上标签“有孩子”,甚至还可以判断出你孩子大概的年龄,贴上“有5-10岁的孩子”这样更为具体的标签,而这些所有给你贴的标签统在一次,就成了你的用户画像,因此,也可以说用户画像就是判断一个人是什么样的人。

用户画像的作用
精准营销,分析产品潜在用户,针对特定群体利用短信邮件等方式进行营销;
用户统计,比如中国大学购买书籍人数 TOP10,全国分城市奶爸指数;
数据挖掘,构建智能推荐系统;
进行效果评估,完善产品运营,提升服务质量,其实这也就相当于市场调研、用户调研,迅速下定位服务群体,提供高水平的服务

数据收集
数据收集大致分为网络行为数据、服务内行为数据、用户内容偏好数据、用户交易数据这四类。

  • 网络行为数据:活跃人数、页面浏览量、访问时长、激活率、外部触点、社交数据等
  • 服务内行为数据:浏览路径、页面停留时间、访问深度、唯一页面浏览次数等
  • 用户内容便好数据:浏览/收藏内容、评论内容、互动内容、生活形态偏好、品牌偏好等
  • 用户交易数据(交易类服务):贡献率、客单价、连带率、回头率、流失率等

用户画像的系统架构:

2-9-880x450

美团架构

523

携程架构

上图是美团的和携程的系统架构,不知道是几年前的了。但是从两个架构里可以看出共同的部分:采集、计算、存储/查询和监控。采集的数据分为非实时和实时两种。数据的准确性是衡量用户画像价值的关键指标。

所以,我们现在要考虑的问题是:

我们为什么要给教师做画像?

我们怎么给教师做画像?

我们怎么评估我们给教师做的画像?

教师端H5埋点的现状与改进

        数据很重要。用户的行为数据就好比给了我们一双天眼,清晰的观察用户的习惯和需求,同时也能从中看出我们自己产品设计和经营中的一些不合理存在。大数据时代背景下,海量数据喷涌而出,“数据收集——数据整理——数据分析——数据可视化”,势在必行。
        数据第一步:数据收集。实现方式: 埋点。
教师端埋点现状
        提前封装好js片段,当需要统计的行为发生时,触发脚本执行,发送统计信息。
教师端埋点–每个页面统一的配置信息 和 页面统一入口 http://note.youdao.com/noteshare?id=f65209f833f444fbeb06b8a872c56928&sub=9C87991238584D7FA1C54461952DB75F
        教师端埋点–某些页面需要特殊配置的地方,就行单独处理 http://note.youdao.com/noteshare?id=cf762255544e27dc23f5a0a3a6fd7225&sub=96734D456CEA4F208DBF095E7E001EA0
        教师端埋点–stat.js(发送统计信息的方法和一些逻辑处理)http://note.youdao.com/noteshare?id=ae7ae2a06d973e173a294eb38fa34153&sub=3F5F4ECCE54149CB84FF8E825C0D9A7A
        大部分信息在大部分场景是通过统一配置和相应的过滤、映射等方式获取,个别除外。信息部分:进入首页的方式(refer)必须从客户端插件获取,共有登陆,启屏,开机视频,修改密码4种,这个只有客户端自己清楚,而H5只有通过调用原生插件,才能获取到。场景部分:某个按钮的点击(去分享按钮),tab的切换(首页底边栏4个,班级详情顶部栏3个),这些目前是手动触发的,可以参考第二个链接中的代码。
教师端埋点改进
        H5在埋点方面已经实现一定程度上的自动化,统一的配置、统一的入口拦截和发送。又或者当我们又增加了新的页面时,只需在views里做统一配置,然后去stat.js里面作统一映射,就可以了。但是,有些事臣妾做不到。比如IOS的事件有统一管理栈,可以作统一拦截和处理,但H5并没有这样机制,H5只能自己去做标识,再去跟自己自己做的标识作相应的监听处理,它跟我们的需求数据(即收集的数据)有莫大的关联,只能根据BI小组的需求去做相应的特殊处理。这种方式已经在PC的埋点中有一定的实践和应用,确实在一定程度上减少了前端的工作量和以后扩展的成本,这个也可以应用到教师端,作为改进部分方案,可在一定程度上解决“大部分场景”的问题,实现可配置的自动化,当然,相应的js脚本也得跟着变化,需求去执行监听和处罚的操作。
        这些优化只能算是局部优化,离完全自动化的埋点还有很大差距,欢迎老司机们献计献策~
        随着公司的发展壮大,对数据的需求会变化,之前很多额埋点信息会作废,推倒重新来埋不是不可能,为了面对这种情况,埋点的可扩展,可更改,高效率的实现方式很重要,一劳永逸永远是代码的最高境界。但,我们在寻求这个方式的前提下,不得不去考虑
        1、数据的准确性;
        2、性能。
        数据的全面、及时很重要,但数据的准确、可靠才是其价值的根基。
        扯这段没别的意思,只是想提醒一下,所有的埋点的解决方案都必须以数据的准确可靠作为基础。

客户端埋点的一些分析和体会

最近了解很多的关于埋点的相关文章,感受颇多,在此给大家分享一下。有不正确的地方还望各位指正。这篇文章主要分为3部分:

一、埋点的重要性

追踪用户在平台每个界面上的系列行为,事件之间相互独立(如打开商品详情页——选择商品型号——加入购物车——下订单——购买完成);
联合公司工程、ETL采集分析用户全量行为,建立用户画像,还原用户行为模型,作为产品分析、优化的基础。
无疑,数据埋点是一种良好的私有化部署数据采集方式。数据采集准确,满足了企业去粗取精,实现产品、服务快速优化迭代的需求。
但是,因手动埋点工程量极大,且一不小心容易出错,成为很多工程师的痛。且其开发周期长,耗时费力,很多规模较小的公司并不具备自己埋点的能力。无埋点成为市场新宠。最后埋点、无埋点两种技术谁能成为最后赢家,我们拭目以待。

二、埋点的终极方案

埋点的最终理想状态是:不用修改客户端,通过网络进行配置相关数据,就可以达到修改客户端的埋点数据。
原理如下图:111
具体原理:
服务端通过网络配置json数据表(服务端进行数据表的版本控制)
客户端每次开启数据的时候,都会发请求BIVersion,将获取到的版本号和本地存储的版本号进行比较。如果版本号大于本地保存的版本号,根据网络数据,更新本地数据。
客户端每次发送请求的时候,发送的相关数据,和数据结构配置,都通过读取配置表里面的数据(服务端保存的JSon数据)。这样就可以做到只通过网络端进行配置,就可以实现客户端的埋点上传数据更改了。之后维护全部都是维护网络上的数据表就可以了。不用再修改客户端

三、目前项目现状

目前版本的教师端埋点实现方案:

Android客户端,已经实现抽出一个工具类。埋点的时候只需要调用一下工具类方法,并且将需要上传的参数传过去就可以实现埋点。
优点:抽出一个公共方法,可以避免代码的冗余,如果需要修改某一个公共字段,实现了一定的数据解耦和逻辑解耦。
缺点:手动埋点工程量极大,且一不小心容易出错,成为很多工程师的痛。且其开发周期长,耗时费力。

重构版本的教师端埋点实现方案:

在抽出工具类的基础上,对页面进行封装,抽出一个基类,在基类的生命周期中,调用工具类方法,同时最大的改进是所有的埋点需要上传数据和页面埋点发送数据结构,全部都通过本地保存的数据配置表来动态获取,做到了只需要修改数据表,就可以实现BI埋点的更改。
优点:避免代码荣誉,进一步对代码进行了优化。如果需要修改某一个字段,只需要修改配置表就可以,简化了数据操作,对数据和逻辑进行了进一步的解耦,实现了无埋点的理论
缺点:由于目前服务端不支持网络配置表,所以如果配置表修改了,就需要跟新客户端才能获取到最新的数据据,不能做到动态的数据变更。

理想版本的教师端埋点实现方案:

详情请见第二条
优点:所有埋点数据全部从网络获取,可以远程的进行动态配置,远程进行埋点数据更新,无论客户端是否进行更新,都可以做到全部的客户端同时更新。
缺点:再设计数据结构之前,需要做较多的调研,从而设计出一套比较完善合理的网络配置表。如果数据结构变动比较大的话,可能会出现不兼容问题。

Android教师端阿里埋点分析

一. 爱学习教师端需求分析

教师端当前埋点需求,简单描述就是: 基础数据 , “线” , “点” 三部分;

1.基础数据

BI需要记录用户的当前使用环境的基本数据,举个栗子如:设备IMEI , 经纬度, 客户端当前的毫秒值,设备型号,品牌 …

2.线

收集用户的使用行为,把页面的跳转都可以归纳为”线”的部分,目的了解用户使用app的行为线,在行为线的过程中还伴随着浏览资源数据的收集,例如:用户浏览课件视频的ID

3.点

收集特殊行为,例如:分享

二. 教师端当前埋点情况

根据BI提供的页面ID,自定义属性, 在页面抽取的基类中初始化埋点:

1.初始化

/**
 * 通过EndPoint、accessKeyID、accessKeySecret 构建日志服务客户端
 * @endPoint: 服务访问入口,参见 https://help.aliyun.com/document_detail/29008.html
 */
final LOGClient myClient = new LOGClient("cn-beijing.log.aliyuncs.com", "LTAICRtCfbl0u55m",
        "nrRJLautQTQzQopzePrP2NeKlEZAx9", "axx-logs");

/* 创建logGroup */
final LogGroup logGroup = new LogGroup();

2.抽取页面参数model(减少篇幅,简化代码)

public class AliLogBean {

    private String pad = "";
    private String da_src = "";
    private String rpad = "";
    private String pp2 = "";
}

3.定义数据收集,发射公共方法

/**
 * 阿里统计 2017.5
 * @param aliLogBean
 */
public void aliLog(AliLogBean aliLogBean){
   
    logGroup2.PutLog(log);
    //发送log ,在后台发送数据,以防止阻塞主线程
    Thread t = new Thread() {
        public void run() {
            try {
                myClient.PostLog(logGroup2, "user-log");
            } catch (LogException e) {
                e.printStackTrace();
            }
        }
    };
    t.start();

4.页面发送

QQ截图20170601180956

由于只记录跳转的轨迹,而不记录返回的轨迹,根据android中activity的生命周期,这部分是放在onCreate()中进行的

三. 关于埋点阅读的一些文章的总结

这是阿里云栖社区里  https://yq.aliyun.com/articles/8238   关于埋点的一篇文章内容比较概括, 但观点可以借鉴,主要说到:

1.代码埋点数据收集

我们现有的埋点方案应该就是这个;

优点:数据收集准确性高,可以完全契合需求(页面跳转线数据的收集)

缺点: 埋点代码分布到各个页面, 耦合性高, 代码不优雅

2.无埋点数据收集

无埋点暂且理解为一种集中数据收集,android这一块主要就是根据Page + ViewTree 的方式对页面的组件进行定位,viewtree概念图如下:

QQ截图20170601183647

注重于针对view的单点数据收集,并且在遍历vietree的同时非常的消耗性能,来看了两篇关于埋点的文章 <Android AOP之字节码插桩>  http://www.jianshu.com/p/c202853059b4   和 <Android 无埋点数据收集SDK关键技术> http://www.jianshu.com/p/b5ffe845fe2d?from=jiantop.com  ;

Android中AOP框架的局限性:

Dexposed,Xposed的缺陷很明显,xposed需要root权限,Dexposed只对部分系统版本有效。(这两个对于现在的Andorid 来说完全可以放弃)
与之相比AspactJ没有这些缺点,但是aspactJ作为一个AOP的框架来讲对于我们来讲太重了,不仅方法数大增,而且还有一堆AspactJ的依赖要引入项目中(这些代码定义了aspactJ框架诸如切点等概念)。更重要的是我们的目标仅仅是按照一些简单的切点(用户点击等)收集数据,而不是将整个项目开发从OOP过渡到AOP。
AspactJ对于我们想要实现的数据收集需求太重了,但是这种编译期操作class文件字节码实现AOP的方式对我们来说是合适的。

四.个人理解

关于当前Andorid 教师端的埋点,有需要优化的地方,例如:初始化不应在BaseActivity中进行,而应该放在Application中, 还有现在的点完全分布的, 应该对点进行集中整理,方便管理,  至于自动化埋点 和  上述的无埋点数据收集, 主要针对原生的页面, 着重于组件的点击事件收集(个人理解为着重于”点”数据收集),   并且在数据收集的准确性和性能消耗上不占优势,   我们BI业务需求着重于”线”数据收集, 对于教师端的APP 我们为混合开发,大部分的页面交互都是前端插件和原生页面之间的,且原生页面占比甚少, 也是我们业务的一个特殊性! 关于之前考虑到的点数据的变化, 想到走接口, 本地维护映射表的方式以解决, 就这个问题个人和吴世勇交流过, 约定的点数据是不会发生变化的,   也就不存在这个问题了!

个人观点,欢迎指正!

用户行为统计整体设计探讨 — iOS技术实现篇

在保证移动端流量不会受较大影响的前提下,PM 们总是希望埋点覆盖面越广越好与此同时,作为开发者的我们更希望以一种可复用、解耦、动态可配、易于维护的可执行方案。所以,本文旨在探讨一种可复、解耦、动态可配的、容易维护的用户行为统计 (User Behavior Statistics, UBS) 方案。我将尽量从整体设计的视野,以 iOS 技术实现为例,与大家一起探讨 UBS (俗称:埋点)。本文整体分为三部分:

常规做法的优缺点

目前常规的做法是将埋点代码封装成工具类,但凡工程中需要埋点(如点击事件、页面跳转)的地方都插入埋点代码:

  • 优点:哪里需要哪里注入代码,简单明了;不会出现莫名其妙的崩溃问题。 ps.相较于使用 Runtime 的实现而言
  • 缺点:随着项目越来越复杂,埋点的代码散落在程序的各个角落,不利于维护以及复用;统计操作完全依赖于移动端的版本升级,无法动态配置;工作量大

可复用、解耦方案初步落成

我们的项目中是怎么处理的呢?首先谈一下解耦,避免埋点代码散落在程序的各个角落与业务代码糅杂在一起。同时,不得不谈到 AOP 名词这里不做过多解释了,自己Google 吧。在 iOS 中实现 AOP 编程的技术就是基于 Objective-C Runtime 特性的 Method Swizzling。上干货:

+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
    Class class = cls;
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

接下来就是 hook 的方法:

  • 对于页面事件的收集,主要通过 hook 系统类 UIViewController 的生命周期方法来实现,比如:viewDidAppear
  • 所有的 UIControl 类型的控件、UITabBarButton 以及在导航栏上自定义添加的 UIBarButtonItem 的点击事件,都可以通过 hook 系统类UIApplication 的 sendAction:to:from:forEvent: 方法进行拦截。但是,这个方法并不能拦截到导航栏上系统自动添加的返回按钮的点击,因此又 hook 了 UINavigationController 的 navigationBar:shouldPopItem: 方法来实现对它的点击的拦截

这时候问题来了,项目中每个页面都会有自己的页面事件编号(pad),此处的埋点代码如何知道要发送什么 pad 给服务端呢?轻松祭出 KVC与Dictionary ,创建一个配置文件 GSUserStatisticsConfig.plist 代码示例:

{
    "GSLoginViewController": {
        "pad": {
            "enter":"at1"
        },
        "ped": {
            "onxxxBtnPressed":"xxx"
        }
    }
}

通过上述处理,基本上实现了埋点代码与业务代码的解耦,作为一个统计模块复用性也非常显著。接下来谈一下

探索动态可配与易维护

在探索动态可配与已维护的数据采集方面,业界有一种方案称之为无埋点也叫全埋点,即不需要用户主动埋点,可以收集用户所有的操作行为。
接下来贴一张图来的更为直观一点:
userStastics
从上图可以看出,在实现无埋点数据收集时,主要分为3步:上传统计配置文件、请求统计配置文件、业务数据的收集与上报。
配置文件的设计与自身业务息息相关不再详述,请求配置文件也不是要讨论的核心,核心在于业务数据根据配置文件的动态收集,业界开发者称之为无埋点去获取配置文件中想要的业务数据。
参考案例:

学习该方案与我们自身产品业务埋点的梳理:

优点非常突出:

  • 维护成本,主要管理配置文件即可
  • 弥补埋点时存在错埋、漏埋等情况,动态更新及添加
  • 埋点代码无需跟随APP版本一起发布,不耽误数据的收集与统计
  • 对于一些动态事件做到很好的支持,例如:同一位置显示不同的内容,同一内容显示在不同的位置
  • 可以统计同一个按钮事件,在产品上可能代表不同的状态,例如在播放状态下关闭按钮和回放状态下关闭按钮,按钮的事件是一样的,但是需求去判断是播放状态还是回放状态

缺点:

  • 我们的埋点注重的是用户的浏览行为是一条路径(亦可称之为一条线),对单点操作行为统计要求不强,有点“杀鸡焉用牛刀”的感觉;
  • 无埋点方案需要大量hook系统方法,该行为导致 crash 的几率升高;
  • 如果收集数据量比较庞大,遍历事件然后拿到配置文件匹配事件的能耗会显现出来导致 APP 卡顿;
  • 开发周期会相对长一些:需要前端要搭建配置文件的平台;需要 BI 配合下发和处理 采集的数据;需要 iOS 和 Android 两端攻克无埋点采集数据的技术难点。

补充:

weex 页面的业务埋点思路同上,但是需要再做额外的处理;

最后,探讨毕竟是探讨,欢迎留言讨论。

移动端埋点方式探讨 – iOS端

目标:灵活的埋点方式,移动端不需要大量修改代码,甚至不用重新发包即可按照PM要求发送需要的统计数据。

要求:高度解耦,复用性强,动态配置,易于维护。

知道了要努力的目标和要求,我们先来看一下现在老版教师端和重构版教师端的优缺点。

一.老版教师端采用的是一个工具类发送数据,需要的个性化数据由每个界面自行传值进入。

优点:

1.灵活性较高,能满足个性需求:按照现在的发送的数据映射表,个别界面需要发送一些特殊信息,此种实现方式可以较好的满足这种情况的出现。

举个栗子:播放教案时,需要发送教案本身的id以及教案所在讲次的id,这两个数据其余界面并不需要发送。

2.有一定的解耦:这里解耦的就是发送数据这块,做成了工具,每个界面调用工具的API然后传值进行发送,不需要每个界面都写一遍工具类中的实现代码。

缺点:

1.对界面侵入性强,后期维护困难:由于工具类只负责发送数据,每个界面都会写一遍调用API方法,这样就造成重复代码冗余,解决方案是所有的界面都继承一个公用基类,每个界面自己去实现基类的方法即可。

但是由于老版的界面没有一个基类来管理,所以导致现在如果添加基类需要修改大量代码,需要大量的测试工作以保证APP本身的质量和埋点的完善。

2.灵活性不强:这里所说的不灵活是指当统计需要的数据映射改变后,尤其是一些个性公用字段的修改(比如:界面id),就会导致APP涉及埋点界面全部修改一遍,非常不灵活。同时由于iOS本身的审核机制,导致了上架周期相对安卓变长,会造成数据的丢失和上架的不及时。

二.新版教师端的埋点,采用的是统一配置表,由基类抓取当前界面,然后根据配置表获取到界面,再根据界面来发送数据。

优点:

1.高度解耦:每个界面自身的id有一个配置表,通过运行时的一个抓取类来抓取当前界面,然后在配置表来获取界面id再发送数据。

2.侵入性低,维护简单:由于有基类的存在,每个界面都实现了基类的方法,就可以被钩子抓取到界面,统计的所有代码没有和业务代码在一起,后期维护时只需要修改配置表即可。

缺点:

1.灵活性不够,个性数据获取困难:钩子抓取到的界面,通过配置表获取需要数据发送,但是这些都是写死的,个别界面自身的特殊数据无法获取然后进行发送,可以看上面的例子,这种实现方式就无法获取,如果获取就需要侵入相应界面。

 

新老的埋点都是在原生写死的映射,无法实现灵活的修改更新。而且后期定下来的技术方案最好在新版本中进行实现,因为老版本的代码的问题可能不容易或无法满足一些条件。

 

下面是最近看的文章中关于统计方案的思路和实现方法。大部分的文章采用的都是和新版教师端一样的方法,利用OC的RunTime黑科技Method Swizzling来Hook界面注入代码实现。

先来这些文章的链接

http://blog.csdn.net/w10207010218/article/details/56274431

http://www.jianshu.com/p/0497afdad36d

http://blog.csdn.net/kaka_2928/article/details/61201320

http://www.cocoachina.com/ios/20170427/19108.html 主要查看了这篇文章,里面还有个链接可以查看基础篇

重点讲一下最后链接文章的内容,文章是网易乐得的技术人员写的,不过他们的SDK没有对外公开(好像安卓公开了),他们声称:SDK 已经具备不需要代码埋点就能 自动的、动态可配的、全面且正确 的收集用户在使用 App 时的所有事件数据。除此之外,还单独开发了与之配合的圈选SDK,能够在 App 端完成对界面元素的圈配以及 KVC 配置的上传。而界面元素圈配的工作完全可以交给用研与产品人员来做,减轻了开发人员的工作量。

完成了两大部分:基本事件数据的收集和业务层数据的收集

文章中的SDK的实现思路和大部分主流一样是利用了Runtime 特性的 Method Swizzling 黑魔法,但是SDK也可以实现对界面中的子控件的数据收集,通过viewPath 及 viewId ,内容很多建议通过链接查看

Snip20170602_5

要实现灵活的进行数据收集和发送,就需要使用KVC实现。

什么是KVC配置。其实 KVC配置 就是一些用来描述 App 应该在什么时机去收集什么数据的信息

  • 上传KVC配置
    • 利用 圈选SDK 上传 KVC配置 的操作对于用户是透明的,主要由开发人员进行上传与管理。此操作可以在任何时候进行,在想要收集某个或某些版本的 App 中的业务数据时,上传相应的KVC配置信息至后台即可,达到了根据需要动态可配的效果。
  • 请求KVC配置
    • SDK 在初始化时会触发 KVC配置 的请求操作,从后台拉取 App 当前版本对应的所有KVC配置,并将请求结果缓存起来,以提供给下一步使用。

上传的所有的 KVC配置 需要与 App 的版本相对应,因为 App 版本不同会直接导致keyPath可能不一样。所以与 KVC配置 相关的工作有如下2个:

  1. 针对当前 App 版本上传相应的 KVC配置,以获取想要的业务数据
  2. 当 App 新版本发布时,需要对之前版本上的 KVC配置 逐一验证,是否仍然适用于新版本。如果仍然适用,则直接在管理后台上把新的版本号添加到此 KVC配置;如果不再适用,则对新版本再上传一个新的KVC配置。

业务数据的收集与上报的流程

8db8721df6e82075t

重构版的埋点采用的是现在的主流方案,现在需要攻克的难点就是灵活性的问题,怎么样从服务器去获取配置表,然后本地映射获取需要的数据,然后发送给统计平台。

数据仓库——维度建模十大原则

前言

特别声明:本文整理自互联网。

遵循这些原则进行维度建模可以保证数据粒度合理,模型灵活,能够适应未来的信息资源,违反这些原则你将会把用户弄糊涂,并且会遇到数据仓库障碍。

原则1、载入详细的原子数据到维度结构中

维度建模应该使用最基础的原子数据进行填充,以支持不可预知的来自用户查询的过滤和分组请求,用户通常不希望每次只看到一个单一的记录,但是你无法预测 用户想要掩盖哪些数据,想要显示哪些数据,如果只有汇总数据,那么你已经设定了数据的使用模式,当用户想要深入挖掘数据时他们就会遇到障碍。当然,原子数 据也可以通过概要维度建模进行补充,但企业用户无法只在汇总数据上工作,他们需要原始数据回答不断变化的问题。

原则2、围绕业务流程构建维度模型

业务流程是组织执行的活动,它们代表可测量的事件,如下一个订单或做一次结算,业务流程通常会捕获或生成唯一的与某个事件相关的性能指标,这些数据转换 成事实后,每个业务流程都用一个原子事实表表示,除了单个流程事实表外,有时会从多个流程事实表合并成一个事实表,而且合并事实表是对单一流程事实表的一 个很好的补充,并不能代替它们。

原则3、确保每个事实表都有一个与之关联的日期维度表

原则2中描述的可测量事件总有一个日期戳信息,每个事实表至少都有一个外键,关联到一个日期维度表,它的粒度就是一天,使用日历属性和非标准的关于测量事件日期的特性,如财务月和公司假日指示符,有时一个事实表中有多个日期外键。

原则4、确保每个事实表中的事实具有相同的粒度或同级的详细程度

在组织事实表时粒度上有三个基本原则:事务,周期快照或累加快照。无论粒度类型如何,事实表中的度量单位都必须达到相同水平的详细程度,如果事实表中的事实表现的粒度不一样,企业用户会被搞晕,BI应用程序会很脆弱,或者返回的结果根本就不对。

原则5、解决事实表中的多对多关系

由于事实表存储的 是业务流程事件的结果,因此在它们的外键之间存在多对多(M:M)的关系,如多个仓库中的多个产品在多天销售,这些外键字段不能为空,有时一个维度可以为 单个测量事件赋予多个值,如一个保健对应多个诊断,或多个客户有一个银行账号,在这些情况下,它的不合理直接解决了事实表中多值维度,这可能违反了测量事 件的天然粒度,因此我们使用多对多,双键桥接表连接事实表。

原则6、解决维度表中多对一的关系

属性之间分层的、多对一(M:1)的关系通常未规范化,或者被收缩到扁平型维度表中,如果你曾经有过为事务型系统设计实体关系模型的经历,那你一定要抵抗住旧有的思维模式,要将其规范化或将M:1关系拆分成更小的子维度,维度反向规范化是维度建模中常用的词汇。

在单个维度表中多对一(M:1)的关系非常常见,一对一的关系,如一个产品描述对应一个产品代码,也可以在维度表中处理,在事实表中偶尔也有多对一关系,如详细当维度表中有上百万条记录时,它推出的属性又经常发生变化。不管怎样,在事实表中要慎用M:1关系。

原则7、存储报告标记和过滤维度表中的范围值

更重要的是,编码和关联的解码及用于标记和查询过滤的描述符应该被捕获到维度表中,避免在事实表中存储神秘的编码字段或庞大的描述符字段,同样,不要只 在维度表中存储编码,假定用户不需要描述性的解码,或它们将在BI应用程序中得到解决。如果它是一个行/列标记或下拉菜单过滤器,那么它应该当作一个维度 属性处理。

尽管我们在原则5中已经陈述过,事实表外键不应该为空,同时在维度表的属性字段中使用“NA”或另一个默认值替换空值来避免空值也是明智的,这样可以减少用户的困惑。

原则8、确定维度表使用了代理键

按顺序分配代理键(除了日期维度)可以获得一系列的操作优势,包括更小的事实表、索引以及性能改善,如果你正在跟踪维度属性的变化,为每个变化使用一个 新的维度记录,那么确实需要代理键,即使你的商业用户没有初始化跟踪属性改变的设想值,使用代理也会使下游策略变化更宽松,代理也允许你使用多个业务键映 射到一个普通的配置文件,有利于你缓冲意想不到的业务活动,如废弃产品编号的回收或收购另一家公司的编码方案。

原则9、创建一致的维度集成整个企业的数据

对于企业数据仓库一致的维度(也叫做通用维度、标准或参考维度)是最基本的原则,在ETL系统中管理一次,然后在所有事实表中都可以重用,一致的维度在 整个维度模型中可以获得一致的描述属性,可以支持从多个业务流程中整合数据,企业数据仓库总线矩阵是最关键的架构蓝图,它展现了组织的核心业务流程和关联 的维度,重用一致的维度可以缩短产品的上市时间,也消除了冗余设计和开发过程,但一致的维度需要在数据管理和治理方面有较大的投入。

原则10、不断平衡需求和现实,提供用户可接受的并能够支持他们决策的DW/BI解决方案

维度建模需要不断在用户需求和数据源事实之间进行平衡,才能够提交可执行性好的设计,更重要的是,要符合业务的需要,需求和事实之间的平衡是DW/BI 从业人员必须面对的事实,无论是你集中在维度建模,还是项目策略、技术/ETL/BI架构或开发/维护规划都要面对这一事实。

End.