Плохая идея регекспами такое парсить. Лучше воспользйтесь lxml или любым таким парсером.
Проблема в том, что у вас внтури такого тега может (теоретически) быть непредсказуемая вложенность других тегов. Рекурсивные и контекстные вещи регекспами делаются очень неудобно.
Разбейте весь текст запроса на лексемы, например так:
re.split('<|>')
И вы получите сисок, где нулевой и все четные элементы - это фрагменты текста, а все нечетные по индексу элементы - это содержимое тегов. Содержимое закрывающих тегов можно распозать по слешу.
Дальше нужно запрограммировать конечный автомат с двумя состояниями, которому можно скормить этот список, а вернёт он такой же список, но отфильтрует ненужные элементы.
Грубо говоря, в первом состоянии вы перебираете входной список и когда встречаете нечетный эелемент (тег), начинающийся со слова span и содержащий атрибуты, сбрасываете счетчик в ноль и переходите во творое состояние.
Во втором - перебираете се элементы и инкрементируете счетчик каждый раз когда попадается открывающий тег, и декрементируете когда попадается закрывающий (нечетный элемент, начинающийся начинается со слеша). Если счетчик снова стал нулём, переходите в первое состояние.
На выход следует пропускать только элементы находясь в первом состоянии. Второе состояние подавляет выхлоп.
def f(lexems):
state, deep = 0, 0
for i, lex in enumerate(lexems):
if state == 0:
if i%2 and lex.startswith('span '):
state = 1
deep = 1
else:
yield f'<{lex}>' if i%2 else lex
else:
if i%2:
deep += -1 if lex.startswith('/') else 1
if deep == 0:
state = 0