如何优雅地统计代码(一)

简介: *精美排版详见钉钉文档其实这个事情要从一个下午讲起,对我来说是个尤里卡时刻;其实一开始让我直接从数据里统计大家提交代码是有点无从下手的,前几天开始调研了一波代码统计方案后发现大部分都是基于文件来统计代码的各种行数并没有这种基于前后版本的变更代码统计,大家更多的使用Git自带的统计方法但显然我这里没有这样的环境(下面背景会详细展开),快要放弃今天的技术调研遂下楼散步刷新思维,我又回溯了我在这个项目中

*精美排版详见钉钉文档

其实这个事情要从一个下午讲起,对我来说是个尤里卡时刻;其实一开始让我直接从数据里统计大家提交代码是有点无从下手的,前几天开始调研了一波代码统计方案后发现大部分都是基于文件来统计代码的各种行数并没有这种基于前后版本的变更代码统计,大家更多的使用Git自带的统计方法但显然我这里没有这样的环境(下面背景会详细展开),快要放弃今天的技术调研遂下楼散步刷新思维,我又回溯了我在这个项目中每个步骤,那几天又恰逢在看关于第一性原理思考的书,越想越发现代码的统计本质就是一个文本比对的问题,于是激动地停止散步冲上楼并有了下面这些内容

背景

目前效能360产品的代码统计模块已经接入超过17个平台的代码数据(包括Just, Def, Aone, Gerrit等)许多平台都有相应的代码统计数据可以直接接入(如下图所示),但Dataphin平台本身接入的表没有做相应的代码统计,只有提交时间和对应的代码内容,相当于存取了同一个分支的前后版本。为什么写这篇文章呢?因为ATA和外网都没有相应文献可供直接参考,集团同学的实时链路开发基本在Dataphin上完成,但Dataphin上的代码行为却无法被统计,不利于自下而上的场景,为了咱们技术小二的行为在线,让每一个劳动都能被看到;

挑战

这里在做代码量统计时遇到许多挑战面临,与其他平台不同的是Dataphin 平台的源数据没有做代码量统计,而是日志型的 ODS 数据(如下图所示)只存提交版本与代码文本,没有直接的统计数据结果,这就需要我们在接入前做一层计算处理来统计出其各个代码量

      V.S      

上面说的是业务层面的困难,这里针对这些挑战拆解成数据问题:

  1. 不同版本之间如何 放在一起进行比对 ?→ 同一个任务且同一个提交人的前后版本代码内容如何打在一行上为后续列运算做准备?
  2. 代码文本放在一起后如何进行统计? → 两个版本代码文本放在一起后如何再进行新增、修改、删除、注释、空行行数等6种指标统计?

对策与方案

针对以上挑战和接入源的格式拆解出来的两个技术方案,并结合代码行为链路给出接入方案Pipline方案如下预期链路

原有链路(Before)

预期链路(After)

这里受技梦社的影响把我遇到的挑战写成问题的形式来让不管是读者还是笔者都更有表达欲,以下就是关于数据链路设计与其中的代码统计具体实现

问题1:同一个任务且同一个提交人的前后版本代码内容如何打在一行上为后续列运算做准备?

在同一个任务ID和提交人ID下的不同版本进行比对,使用窗口函数按照提交版本进行排序,具体结构思路可以见于如下CTE语句,先使用窗口函数进行每个代码提交记录的分组排序识别出版本的前后关系,再将识别出来的前后版本通过连接语句打在一行上为列运算作准备

WITH DATAPHIN_TABLE AS(
    -- 代码提交明细的排序预处理
    SELECT  job_id		   -- 任务ID
            ,modifier 	 -- 提交人ID
            ,version_num -- 版本号
            ,context     -- 内容
            ,ROW_NUMBER() OVER(PARTITION BY job_id,modifier ORDER BY version_num DESC ) AS ver_ord    -- 窗口函数:按任务按人进行分组排序,对版本顺序进行标准化
    FROM    DATAPHIN_TABLE
)
SELECT  PY3_UDF(t2.context,t1.context) AS CODE_DIFF_NUM -- 把前后版本打在一行中进行UDF的列运算
FROM 						 DATAPHIN_TABLE AS t1
LEFT OUTTER JOIN DATAPHIN_TABLE AS t2
ON      t1.job_id = t2.job_id 		  -- 同一个JOB下即同一个节点下
    AND t1.modifier = t2.modifier 	-- 同一个代码提交人
    AND t1.ver_ord = t2.ver_ord - 1 -- 版本必须是前后关系

