从 python 中的字符串中提取罗马数字

Extract roman numerals from string in python

我有一个字符串列表,其中包含以拉丁数字表示的和弦,如下所示:

['ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7', 'ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7']

我想将这些字符串分成 3 个子列表,如下所示:

numerals = ['ii', 'vi', 'V', 'IV', 'I', 'V', 'IV', 'ii', 'vi', 'V', 'IV', 'I', 'V', 'IV']
chord_type=['min', 'min', 'maj', 'maj', 'maj', 'maj','maj', 'min', 'min', 'maj', 'maj', 'maj', 'maj','maj']
extentions=['7','7','', 'add9','add9','','7','7','7','','add9','add9','','7']

(可以看到,大写的罗马数字对应和弦类型的'maj',非大写的罗马数字对应'min'。)

在我的例子中所有可能的罗马数字:

i, ii, iii, iv, v, vi, vii, I, II, III, IV, V, VI, VII

请注意,我们不需要 MCLX

我知道我可以从 Python 中的字符串中的字母中提取或拆分数字,如 here 所述,但如何提取罗马数字?

我考虑过使用 match 正则表达式之类的东西,但我对如何定义这 7 个罗马数字感到困惑,因为这些字符可能会再次出现在字符串中。

如果罗马数字总是第一个那么你可能会这样做

import re
chords = ['ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7', 'ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7']
numerals = [re.match('[IiVv]+', i).group(0) for i in chords]
print(numerals)

输出

['ii', 'vi', 'V', 'IV', 'I', 'V', 'IV', 'ii', 'vi', 'V', 'IV', 'I', 'V', 'IV']

请注意,我按原样使用了 re.match 尝试在字符串的开头应用模式 并将数字限制为您示例中的现有数字(而不是使用众所周知,即 IiVvXxLlCcDdMm).

您可以像这样使用 .startswith 字符串方法非常简单地做到这一点:

nummerals = ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII']
lst = ['ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7', 'ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7']


nummerals.sort(key=len, reverse=True)  # see note 1

res = [next(n for n in nummerals if y.startswith(n)) for y in lst]  # see note 2
print(res)  # -> ['ii', 'vi', 'V', 'IV', 'I', 'V', 'IV', 'ii', 'vi', 'V', 'IV', 'I', 'V', 'IV']

备注

  1. 原始 nummerals 列表必须按长度(降序)排序,以确保匹配最大可能的数字(.startswith for 'ii7' 将同时匹配 'i''ii' 但你想要第二个)。
  2. 以上代码可能会引发 StopIteration 错误。如果你想防止这种情况发生,请为 next.
  3. 提供后备值

我的解决方案使用了一个有点复杂的正则表达式,它有两个优点:

  1. 如果一个数字的扩展看起来像一个不可能的罗马数字的一部分,例如 IV 然后 I,一个天真的方法会考虑数字 IVI 而我的方法只会考虑 IVI 作为扩展名。
  2. 如果您需要用更大的数字扩展您的应用程序,这将适用于非常大的数字。

编辑: 显然对于和弦来说,数字越大可能没用,但谁知道呢?也许你会更新音乐的工作方式

我使用的正则表达式来自here。我对它做了一点修改,让它在这里工作。

import re

l = ['ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7', 'ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7']

numerals = []
chord_type = []
extensions = []

roman_regex = '^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})'

for e in l:
    roman_search = re.search(roman_regex , e.upper())
    start = roman_search.start()
    end = roman_search.end()
    roman = e[start:end]

    numerals.append(roman)
    chord_type.append('maj' if roman[0].upper() == roman[0] else 'min')
    extensions.append(e[end:])
>>> print(numerals)
... print(chord_type)
... print(extensions)

['ii', 'vi', 'V', 'IV', 'I', 'V', 'IV', 'ii', 'vi', 'V', 'IV', 'I', 'V', 'IV']
['min', 'min', 'maj', 'maj', 'maj', 'maj', 'maj', 'min', 'min', 'maj', 'maj', 'maj', 'maj', 'maj']
['7', '7', '', 'add9', 'add9', '', 'maj7', '7', '7', '', 'add9', 'add9', '', 'maj7']

你可以试试这个:

import re

matcher = re.compile(r'([IiVv]+)(min|maj|)(.*)')

def parse_string(s):
    gs = matcher.findall(s)[0]
    if gs[1] == '':
        gs = (gs[0], 'maj' if gs[0].isupper() else 'min', gs[2])
    return gs
    
def parse_array(A):
    return [parse_string(chord) for chord in A]
    
parsed = parse_array(['ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7', 'ii7', 'vi7', 'V', 'IVadd9', 'Iadd9', 'V', 'IVmaj7'])

numerals, chord_type, extensions = zip(*parsed)

print(list(numerals))
print(list(chord_type))
print(list(extensions))

我使用re进行正则解析,肯定没问题。