数据开发 的 代码规范 以及 代码评审脚本

1、概述

  • 场景
    在数据开发中,由于各程序员风格不一部分程序员代码太烂代码注释过少等原因,导致代码维护时困难重重
    同事A请假去生娃,此时Ta的代码出了问题需要同事B去修改,但由于代码太烂,同事B改不动
  • 代码评审:通过 阅读代码 来 检查代码质量
    目的:降低代码维护成本
  • 使用代码评审自动化脚本(Python3实现),可提高代码评审的效率

2、代码规范

以下规范按照高危强制建议3个级别进行标注,优先级从高到低

2.1、通用代码规范

  • 注释
    【建议】中文注释≥1行
    【建议】注释不用太多,代码即注释
  • 命名规范
    【建议】用纯英文
    【建议】查词典用英文全称;当 名称过长 或 关键字冲突 时,可使用缩写
    【建议】变量、参数、类使用[形容词+]名词
    【建议】方法、函数使用动词[+名词]
    【强制】禁止数字开头
    【强制】禁止拼音和英文混用
    【建议】不使用保留字
时间命名规范【强制】格式示例【强制】变量名
yyyy2022
年月yyyy-mm2021-01
年月日yyyy-mm-dd2020-02-02ymd
季度yyyyQq2021Q42022Q1
时分秒hh:mm:ss08:00:00
年月日时分秒yyyy-mm-dd hh:mm:ss2022-02-01 08:00:00
常见缩写全称中文常见缩写全称中文
fnfunction函数ymdYear Month Day日期
txttextfile文本文件cntcountv. 计数;n. 总数
objobject对象numnumber数字;号码
lslistn. 列表;v. 列清单lvllevel等级
liblibrary软件库dwdata warehouse数据仓库
strstring字符串bakbackup备份
probprobability概率skuStock Keeping Unit库存单位
confconfiguration file配置文件idxindex file索引文件
calccalculationn. 计算uvunique visitor独立访客
regexpregular expression正则表达式pvpage view页面浏览量
app application应用程序aiArtificial Intelligence人工智能
deptdepartment部;科;处addraddress地址
dbdatabase数据库pwdpassword密码
mktmarket市场bizbusiness商业
  • 括号
    【强制】左括号前不换行
    【强制】当行过长时,左括号后换行,换行后要缩进
    【强制】当右括号后还没结束时,不允许换行
{
	"timestamp": 1585744376001,
	"page": {
		"page_id": "页面ID",
		"last_page_id": "上个页面ID",
		"page_type": "登录页"
	},
	"actions": [{
		"action": "拖动",
		"item": "拼图验证码",
		"timestamp": 1585744376605
	}, {
		"action": "点击",
		"item": "登录键",
		"timestamp": 1585744377778
	}]
}
  • 其它
    【建议】代码中不得出现生产环境的明文密码
    【建议】单行代码不可太长(例外:长URL、长import
    【建议】项目要有说明文档,名为README.md

2.2、配置文件和传参规范

  • 配置文件通常用于存储数据库连接参数
  • 通常是时间传参

https://yellow520.blog.csdn.net/article/details/122088401

2.3、Python代码规范

Python代码规范 继承 通用代码规范

命名规范说明示例
变量、方法、函数、包【强制】全小写,下划线分隔单词function_name
类中的方法和函数(不被外部直接调用的)【强制】双下划线开头,全小写,下划线分隔单词__method_name
常量【强制】全大写 和 下划线PUBLIC_CONSTANT
模块【强制】全小写,下划线分隔单词module_name.py
【强制】每个单词首字母大写ClassName
项目名【建议】每个单词首字母大写ProjectName
注释规范注释建议
模块注释、函数注释、类注释、方法注释…【强制】三双引号,注释的前面没有空行
单行代码和多行代码 的 单行注释【强制】井号
单行代码后 的 单行注释【强制】两个空格+井号
多行注释(单行注释太长时,建议写成多行)【建议】井号(连续行)
其它规范说明
缩进、空行、空格【强制】Tab缩进量:4个空格
【建议】把Pycharm更到最新,用Ctrl+Alt+l来规范代码
字符串【建议】单行优先用单引号
【建议】单行子字符串里有单引号时用双引号,如:sql = "SELECT '2021-12-02'"
【强制】多行用三单引号
【建议】不使用三双引号
"""
模块注释
"""
from time import time

# 多行代码的
# 多行注释(一行写不下的时候才写多行)
NUM = 5
CNT = 1000


def function1():
    """函数注释"""
    return time()  # 单行代码后 的 单行注释


class ClassName:
    """类注释"""

    @classmethod
    def method4(cls):
        """方法注释"""


if __name__ == '__main__':
    print(__doc__)
    print(function1.__doc__)
    print(ClassName.__doc__)
    print(ClassName.method4.__doc__)

2.4、SQL代码规范

SQL代码规范 继承 通用代码规范

  • 命名
    【强制】库、表、字段、视图:全小写,下划线分隔单词
    【强制】视图:view_作为前缀
    【强制】临时表:temp_作为前缀
    【强制】备份表:bak_作为前缀
    【建议】库名和表名不使用复数名词,正例user_info,负例users_infouser_informations
  • 建表
    【强制】创建 表和字段 要添加中文注释
    【强制】涉及外键关联的字段,须添加外键注释
    【强制】建表时,主键字段排在所有字段的第一行
  • 查询
    【建议】关键字:全大写
    【建议】使用别称时不要省略AS
    【强制】注释用-- (双减号+空格,MySQL和HIVE都支持)
    【建议】子查询用WITH AS,每个子查询都附带中文注释
-- 整个查询的注释
WITH
-- 子查询注释
t1 AS (
  SELECT a FROM t0
),
-- 子查询注释
t2 AS (
  SELECT a FROM t1
)
-- 查询注释
SELECT a FROM t2;
  • 换行和缩进
    【建议】缩进量:4空格或2空格
    【建议】单行过长时,按允许换行的关键字换行
【强制】关键字前后是否允许换行
AS不允许
SELECT允许
FROM允许不允许
[LEFT/RIGHT/...] JOIN允许不允许
ON不允许
WHERE允许不允许
GROUP BY允许
HAVING允许
ORDER BY允许
LIMIT允许不允许
SELECT t1.f1
      ,t2.f2
      ,t2.f3
FROM t1
LEFT JOIN t2 ON t1.f1=t2.f4
WHERE t2.f1>4
  AND t2.f2<5
  AND t2.f3<>1
ORDER BY t1.f1;
  • 【建议】代码头部添加 日期、需求、业务、作者 等信息
-- 名称:A9527
-- 所属业务:A
-- 需求文档:链接
-- 创建者:小基基
-- 创建日期: 2021-10-24
-- 修改日志(修改日期,修改人,修改内容):
-- 2021-12-12,小黄,添加xxx指标

2.4.1、MySQL代码规范(待完善)

MySQL代码规范 继承 SQL代码规范

  • 库、表、字段
    【建议】建库时显式指定字符集utf8utf8mb4
    示例:CREATE DATABASE db1 DEFAULT CHARACTER SET utf8mb4;

  • 表、字段
    【强制】非负数必须UNSIGNED
    【建议】主键pk_开头,唯一索引uq_开头,普通索引idx_开头
    【建议】建立组合索引,把区分度高的字段放在前面
    【强制】手机号存储不得使用数字,而使用VARCHAR(支持开头0及模糊查询)
    【建议】金额存储用INT,程序端乘以和除以100进行存取,因为INT占4字节,而DOUBLE占8字节
    【强制】高并发或分布式场景不允许外键约束

  • 【建议】字段允许适当 冗余,以减少联表来提高查询性能,但必须考虑数据一致性
    冗余字段应遵循:不是频繁修改的字段,不是text等较长字段

-- 表必备字段,这些字段起到似metadata的作用;在数据分析的时候,可用update_time作为数据抽取的增量标识
CREATE TABLE `xxx_info` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `delete_flag` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '逻辑删除标识:1=删除,0=未删',
  PRIMARY KEY (`id`) USING BTREE
) DEFAULT CHARSET=utf8mb4 COMMENT 'xxx信息表';