问题2:两个版本代码文本放在一起后如何再进行新增、修改、删除行数等6种指标统计?

第一个问题解决后,紧接着就来到列运算的技术难题,如何实现代码统计将在下一篇《如何优雅地统计代码(二)》展开,这里先讲一下大致思路:① 先进行注释与空行过滤 ② 完成新增,修改,删除等 6 种细分口径统计开发;决定使用Python的Differ()方法先标明增删改的点再统计其标识,简而言之,就是先打标后统计(如图)通过统计每个切分语句题头的比对标识来判断该语句是修改,新增还是删除

最终这个问题最终解决的输出物就是笔者已经封装好的以下通用代码比对统计的函数,也上线到数据地图的函数市场(如下),这里代码比对核心的Python脚本代码见文末附录,欢迎交流呀

1. sql_code_diff (统计新增与修改的代码行数)
2. sql_code_del
3. sql_code_add
4. sql_code_mdf
5. sql_code_blk_line
6. sql_code_cmt_line

结合上面两个问题的对策成功接入Dataphin平台后,目前旧版自我管理和个人页都能看到小二在Dataphin的代码提交行数的行为啦(如图),提高了兵力的覆盖范围;未来可以进一步提高函数的覆盖度比如能兼容其他编程语言的代码统计,并且思考类似Dataphin这种技术行为接入源的能否沉淀出一套标准化接入方法以应对不断变化的市场和服务未来客户

参考文献 & 附录

  1. 代码比对核心的Python3 UDF代码如下
# coding: utf-8 # 确保encoding='utf-8'
from odps.udf import annotate
import difflib as dl
# import sys
# import os

# Differ实例化
d = dl.Differ() 

# 过滤空行与注释
def flt_empty_comment_line(text):
    context = [] # 新增内容行
    text = text.strip() # 消除前后空行,使得空行变得真空
    text_arr = text.splitlines(keepends=False) # 按照换行符号分隔字符串变为数组
    for i in range(0,len(text_arr)):
        if text_arr[i].find("--") != -1:
            text_arr[i] = text_arr[i].split("--")[0] # "--"这种情况可能会误杀!但行数预估不多比较极端
        if len(text_arr[i].strip()) != 0:
            context.append(text_arr[i])
    return context

@annotate("string,string -> bigint")
class sql_code_diff(object):
    # 统计新增与修改代码量
    def evaluate(self, arg0, arg1):
        # d = dl.Differ() # Differ实例化
        v1 = flt_empty_comment_line(arg0)
        v2 = flt_empty_comment_line(arg1)
        result = list(d.compare(v1,v2))
        # diff_line = [] # 用于新增与修改的代码行
        diff_line_cnt = 0 # 用于计算代码行数
        for i in result:
            if i[0] == '+':
                # diff_line.append(i)
                diff_line_cnt += 1
        return diff_line_cnt 

@annotate("string,string -> bigint")
class sql_code_add(object):
    # 统计新增代码提交量
    def evaluate(self, arg0, arg1):

        # d = dl.Differ() # Differ实例化
        v1 = flt_empty_comment_line(arg0)
        v2 = flt_empty_comment_line(arg1)
        result = list(d.compare(v1,v2))
        
        add_cnt = 0 # 用于计算代码行数
        n = len(result)

        if n > 1: # 大于1行的代码才进入细分判定
            for i in range(1,n):
                # 统计新增行
                if (result[i][0]!='?') & (result[i-1][0]=='+'):
                    add_cnt += 1
            # 统计行末新增
            if result[n-1][0] == '+':
                add_cnt += 1

        return add_cnt 

