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

2019-05-17  本文已影响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的灵活性极大方便了配置的修改,且堆应用程序编写友好。比如:

2.问题生产

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

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

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"
            }
        ]
    }
}

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

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

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

至于开发语言选择熟悉的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对象,随意修改后再写回文件存储。不过,实际配置文件格式和内容提出了一些额外的要求:

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

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)

简单说明下:

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)方法,原理大致如下:

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

 
#  主逻辑
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"
            }
        ]
    }
}

假设需要进行修改如下:

那么需要编制如下的配置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元素无法处理,后续有空再处理吧(还不是因为懒~)

上一篇 下一篇

猜你喜欢

热点阅读