2.4.2、HIVE代码规范(待完善)

HIVE代码规范 继承 SQL代码规范

  • 命名
    【强制】自定义函数以udf_开头
    【强制】表名以分层名作为前缀,分层仅仅是逻辑上的区分,所有分层都在同一个库
分层命名规范说明
ODS
Operation Data Store
原始数据
ods+源类型+源表名+full/ifull:全量同步
i:增量同步
ods_postgresql_sku_full
ods_mysql_order_detail_i
ods_frontend_log
DIM
Dimension
合并维度
dim+维度+full/zipfull:全量表
zip:拉链表
日期维度表没有后缀
dim_sku_full
dim_user_zip
dim_date
DWD
Data Warehouse Detail
维度建模
dwd+事实+full/ifull:全量事实
i:增量事实
dwd_inventory_full
dwd_order_detail_i
DWS
Data Warehouse Service
聚合
dws+原子指标时间粒度有1d1h
1d:按1天
1h:按1小时
dws_page_visitor_1d
DWT
Data Warehouse Topic
累积
dwt_consumer
ADS
Application Data Store
最终指标
ads+衍生指标/派生指标
  • 建库
    【强制】建库必须加上注释
    【强制】建库不要添加LOCATION,而使用hive-site.xml中配置的默认值
    【建议】库按业务划分,不同库的表不会有关联(JOIN)
  • 建表
    【强制】普通表使用外部表(EXTERNAL_TABLE),临时表使用内部表(MANAGED_TABLE)
    【强制】创建内部表无需指定LOCATION
    【建议】ADS层使用\t,作为列分隔符,行存\n分隔,不压缩,方便Sqoop导出
  • 分区
    【强制】日期分区使用ymd,数据类型STRING,格式yyyy-MM-dd,例如2022-02-02
    【强制】月分区使用ym,数据类型STRING,格式yyyy-MM,例如2022-02
    【建议】小时分区使用(ymd,h)多级分区,数据类型STRING,格式(yyyy-MM-dd,HH),日期小时
    【建议】ADS层不分区
  • 查询
    【建议】慎用DISTINCT,性能较差;不过在高版本HIVE可能会被优化,具体还要看执行计划
    【建议】慎用多个OR,避免笛卡尔乘积,可用UNION ALL代替(前提是不影响逻辑,因为UNION ALL不去重)
    【建议】慎用ORDER BYORDER BY为全局排序,只有1个Reducer