@annotate("string,string -> bigint")
class sql_code_mdf(object):
    # 统计修改代码提交量
    def evaluate(self, arg0, arg1):

        v1 = flt_empty_comment_line(arg0)
        v2 = flt_empty_comment_line(arg1)
        result = list(d.compare(v1,v2))
        
        mdf_cnt = 0 # 用于计算代码行数
        n = len(result)

        if n > 1: # 大于1行的代码才进入细分判定
            for i in range(1,n):
                # 统计修改行
                if (result[i][0]=='?') & (result[i-1][0]=='+'):
                        mdf_cnt += 1
        return mdf_cnt 


@annotate("string,string -> bigint")
class sql_code_del(object):
    # 统计删除代码提交量
    def evaluate(self, arg0, arg1):

        v1 = flt_empty_comment_line(arg0)
        v2 = flt_empty_comment_line(arg1)
        result = list(d.compare(v1,v2))
        
        del_cnt = 0 # 用于计算代码行数
        n = len(result)

        if n > 1: # 大于1行的代码才进入细分判定
            for i in range(1,n):
                # 统计删除行
                if (result[i][0]!='?') & (result[i-1][0]=='-'):
                        del_cnt += 1
             # 统计行末删除
            if result[n-1][0] == '-':
                del_cnt += 1

        return del_cnt 

@annotate("string -> bigint")
class sql_code_blk_line(object):  # blank 缩写为blk
    # 统计空行数
    def evaluate(self, arg0):
        empty_cnt = 0
        text = arg0.strip() # 消除前后空行,使得空行变得真空
        text_arr = text.splitlines(keepends=False) # 按照换行符号分隔字符串变为数组
        for i in text_arr:
            if len(i.strip()) == 0:
                empty_cnt += 1

        return empty_cnt

@annotate("string -> bigint")
class sql_code_cmt_line(object): # comment 缩写为 cmt
    # 统计注释数
    def evaluate(self, arg0):
        cmt_cnt = 0
        text = arg0.strip() # 消除前后空行,使得空行变得真空
        text_arr = text.splitlines(keepends=False) # 按照换行符号分隔字符串变为数组
        for i in text_arr:
            tmp = i.strip()
            if len(tmp) >= 2:
                if str(tmp[0]) + str(tmp[1]) == '--': # 如果存在行开头只有注释标识,则统计注释行数,行内注释不算
                    cmt_cnt += 1
        
        return cmt_cnt
# coding: utf-8 # 确保encoding='utf-8'
from odps.udf import annotate
import difflib as dl
# import sys
# import os

# Differ实例化
d = dl.Differ() 

# 过滤空行与注释
def flt_empty_comment_line(text):
    context = [] # 新增内容行
    text = text.strip() # 消除前后空行,使得空行变得真空
    text_arr = text.splitlines(keepends=False) # 按照换行符号分隔字符串变为数组
    for i in range(0,len(text_arr)):
        if text_arr[i].find("--") != -1:
            text_arr[i] = text_arr[i].split("--")[0] # "--"这种情况可能会误杀!但行数预估不多比较极端
        if len(text_arr[i].strip()) != 0:
            context.append(text_arr[i])
    return context

@annotate("string,string -> bigint")
class sql_code_diff(object):
    # 统计新增与修改代码量
    def evaluate(self, arg0, arg1):
        # d = dl.Differ() # Differ实例化
        v1 = flt_empty_comment_line(arg0)
        v2 = flt_empty_comment_line(arg1)
        result = list(d.compare(v1,v2))
        # diff_line = [] # 用于新增与修改的代码行
        diff_line_cnt = 0 # 用于计算代码行数
        for i in result:
            if i[0] == '+':
                # diff_line.append(i)
                diff_line_cnt += 1
        return diff_line_cnt 

@annotate("string,string -> bigint")
class sql_code_add(object):
    # 统计新增代码提交量
    def evaluate(self, arg0, arg1):

        # d = dl.Differ() # Differ实例化
        v1 = flt_empty_comment_line(arg0)
        v2 = flt_empty_comment_line(arg1)
        result = list(d.compare(v1,v2))
        
        add_cnt = 0 # 用于计算代码行数
        n = len(result)

        if n > 1: # 大于1行的代码才进入细分判定
            for i in range(1,n):
                # 统计新增行
                if (result[i][0]!='?') & (result[i-1][0]=='+'):
                    add_cnt += 1
            # 统计行末新增
            if result[n-1][0] == '+':
                add_cnt += 1

        return add_cnt 

