ConfigParser と json の MIXでは、よい塩梅の味付けだ、と言ったけれども。
ConfigParser と json の MIXの本題は「書きやすく、読みやすいユーザ設定ファイル」であって、決して「JSON を使うこと」ではないわけで。
とりわけ実際に使うとなった場合によくやらかすのが、ついつい Python の癖でこう書いてしまうこと:
1 [build]
2
3 libraries: [
4 "advapi32",
5 "user32",
6 "shell32",
7 ]
最後のカンマが許容されるかされないかは言語によって違うし、同じ言語でも良い場合とダメな場合があったりもするけれど、json は常にこれを許容してくれない。
それが json なのだから、と、「jsonを使うこと」が目的なら別に気にならないのだけれど、「書きやすく、読みやすいユーザ設定ファイル」のためにはあまりよろしくない。
かといって、正規表現で「,]」「,}」を問答無用で置換してしまうのはさすがに気が引ける。(実は「使ってる場所では」今はそうしてるけど。)
なんかいい方法はないかなぁと思い、json モジュールの中をガン見してみたところ、parse_array 属性を差し替えてやれば対応出来そうだ、と思った。ただし、丸ごと差し替えるのは鬱陶しいので、「エラーになった場合だけ余分なカンマを置き換える」とすることにした。1時間ほどで出来た:
1 # -*- coding: utf-8 -*-
2 import json
3 import re
4 _RGX_EXPECT_OBJECT = re.compile(
5 r'Expecting object: line ([0-9]+) column ([0-9]+) \(char ([0-9]+)\)')
6
7
8 #
9 # a JsonArray parser which allows extra comma.
10 #
11 class ParseArrayProxy(object):
12 def __init__(self, orig_parser):
13 self.orig_parser = orig_parser
14
15 def __call__(
16 self,
17 s_and_end,
18 scan_once,
19 _w=json.decoder.WHITESPACE.match,
20 _ws=json.decoder.WHITESPACE_STR):
21
22 try:
23 return self.orig_parser(s_and_end, scan_once, _w, _ws)
24 except ValueError as e:
25 m = _RGX_EXPECT_OBJECT.match(str(e))
26 if not m:
27 raise
28 s, end = s_and_end
29 maybe_bracket = int(m.group(3))
30 if s[maybe_bracket] == ']':
31 for i in range(maybe_bracket - 1, end, -1):
32 if s[i].isspace():
33 continue
34 if s[i] == ',':
35 s = "%s %s" % (s[:i], s[i+1:])
36 return self.orig_parser(
37 (s, end), scan_once, _w, _ws)
38 raise
39 raise
40
41
42 #
43 # `json-like' decoder
44 #
45 class LooseJSONDecoder(json.JSONDecoder):
46 def __init__(self, encoding=None, object_hook=None, parse_float=None,
47 parse_int=None, parse_constant=None, strict=True,
48 object_pairs_hook=None):
49
50 json.JSONDecoder.__init__(
51 self,
52 encoding, object_hook, parse_float,
53 parse_int, parse_constant, strict,
54 object_pairs_hook)
55
56 orig = self.parse_array
57 self.parse_array = ParseArrayProxy(orig)
58 self.scan_once = json.scanner.py_make_scanner(self)
59
60
61 #
62 decoder2 = LooseJSONDecoder()
63 print(decoder2.decode("""
64 {"a": [1, 2, 3, ], "b": ["x", "y", "z",]}
65 """))
66
67 #decoder = json.JSONDecoder() # original
68 #print(decoder.decode("""
69 #{"a": [1, 2, 3, ], "b": ["x", "y", "z",]}
70 #""")) # raise ValueError
parse_object (JSONObject) も対応すれば、辞書の場合の余分なカンマにも対応出来るよ。
なお、実現上のいくつかのポイント:
json.scanner.py_make_scanner
は、json.scanner.make_scanner
ではダメです。pure Python バージョンを使わないと、決して「ワタシの」プロキシは呼び出されません。if s[i] == ',':
内で s の len が変わらないように注意な。つまりは、「スペースで差し替え」以外の方法はうまくいかんですよ