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

简介: *精美排版详见钉钉文档其实这个事情要从一个下午讲起,对我来说是个尤里卡时刻;其实一开始让我直接从数据里统计大家提交代码是有点无从下手的,前几天开始调研了一波代码统计方案后发现大部分都是基于文件来统计代码的各种行数并没有这种基于前后版本的变更代码统计,大家更多的使用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

相关文章
|
存储 Java 定位技术
gis利器之Gdal(二)shp数据读取
本文首先简单介绍了空间数据shp数据的基本知识,其常见的文件组成形式。使用qgis软件对数据进行常规预览,最后重点介绍了使用gdal对矢量信息进行读取,​包括空间信息和属性信息
1566 0
gis利器之Gdal(二)shp数据读取
|
12月前
|
Kubernetes 架构师 Java
史上最全对照表:大厂P6/P7/P8 职业技能 薪资水平 成长路线
40岁老架构师尼恩,专注于帮助读者提升技术能力和职业发展。其读者群中,多位成员成功获得知名互联网企业的面试机会。尼恩不仅提供系统化的面试准备指导,还特别针对谈薪酬环节给予专业建议,助力求职者在与HR谈判时更加自信。此外,尼恩还分享了阿里巴巴的职级体系,作为行业内广泛认可的标准,帮助读者更好地理解各职级的要求和发展路径。通过尼恩的技术圣经系列PDF,如《尼恩Java面试宝典》等,读者可以进一步提升自身技术实力,应对职场挑战。关注“技术自由圈”公众号,获取更多资源。
|
安全 编译器 开发者
Python打包成.exe文件直接运行
Python打包成.exe文件直接运行
1421 1
|
消息中间件 测试技术 领域建模
DDD - 一文读懂DDD领域驱动设计
DDD - 一文读懂DDD领域驱动设计
39055 5
|
存储 Oracle 关系型数据库
RAC创建ASM磁盘组时配置多路径和UDEV
RAC创建ASM磁盘组时配置多路径和UDEV
2894 7
|
JSON API 网络安全
【gerrit】【技巧】如何获取gerrit库入库统计信息之一——概述
【gerrit】【技巧】如何获取gerrit库入库统计信息之一——概述
2491 0
【gerrit】【技巧】如何获取gerrit库入库统计信息之一——概述
SQL-条件查询与聚合函数的使用
SQL-条件查询与聚合函数的使用
|
Ubuntu Java 程序员
手把手教你搭建自己的git+gerrit代码评审服务器
搭建自己的git+gerrit代码评审服务器
1969 0
手把手教你搭建自己的git+gerrit代码评审服务器
|
人工智能
【AI绘画】使用ControlNet- lineart模型
【AI绘画】使用ControlNet- lineart模型
837 0
【AI绘画】使用ControlNet- lineart模型
|
存储 算法
滑动窗口算法的基本思想、应用场景、实现方法、时间复杂度和常见问题
滑动窗口算法的基本思想、应用场景、实现方法、时间复杂度和常见问题
1587 1