@annotate("string,string -> bigint")
class sql_code_mdf(object):
    # 统计修改代码提交量
    def evaluate(self, arg0, arg1):

        v1 = flt_empty_comment_line(arg0)
        v2 = flt_empty_comment_line(arg1)
        result = list(d.compare(v1,v2))
        
        mdf_cnt = 0 # 用于计算代码行数
        n = len(result)

        if n > 1: # 大于1行的代码才进入细分判定
            for i in range(1,n):
                # 统计修改行
                if (result[i][0]=='?') & (result[i-1][0]=='+'):
                        mdf_cnt += 1
        return mdf_cnt 


@annotate("string,string -> bigint")
class sql_code_del(object):
    # 统计删除代码提交量
    def evaluate(self, arg0, arg1):

        v1 = flt_empty_comment_line(arg0)
        v2 = flt_empty_comment_line(arg1)
        result = list(d.compare(v1,v2))
        
        del_cnt = 0 # 用于计算代码行数
        n = len(result)

        if n > 1: # 大于1行的代码才进入细分判定
            for i in range(1,n):
                # 统计删除行
                if (result[i][0]!='?') & (result[i-1][0]=='-'):
                        del_cnt += 1
             # 统计行末删除
            if result[n-1][0] == '-':
                del_cnt += 1

        return del_cnt 

@annotate("string -> bigint")
class sql_code_blk_line(object):  # blank 缩写为blk
    # 统计空行数
    def evaluate(self, arg0):
        empty_cnt = 0
        text = arg0.strip() # 消除前后空行,使得空行变得真空
        text_arr = text.splitlines(keepends=False) # 按照换行符号分隔字符串变为数组
        for i in text_arr:
            if len(i.strip()) == 0:
                empty_cnt += 1

        return empty_cnt

@annotate("string -> bigint")
class sql_code_cmt_line(object): # comment 缩写为 cmt
    # 统计注释数
    def evaluate(self, arg0):
        cmt_cnt = 0
        text = arg0.strip() # 消除前后空行,使得空行变得真空
        text_arr = text.splitlines(keepends=False) # 按照换行符号分隔字符串变为数组
        for i in text_arr:
            tmp = i.strip()
            if len(tmp) >= 2:
                if str(tmp[0]) + str(tmp[1]) == '--': # 如果存在行开头只有注释标识,则统计注释行数,行内注释不算
                    cmt_cnt += 1
        
        return cmt_cnt

相关文章
|
6月前
|
存储 安全 Java
写出漂亮代码的45个小技巧(下)
大家好,我是三友~~ 不知道大家有没有经历过维护一个已经离职的人的代码的痛苦,一个方法写老长,还有很多的if else ,根本无法阅读,更不知道代码背后的含义,最重要的是没有人可以问,此时只能心里默默地问候这个留坑的兄弟。。
写出漂亮代码的45个小技巧(下)
|
10月前
|
Java Android开发
几行代码就能实现为何要多此一举
几行代码就能搞定,不能代表一个人很牛,借助了开源,只是站在了巨人的肩膀上,让你省去了去往成功的一大段路,然而这一段路上的风景,还请你仔细去欣赏,到头来,你会发现,路上的风景会远远美于终点的成功。
x11获得窗口名的代码
x11获得窗口名的代码
97 0
李峋的爱心代码
《点燃我温暖你》中李峋的爱心代码
137 0
李峋的爱心代码
|
设计模式 前端开发 Java
握草,你竟然在代码里下毒!
Java程序员👨‍💻‍,10个编码小技巧,用好了升职加薪,用不好开除走人!
879 0
握草,你竟然在代码里下毒!
|
程序员 数据库
【评论】好代码不值钱
导读: 原文来自geekm.ag 上一篇《 Good code is cheap code》,由国内整理编译《好代码不值钱》。作者认为好的程序员和伟大的程序员之间的区别就在于伟大的程序员理解他们的模式。
850 0
|
Python DataX
83行代码
#!/usr/bin/env python #-*-coding:utf-8-*- #****************************************************************************** #****************Descripti.
1463 0