美文网首页
配置文件(JSON格式)替换工具

配置文件(JSON格式)替换工具

作者: 西瓜雪梨桔子汁 | 来源:发表于2019-05-17 18:20 被阅读0次

    1.问题背景

    为了支持灵活的配置:配置项可随时按需增减,我们应用的配置文件采用了JSON格式的配置文件。其整体是一个JSON对象,内部按照key分设各个服务:前置、核心、入库、校验等的子JSON对象,子JSON对象内部按照服务自身需要配置:可以是JSON Object、也可以是JSON Array。大致如下:

    # 注释行,将被忽略
    ####################################################################
    # 配置文件样例
    ####################################################################
    {
        "completion_to_fileCore_dir": "/completion_to_fileCore/#{busiLine}/#{settleDate}", 
        "redis_key_expire_time": "100", 
        ... ....
        "CsvFileFrontToCoreService": [ ... ],
        "CsvFileCoreToFrontService": [ ... ],
        "CsvFileCoreService": {
            "CsvFileCoreToValidatorService": [ ... ],
            "CsvFileExportToCoreService": [ ... ],
        },
        "ClearingRedisService": { ... },
        ... ....
    }
    

    JSON的灵活性极大方便了配置的修改,且堆应用程序编写友好。比如:

    • 对于1个或多个的配置项,直接使用JSON Array,应用程序直接遍历处理。
    • 对配置项某些情景有、某些情景无,应用程序直接检查JSON Object是否含有这个配置key,据此按有无配置项的策略处理即可,比如设默认值等等。

    2.问题生产

    方便开发的情况下,对运维和测试的修改就不是很友好,主要表现在:

    • 不熟悉配置文件情况下需要整个配置文件找待修改配置项
    • JSON的逗号、反括弧等容易修改错误

    这就加剧了人工修改导致错误的可能,切效率低下。在生产、开发、测试环境差异巨大且各方没有建设集中配置环境中心的情况下,寻找自动化替换配置、避免低效且易错的手工修改配置方案就显得很重要。

    3.基于INI文件的JSON格式配置文件自动化替换

    3.1 思路

    受爬虫解析HTML使用xpath的启发,可以把JSON对象也抽象为这种path形式。下面以一个JSON实例说明:

    {
        "commonProduct":{
            "name":"普通商品汇总",
            "productList":[
                {
                    "productId":"commonProduct_001",
                    "productName":"矿泉水",
                    "productPrice":"2.00"
                },
                {
                    "productId":"commonProduct_002",
                    "productName":"冰可乐",
                    "productPrice":"3.50"
                }
            ]
        },
        "specialityProduct":{
            "name":"特色商品汇总",
            "productList":[
                {
                    "productId":"pecialityProduct_001",
                    "productName":"椰子糖",
                    "productPrice":"30.00"
                },
                {
                    "productId":"pecialityProduct_002",
                    "productName":"芒果干",
                    "productPrice":"35.00"
                }
            ]
        }
    }
    

    各个属性可以按这样的定义:

    • JSON对象:
    commonProduct.name:代表取值“commonProduct”这个JSON 对象的“name”属性
    specialityProduct.productList:代表取值“specialityProduct”这个JSON 对象的“productList”属性
    
    • JSON数组:
    commonProduct.productList#0:代表取值“commonProduct”这个JSON 对象的“productList”这个JSON Array的第一个属性,即"矿泉水"那个属性
    specialityProduct.productList#1.productName.:同理,代表取值“specialityProduct”这个JSON 对象的“productList”属性,即JSON Array第2个的"productName"属性,取值为"芒果干"
    

    定义好每个属性的获取规则,替换过程就比较容易,思路是:

    • 解析INI文件
    • 读取JSON文件
    • 按照INI文件的path逐个属性进行替换
    • 替换后的JSON写入文件

    至于开发语言选择熟悉的Python,只需使用基础库就能完成这个功能,不必考虑生产环境包安装等其它问题。

    3.2 解析INI文件

    解析INI文件是python自带的包ConfigParser就足够了,为了方便操作,将操作方法封装到一个类中,代码如下:

    import ConfigParser
    
    class ConfigParserWithoutChangeOpions(ConfigParser.ConfigParser):
        '''
        继承ConfigParser.ConfigParser,重写optionxform()方法,无需将option的值改为小写
        '''
        def __init__(self,defaults=None):
            ConfigParser.ConfigParser.__init__(self,defaults=None)
            
        def optionxform(self, optionstr):
            '''
            复写父类optionxform方法,不改变key值大小
            '''
            return optionstr
    
    class IniConfigParser(object):
        '''
        INI文件解析
        '''
        def __init__(self, path):
            '''
            初始化方法
            '''
            self.path = path
            # self.conf = ConfigParser.ConfigParser()
            self.conf = ConfigParserWithoutChangeOpions()
            self.conf.read(path)    
    
        def get_sections(self):
            '''
            获取所有的section节点
            '''
            return self.conf.sections() 
    
        def get_section_options(self,section):
            '''
            获取section节点的所有key
            '''
            return self.conf.options(section)
    
        def get_section_items(self,section):
            '''
            获取section节点的所有属性
            '''
            return self.conf.items(section)
    
        def get_section_item(self, section, option):
            '''
            获取section节点的指定属性
            '''
            return self.conf.get(section, option)
    
        def exist_section(self, section):
            '''
            检查section是否存在
            '''
            return self.conf.has_section(section)
    
        def exist_option(self, section, option):
            '''
            检查option是否存在
            '''
            return self.conf.has_option(section, option)
    

    只是简单封装了下提供额外的获取INI文件内部配置块的一些方法,唯一特别的是继承了ConfigParser,重写了其optionxform()方法。
    在源码:/usr/lib64/python2.7/ConfigParser.pyConfigParser类中找到optionxform()方法实现:

    def optionxform(self, optionstr):
            return optionstr.lower()
    

    源码中将INI的key全部转为小写,这意味着如果配置:notifyUrl=http://test.url,那默认的ConfigParser解析后会变成:'notifyurl=http://test.url',这回严重影响后续JSON替换,因为JSON的key存在大小写区分,不能随意更改key的大小写,这回影响应用程序解析配置。
    通过继承ConfigParser.ConfigParser类重写optionxform()方法,不会将optionstr转为小写避开这个问题。

    3.3 JSON文件读写

    python自带json模块能够很容易将json字符串转为dict对象,随意修改后再写回文件存储。不过,实际配置文件格式和内容提出了一些额外的要求:

    • 读文件转为字典对象,要求能够区分原始文件#号开头的注释和实际配置内容
    • 读取配置内容(JSON对象)必须能够与原始配置文件顺序一致,否则写入文件后顺序会与读取顺序不一致

    为此,封装了更加适合配置替换修改的类:

    import json
    import collections   
    
    class JsonConfigParser(object):
        '''
        JSON文件读取解析
        '''
        def __init__(self):
            '''
            初始化方法
            '''
            self.comments = ''
            
        def read(self, path):
            '''
            读取json
            '''
            self.path = path
            conf = ''
            with open(path, 'rb') as f:
                for line in f.readlines():
                    # 剔除json文件已#开头的注释行
                    if(not line.startswith('#')):
                        conf += line
                    else:
                        self.comments += line
            # 解析成json:使用collections.OrderedDict保证读取顺序和文件顺序一致
            conf_json = json.loads(conf, object_pairs_hook=collections.OrderedDict)
            # 返回配置json
            return conf_json
            
        def write(self, path, conf):
            '''
            写入json
            '''
            # 如果路径不存在,尝试创建父路径
            print conf
            dir = path[0:path.rfind('/')]
            if(not os.path.exists(dir)):
                print(u'路径:%s不存在, 开始创建...' % dir)
                os.makedirs(dir)
            # 写入文件
            with open(path, "wb") as f:
                # 先写入注释
                f.write(self.comments)
                # indent默认为None,小于0为零个空格,格式化保存字典;默认ensure_ascii = True,即非ASCII字符被转化为`\uXXXX`
                f.write(json.dumps(conf, indent=4, ensure_ascii=False)) 
                # f.write(unicode(json.dumps(conf, indent=4), "utf-8")) 
            print(u'修改后配置写入文件:%s完成!' % path)
    

    简单说明下:

    • 读取配置文件方法时,会检查每行行首是否为#号(文件都是utf-8编码),如果是将它们作为注释暂存起来
    • 对读取到的JSON字符串内容,使用json.loads(conf, object_pairs_hook=collections.OrderedDict)确保读取转换后的dict顺序与文件内容一致的,collections包的OrderedDict能够确保这样的顺序,由此确保后续修改完成写入文件时还能保持原本配置文件的顺序
    • 读写文件的文件路径参数都是全路径,写文件时限检测父目录是否存在,不存在会先创建目录(没有做IO异常之类判断,还不是因为懒...)
    • 写文件时先把读取时缓存的注释写入文件,再写入修改好的dict对象,利用json.dumps(conf, indent=4, ensure_ascii=False)有2点考虑:
      • indent=4使得父子两级之间缩进增加4个空格位
      • ensure_ascii=False使得非ASCII能够不被转义成Unicode写入文件,比如中文,否则像中文“中国”写入文件会变成“\u4e2d\u56fd”

    4.修改功能实现

    如前所述,修改功能就是遍历INI文件指定section的配置项,其key为类似xpath的表述形式,按它找到读取的JSON文件找到带替换的key,将值替换掉。

    class ContentModifier(object):
        '''
        配置内容修改器,依据配置项的key-val对,进行配置文件的修改
        '''
        def __init__(self, conf_paraser):
            '''
            初始化方法
            '''
            self.conf_paraser = conf_paraser       
            
        def json_replace_conf(self, conf, key, val):
            '''
            对json的指定key替换值
            ''' 
            if(not conf.has_key(key)):
                print(u'未找到key为:%s的选项,原始值为:%s' % (key, conf))
            # 替换值
            conf[key] = val
            # 返回
            return conf
                    
        def json_replace_recursive(self, conf, key_pattern, val):
            '''
            按照key_pattern递归到最后一层,将其值修改为传入的val
            以CsvFileExportToCoreService#0.exportRules#0.fileExportRules.rule为例,表示:
                待修改的值在一级keyCsvFileExportToCoreService的值中,且它是array,#0指明要修改的在array的第一个
                待修改的值在第一个array的key为exportRules中,这个exportRules的值也是array,#0需要修改的指明要修改的在array的第一个
                待修改的值在第一个array的fileExportRules指定值中,此为json对象
                待修改的值在json对象的rule中
            '''
            print '-------%s : %s' % (key_pattern, val)
            if(len(key_pattern.split('.')) == 1):
                if(not '#' in key_pattern):
                    return self.json_replace_conf(conf, key_pattern, val)     
                else:
                   real_key = key_pattern.split('#')[0]
                   index = key_pattern.split('#')[1]
                   conf_arrary = conf[real_key]
                   # print conf_arrary
                   conf_arrary[int(index)] = val
                   conf[real_key] = conf_arrary
                   return conf
            else:
                key = key_pattern.split('.')[0]
                if '#' in key:
                    # 剔除#index拿到key
                    real_key = key.split('#')[0]
                    # 从#index拿到array的index
                    index = key.split('#')[1]
                    # 先取的array,在从array中按照index取出需要的
                    conf_arrary = conf[real_key]
                    real_conf = conf_arrary[int(index)]
                    # 对待替换的配置继续递归处理
                    # print '========== ' + key_pattern[key_pattern.index('.')+1:]
                    replaced_conf = self.json_replace_recursive(real_conf, key_pattern[key_pattern.index('.')+1:], val)
                    # 修改好的值替换掉原本的这个index的array中的值
                    conf_arrary[int(index)] = replaced_conf
                    # 再将这个array赋值回原本json的这个key的部分,达到改变配置效果
                    conf[real_key] = conf_arrary
                    # 返回调用者的是对原始json替换后的
                    return conf
                else:
                    # 不是array类型,直接取出值进行递归替换
                    # print '========== ' + key_pattern[key_pattern.index('.')+1:]
                    replaced_conf = self.json_replace_recursive(conf[key], key_pattern[key_pattern.index('.')+1:], val)
                    # 修改好的json替换原始json
                    conf[key] = replaced_conf
                    # 返回替换后的原始json
                    return conf
                
        def json_modify(self, section, content):
            '''
            按照配置conf,取出其section段配置,对content进行修改
            '''
            #print content
            replaced_json = content
            if(not self.conf_paraser.exist_section(section)):
                raise RuntimeError(u'配置文件:%s没有section名为:%s的配置' % (self.conf_paraser.path, section))
            else:
                items = self.conf_paraser.get_section_items(section)
                # 替换所有需要的项
                for item in items:
                    print '%s : %s' % (item[0], item[1])
                    replaced_json = self.json_replace_recursive(replaced_json, item[0], item[1])
            # 返回修改好的配置json
            return replaced_json
    

    json_modify(self, section, content)是供外部调用方法,参数section指定INI文件的配置块,参数content需要替换的JSON/dict对象。
    核心实现是json_replace_recursive(self, conf, key_pattern, val)方法,原理大致如下:

    • 检测key_pattern没有"."分隔符,则有2种可能:
      • 含有"#"分隔符,说明该JSON key的值为一个JSON Array,#号后的数字就是需要替换的JSON Array编号
      • 不含有"#"分隔符,说明其值就是需要替换的,直接将传入val替换原本的值
    • 检测key_pattern含有"."分隔符,取key_pattern按照"."分隔的第一个值,判断其是否含有"#"也有2种可能:
      • 若无,说明它的子JSON对象才是需要替换的,递归处理,递归参数:子JSON对象和key_pattern第一个"."之后的字符串为子key
      • 如有,说明待替换的目标在key_pattern按照"."分隔的第一个值除去#index指向的JSON Array中,具体是哪个Array由#好后面的index指定

    为了方便使用,使用命令行参数的方式实现使用:

     
    #  主逻辑
    if __name__ == '__main__': 
        tips = u'使用方法:%s conf.ini 0091-account.config' % sys.argv[0]
        print "\r\n┌─" + (len(tips) - 4)*"-" + "─┐"
        print "│"+2*" " + tips +1*" " +"│"
        print "└─" + (len(tips) - 4)*"-" + "─┘\r\n"    
        
        # 参数校验
        print sys.argv
        if(len(sys.argv) != 3):
            print u'参数个数错误,请检查!使用例子:%s' % tips
            print ""
            exit(1)
            
        
        # 装配目录:默认都是当前文件夹下
        ini_path = os.path.join(os.getcwd(),sys.argv[1])
        conf_path = os.path.join(os.getcwd(),sys.argv[2])
        # 初始化解析器
        paraser = IniConfigParser(ini_path)
        # 初始化json解析器,并读取json配置文件
        jsonparaser = JsonConfigParser()
        jsondata  = jsonparaser.read(conf_path)
        #初始化修改器
        modifier = ContentModifier(paraser)
        # 按照指定ini文件section部分进行替换
        replaced_json = modifier.json_modify(sys.argv[2], jsondata)
        # 替换后结果写入文件
        jsonparaser.write(os.path.join(os.getcwd(),'reslut',sys.argv[2]), replaced_json)
        
    

    5.使用例子

    假设我们有如下的JSON格式文件data.config,其内容如下:

    # 注释行,将被忽略
    ####################################################################
    ##   注释
    ####################################################################
    {
        "commonProduct":{
            "name":"普通商品汇总",
            "productList":[
                {
                    "productId":"commonProduct_001",
                    "productName":"矿泉水",
                    "productPrice":"2.00"
                },
                {
                    "productId":"commonProduct_002",
                    "productName":"冰可乐",
                    "productPrice":"3.50"
                }
            ]
        },
        "specialityProduct":{
            "name":"特色商品汇总",
            "productList":[
                {
                    "productId":"specialityProduct_001",
                    "productName":"椰子糖",
                    "productPrice":"30.00"
                },
                {
                    "productId":"specialityProduct_002",
                    "productName":"芒果干",
                    "productPrice":"35.00"
                }
            ]
        }
    }
    

    假设需要进行修改如下:

    • 矿泉水价格改为3.00
    • 冰可乐价格改为2.50
    • 椰子糖的productId改为modifiedSpecialityProduct_001
      -椰子糖名称改为椰子糖(无糖型)

    那么需要编制如下的配置INI文件:

    [data.config]
    ;price
    commonProduct.productList#0.productPrice=3.00
    commonProduct.productList#1.productPrice=2.50
    
    ;id
    specialityProduct.productList#0.productId=modifiedSpecialityProduct_001
    ;name
    specialityProduct.productList#0.productName=椰子糖(无糖型)
    

    注意中括弧中section名称为data.config,与需要修改的JSON文件保持一致。
    执行如下命令:

    ./modify.py conf.ini data.config
    
    20190517181601.jpg

    可以得到最终结果:


    20190517181733.png

    所有指定的价格、名称均已替换完成,达到了预期的效果。

    6.总结

    暂时不支持JSON内容的增加和减少,比如一个Array含有3个元素,其实只需要1个元素,那只会让1个元素替换修改,其它Array元素无法处理,后续有空再处理吧(还不是因为懒~)

    相关文章

      网友评论

          本文标题:配置文件(JSON格式)替换工具

          本文链接:https://www.haomeiwen.com/subject/huogaqtx.html