2.5、其它

2.5.1、Java和Scala(待完善)

  • 【强制】文档注释/** */;多行注释/* */;单行注释//
  • 【强制】缩进量:2或4个空格
命名规范说明示例
变量、方法【强制】第一个单词全小写,后续单词首字母大写methodName
【强制】每个单词首字母大写ClassName
包、项目名【强制】全小写
常量【强制】全大写 和 下划线PUBLIC_CONSTANT

2.5.2JSON (待完善)

命名规范说明示例
【强制】第一个单词全小写,后续单词首字母大写cityId

2.5.3、Shell(待完善)

  • 【强制】脚本头:#!/usr/bin/sh
  • 【强制】注释方式:井号+空格,例如# 这是注释
  • 【建议】缩进量:4个空格
命名规范说明示例
变量、函数【强制】全小写,下划线分隔单词function_name
常量【强制】全大写 和 下划线PUBLIC_CONSTANT
脚本名【建议】全小写,下划线分隔单词sqoop_mysql2hive.sh

3、代码评审 自动化脚本

  • 功能:
    整体代码扫描、单个代码文件扫描

  • 使用方法:
    将(不含外部包)的代码 和 该代码扫描脚本 放到同级目录,使用Python3运行

代码扫描报告示例:

import os
import re
from collections import defaultdict
from pandas import DataFrame


class File:
    class Compile:
        @staticmethod
        def findall(string):
            return []

        @staticmethod
        def match(string):
            return not None

    # 文件名后缀
    SUFFIX = ''
    # 提取注释的正则表达式
    COMMENT_PATTERN = Compile
    # 合格代码的最低注释占比
    COMMENT_PROPORTION_THRESHOLD = 0
    # 代码头部模板
    HEAD_PATTERN = Compile
    # 单行代码最大长度
    LINE_LENGTH_LIMIT = 120
    # 不合规的代码语句
    UNQUALIFIED_CODE_PATTERN = Compile

    def __init__(self, file_name):
        self.file_name = file_name
        # 读取文件
        txt = self.read_file()
        # 字数
        self.number_of_words = len(txt)
        # 行数
        self.number_of_lines = len(txt.split('\n'))
        # 注释抽取
        comments = self.COMMENT_PATTERN.findall(txt)
        # 注释行数
        self.number_of_comments = sum(len(c.strip().split('\n')) for c in comments)
        # 注释个数的占比
        self.comment_proportion = self.number_of_comments / self.number_of_lines
        # 不及格原因
        self.reason_for_failings = []
        if self.comment_proportion < self.COMMENT_PROPORTION_THRESHOLD:
            self.reason_for_failings.append('注释太少')
        if self.HEAD_PATTERN.match(txt) is None:
            self.reason_for_failings.append('代码头部没有按照指定模板')
        if max(len(line) for line in txt.split('\n')) > self.LINE_LENGTH_LIMIT:
            self.reason_for_failings.append('单行代码过长')
        if self.UNQUALIFIED_CODE_PATTERN.findall(txt):
            self.reason_for_failings.append('含有不合格的代码语句')

    def read_file(self):
        with open(self.file_name, encoding='utf-8') as f:
            return f.read().strip()

    def report(self):
        print('文件名称', self.file_name)
        print('字数', self.number_of_words)
        print('行数', self.number_of_lines)
        print('注释行数', self.number_of_comments)
        print('注释行数占比', self.comment_proportion)
        print('不及格原因:' if self.reason_for_failings else '代码及格!')
        for e, r in enumerate(self.reason_for_failings, 1):
            print(e, r)


