Я пытаюсь разобрать кубические файлы, т.е. примерно так:
Cube file format
Generated by MRChem
1 -1.500000e+01 -1.500000e+01 -1.500000e+01 1
10 3.333333e+00 0.000000e+00 0.000000e+00
10 0.000000e+00 3.333333e+00 0.000000e+00
10 0.000000e+00 0.000000e+00 3.333333e+00
2 2.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
4.9345e-14 3.5148e-13 1.5150e-12 3.8095e-12 6.1568e-12 6.1568e-12
3.8095e-12 1.5150e-12 3.5148e-13 4.9344e-14 3.5148e-13 2.3779e-12
1.0450e-11 3.0272e-11 5.4810e-11 5.4810e-11 3.0272e-11 1.0450e-11
Мой текущий анализатор выглядит следующим образом:
import pyparsing as pp
# define simplest bits
int_t = pp.pyparsing_common.signed_integer
float_t = pp.pyparsing_common.sci_real
str_t = pp.Word(pp.alphanums)
# comments: the first two lines of the file
comment_t = pp.OneOrMore(str_t, stopOn=int_t)("comment")
# preamble: cube axes and molecular geometry
preamble_t = ((int_t + pp.OneOrMore(float_t) + int_t) \
+ (int_t + float_t + float_t + float_t) \
+ (int_t + float_t + float_t + float_t) \
+ (int_t + float_t + float_t + float_t) \
+ (int_t + float_t + float_t + float_t + float_t))("preamble")
# voxel data: volumetric data on cubic grid
voxel_t = pp.delimitedList(float_t, delim=pp.Empty())("voxels")
# the whole parser
cube_t = comment_t + preamble_t + voxel_t
Приведенный выше код работает, но можно ли его улучшить? Особенно мне кажется, что определение preamble_t
можно было бы сделать более элегантно. Однако мне это не удалось: мои попытки до сих пор приводили только к неработающим парсерам.
ОБНОВЛЕНИЕ
Следуя ответу и дальнейшему предложению по прокатке моего собственного countedArray
, вот что у меня есть сейчас:
import pyparsing as pp
int_t = pp.pyparsing_common.signed_integer
nonzero_uint_t = pp.Word("123456789", pp.nums).setParseAction(pp.pyparsing_common.convertToInteger)
nonzero_int_t = pp.Word("+-123456789", pp.nums).setParseAction(lambda t: abs(int(t[0])))
float_t = pp.pyparsing_common.sci_real
str_t = pp.Word(pp.printables)
coords = pp.Group(float_t * 3)
axis_spec = pp.Group(int_t("nvoxels") + coords("vector"))
geom_field = pp.Group(int_t("atomic_number") + float_t("charge") + coords("position"))
def axis_spec_t(d):
return pp.Group(nonzero_uint_t("n_voxels") + coords("vector"))(f"{d.upper()}AXIS")
geom_field_t = pp.Group(nonzero_uint_t("ATOMIC_NUMBER") + float_t("CHARGE") + coords("POSITION"))
before = pp.Group(float_t * 3)("ORIGIN") + pp.Optional(nonzero_uint_t, default=1)("NVAL") + axis_spec_t("x") + axis_spec_t("y") + axis_spec_t("z")
after = pp.Optional(pp.countedArray(pp.pyparsing_common.integer))("DSET_IDS").setParseAction(lambda t: t[0] if len(t) !=0 else t)
def preamble_t(pre, post):
preamble_expr = pp.Forward()
def count(s, l, t):
n = t[0]
preamble_expr << (n and (pre + pp.Group(pp.And([geom_field_t]*n))("GEOM") + post) or pp.Group(empty))
return []
natoms_expr = nonzero_int_t("NATOMS")
natoms_expr.addParseAction(count, callDuringTry=True)
return natoms_expr + preamble_expr
w_nval = ["""3 -5.744767 -5.744767 -5.744767 1
80 0.143619 0.000000 0.000000
80 0.000000 0.143619 0.000000
80 0.000000 0.000000 0.143619
8 8.000000 0.000000 0.000000 0.000000
1 1.000000 0.000000 1.400000 1.100000
1 1.000000 0.000000 -1.400000 1.100000
2.21546E-05 2.47752E-05 2.76279E-05 3.07225E-05 3.40678E-05 3.76713E-05
4.15391E-05 4.56756E-05 5.00834E-05 5.47629E-05 5.97121E-05 6.49267E-05
7.03997E-05 7.61211E-05 8.20782E-05 8.82551E-05 9.46330E-05 1.01190E-04
1.07900E-04 1.14736E-04 1.21667E-04 1.28660E-04 1.35677E-04 1.42680E-04
1.49629E-04 1.56482E-04 1.63195E-04 1.69724E-04 1.76025E-04 1.82053E-04
1.87763E-04 1.93114E-04 1.98062E-04 2.02570E-04 2.06601E-04 2.10120E-04
""", """-3 -12.368781 -12.368781 -12.143417 92
80 0.313134 0.000000 0.000000
80 0.000000 0.313134 0.000000
80 0.000000 0.000000 0.313134
8 8.000000 0.000000 0.000000 0.225363
1 1.000000 0.000000 1.446453 -0.901454
1 1.000000 -0.000000 -1.446453 -0.901454
92 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92
-1.00968E-10 -3.12856E-09 3.43398E-09 -8.36581E-09 -3.70577E-14 9.20035E-07
-3.78355E-06 -2.09418E-06 -9.41686E-13 -1.21366E-06 -4.87958E-06 3.50133E-06
-5.61999E-07 3.54869E-18 -1.30008E-12 -9.48885E-07 -1.44839E-06 -1.68959E-06
-3.21975E-06 -2.48399E-06 -5.12012E-07 -1.60147E-07 -9.88842E-13 -3.77732E-18
"""
]
for test in w_nval:
res = preamble_t(before, after).parseString(test).asDict()
print(f"{res=}")
wo_nval = ["""-3 -12.368781 -12.368781 -12.143417
80 0.313134 0.000000 0.000000
80 0.000000 0.313134 0.000000
80 0.000000 0.000000 0.313134
8 8.000000 0.000000 0.000000 0.225363
1 1.000000 0.000000 1.446453 -0.901454
1 1.000000 -0.000000 -1.446453 -0.901454
92 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92
-1.00968E-10 -3.12856E-09 3.43398E-09 -8.36581E-09 -3.70577E-14 9.20035E-07
-3.78355E-06 -2.09418E-06 -9.41686E-13 -1.21366E-06 -4.87958E-06 3.50133E-06
-5.61999E-07 3.54869E-18 -1.30008E-12 -9.48885E-07 -1.44839E-06 -1.68959E-06
-3.21975E-06 -2.48399E-06 -5.12012E-07 -1.60147E-07 -9.88842E-13 -3.77732E-18
""",
"""3 -5.744767 -5.744767 -5.744767
80 0.143619 0.000000 0.000000
80 0.000000 0.143619 0.000000
80 0.000000 0.000000 0.143619
8 8.000000 0.000000 0.000000 0.000000
1 1.000000 0.000000 1.400000 1.100000
1 1.000000 0.000000 -1.400000 1.100000
2.21546E-05 2.47752E-05 2.76279E-05 3.07225E-05 3.40678E-05 3.76713E-05
4.15391E-05 4.56756E-05 5.00834E-05 5.47629E-05 5.97121E-05 6.49267E-05
7.03997E-05 7.61211E-05 8.20782E-05 8.82551E-05 9.46330E-05 1.01190E-04
1.07900E-04 1.14736E-04 1.21667E-04 1.28660E-04 1.35677E-04 1.42680E-04
1.49629E-04 1.56482E-04 1.63195E-04 1.69724E-04 1.76025E-04 1.82053E-04
1.87763E-04 1.93114E-04 1.98062E-04 2.02570E-04 2.06601E-04 2.10120E-04
"""]
for test in wo_nval:
res = preamble_t(before, after).parseString(test).asDict()
print(f"{res=}")
Это работает для тестовых случаев w_nval
(где присутствует токен NVAL
). Этот токен, однако, является необязательным: синтаксический анализ тестовых случаев wo_nval
завершается ошибкой, хотя я использую Optional
токен. Кроме того, токен NATOMS
не сохраняется в конечном словаре. Есть ли способ также сохранить счетчик в реализации countedArray
?
ОБНОВЛЕНИЕ 2
Это окончательный рабочий синтаксический анализатор:
import pyparsing as pp
# non-zero unsigned integer
nonzero_uint_t = pp.Word("123456789", pp.nums).setParseAction(pp.pyparsing_common.convertToInteger)
# non-zero signed integer
nonzero_int_t = pp.Word("+-123456789", pp.nums).setParseAction(lambda t: abs(int(t[0])))
# floating point numbers, can be in scientific notation
float_t = pp.pyparsing_common.sci_real
# NVAL token
nval_t = pp.Optional(~pp.LineEnd() + nonzero_uint_t, default=1)("NVAL")
# Cartesian coordinates
# it could be alternatively defined as: coords = pp.Group(float_t("x") + float_t("y") + float_t("z"))
coords = pp.Group(float_t * 3)
# row with molecular geometry
geom_field_t = pp.Group(nonzero_uint_t("ATOMIC_NUMBER") + float_t("CHARGE") + coords("POSITION"))
# volumetric data
voxel_t = pp.delimitedList(float_t, delim=pp.Empty())("DATA")
# specification of cube axes
def axis_spec_t(d):
return pp.Group(nonzero_uint_t("NVOXELS") + coords("VECTOR"))(f"{d.upper()}AXIS")
before_t = pp.Group(float_t * 3)("ORIGIN") + nval_t + axis_spec_t("X") + axis_spec_t("Y") + axis_spec_t("Z")
# the parse action flattens the list
after_t = pp.Optional(pp.countedArray(pp.pyparsing_common.integer))("DSET_IDS").setParseAction(lambda t: t[0] if len(t) != 0 else t)
def preamble_t(pre, post):
expr = pp.Forward()
def count(s, l, t):
n = t[0]
expr << (geom_field_t * n)("GEOM")
return n
natoms_t = nonzero_int_t("NATOMS")
natoms_t.addParseAction(count, callDuringTry=True)
return natoms_t + pre + expr + post
cube_t = preamble_t(before_t, after_t) + voxel_t
Замечательно! Гораздо чище, чем моя первая попытка. Я посмотрел на
countedArray
, как вы предложили, но все же есть некоторые острые углы: 1. Количество атомов в измененномcountedArray
не сохраняется в окончательном словаре. Я не совсем понимаю, как этого добиться. 2. ТокенNVAL
является необязательным, его значение по умолчанию равно 1. Я пытался использоватьOptional
, но без особого успеха. Я обновил вопрос с моим обновленным кодом и тестовыми образцами.Вам нужно будет заглянуть за специфику
countedArray
и следовать его логике. Определите часть, которая будет различаться по длине, какForward()
, а затем в действии синтаксического анализа для поля счетчика вставьтеcount*expr
в этотForward()
. Поскольку 1 используется по умолчанию, если счетчик отсутствует, вы можете вставитьexpr
вForward()
после его создания.Если
nval
является необязательным, вы должны быть осторожны, чтобы случайно не прочитать первое целое число следующей строки на своем месте («80» в ваших примерах). Используйте отрицательный просмотр вперед, напримерpp.Optional(~pp.LineEnd() + int_t, default=1)
, чтобы вы не читали дальше конца строки, чтобы получить nval.Спасибо за помощь! Теперь все работает, и я опубликовал полный окончательный код.