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.py
中ConfigParser
类中找到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指定
- 若无,说明它的子JSON对象才是需要替换的,递归处理,递归参数:子JSON对象和
为了方便使用,使用命令行参数的方式实现使用:
# 主逻辑
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元素无法处理,后续有空再处理吧(还不是因为懒~)
网友评论