class PyFile(File):
    SUFFIX = '.py'
    COMMENT_PATTERN = re.compile(r'"""[\s\S]+?"""|# .+')
    COMMENT_PROPORTION_THRESHOLD = 0.05


class SqlFile(File):
    SUFFIX = '.sql'
    COMMENT_PATTERN = re.compile('-- .+')
    COMMENT_PROPORTION_THRESHOLD = 0.05
    UNQUALIFIED_CODE_PATTERN = re.compile(r'select\s+\*', re.I)


class JavaFile(File):
    SUFFIX = '.java'
    COMMENT_PATTERN = re.compile(r'/\*[\s\S]+?\*/|//.+')
    COMMENT_PROPORTION_THRESHOLD = 0.1


class ScalaFile(JavaFile):
    SUFFIX = '.scala'


class ShFile(File):
    SUFFIX = '.sh'
    COMMENT_PATTERN = re.compile('# .+')
    COMMENT_PROPORTION_THRESHOLD = 0.1
    HEAD_PATTERN = re.compile('#!/usr/bin/sh')


class Files:
    FILES = (PyFile, SqlFile, ScalaFile, JavaFile, ShFile)

    def __init__(self):
        self.statistic = {t.SUFFIX: [] for t in self.FILES}

    def traversal(self, path=os.path.dirname(__file__)):
        """递归遍历文件"""
        for file_name in os.listdir(path):
            abs_path = os.path.join(path, file_name)
            if os.path.isdir(abs_path):
                for p in self.traversal(abs_path):
                    yield p
            elif os.path.isfile(abs_path):
                yield abs_path

    def calculate(self):
        """计算 文件数、代码量、注释量…"""
        for abs_path in self.traversal():
            if abs_path == __file__:
                continue
            for f in self.FILES:
                if abs_path.endswith(f.SUFFIX):
                    self.statistic[f.SUFFIX].append(f(abs_path))
                    break

    @property
    def failed_codes(self):
        """不及格代码"""
        return (f.file_name for files in self.statistic.values() for f in files if f.reason_for_failings)

    def files_report(self):
        """分析全部代码"""
        self.calculate()
        df = defaultdict(list)
        index = []
        for suffix, files in self.statistic.items():
            index.append(suffix)
            df['文件数'].append(len(files))
            df['不及格文件数'].append(sum(1 if f.reason_for_failings else 0 for f in files))
            df['代码字数'].append(sum(f.number_of_words for f in files))
            df['代码行数'].append(sum(f.number_of_lines for f in files))
            df['注释行数'].append(sum(f.number_of_comments for f in files))
        df = DataFrame(df, index)
        df.loc['合计'] = df.sum()
        df['注释占比'] = df['注释行数'] / df['代码行数']
        df['不及格率'] = df['不及格文件数'] / df['文件数']
        print(df)
        # df.to_excel('代码扫描报告.xlsx')
        print('不及格代码:')
        for f in self.failed_codes:
            print(f)

    def single_file_report(self, the_file):
        """单个文件分析"""
        for f in self.FILES:
            if the_file.endswith(f.SUFFIX):
                f(the_file).report()
                break


if __name__ == '__main__':
    Files().files_report()
    Files().single_file_report(__file__)  # 检查自己
    # Files().single_file_report(r'')

4、数据逻辑校验机制

  • 数据开发不同于后端开发之处是:后端开发可是有测试妹子帮忙进行功能测试的噢~
    而数据开发工程师却没有😰😂😭

  • 数据逻辑错误不像功能bug那么明显,计算结果错误并不会使程序报错
    对此建立数据逻辑校验机制

  1. 主键重复值检验
  2. 主键NULL值检验
  3. 左联前后数量校验(联表后数据量=左表数据量)
  4. 时间数据类型校验,注意时区
  5. 度量值是否可加
  6. 数值类型校验,是否越界,是否损失精度
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小基基o_O

您的鼓励是我创作的巨大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值