Expressão regular que corresponde a um bloco de texto com várias linhas

105

Estou tendo problemas para fazer um regex Python funcionar ao comparar com texto que se estende por várias linhas. O texto de exemplo é ('\ n' é uma nova linha)

some Varying TEXT\n
\n
DSJFKDAFJKDAFJDSAKFJADSFLKDLAFKDSAF\n
[more of the above, ending with a newline]\n
[yep, there is a variable number of lines here]\n
\n
(repeat the above a few hundred times).

Eu gostaria de capturar duas coisas: a parte 'some_Varying_TEXT' e todas as linhas de texto em maiúsculas que vêm duas linhas abaixo dele em uma captura (posso retirar os caracteres de nova linha mais tarde). Eu tentei com algumas abordagens:

re.compile(r"^>(\w+)$$([.$]+)^$", re.MULTILINE) # try to capture both parts
re.compile(r"(^[^>][\w\s]+)$", re.MULTILINE|re.DOTALL) # just textlines

e muitas variações disso sem sorte. O último parece corresponder às linhas do texto uma a uma, o que não é o que eu realmente quero. Consigo captar a primeira parte, sem problemas, mas não consigo captar as 4-5 linhas de texto em maiúsculas. Eu gostaria que match.group (1) fosse some_Varying_Text e group (2) fosse line1 + line2 + line3 + etc até que a linha vazia fosse encontrada.

Se alguém estiver curioso, é suposto ser uma sequência de aminoácidos que constituem uma proteína.

Jan
fonte
Existe algo mais no arquivo além da primeira linha e do texto em maiúsculas? Não sei por que você usaria um regex em vez de dividir todo o texto em caracteres de nova linha e tomar o primeiro elemento como "some_Varying_TEXT".
UncleZeiv
2
sim, regex é a ferramenta errada para isso.
Seu texto de amostra não possui um >caractere principal . Deveria?
MiniQuark

Respostas:

114

Experimente isto:

re.compile(r"^(.+)\n((?:\n.+)+)", re.MULTILINE)

Acho que seu maior problema é que você espera que as âncoras ^e se $igualem aos avanços de linha, mas não são. No modo multilinha, ^corresponde à posição imediatamente após uma nova linha e $corresponde à posição imediatamente anterior a uma nova linha.

Esteja ciente, também, que uma nova linha pode consistir em um avanço de linha (\ n), um retorno de carro (\ r) ou um retorno de carro + avanço de linha (\ r \ n). Se você não tem certeza de que seu texto de destino usa apenas alimentações de linha, você deve usar esta versão mais abrangente do regex:

re.compile(r"^(.+)(?:\n|\r\n?)((?:(?:\n|\r\n?).+)+)", re.MULTILINE)

BTW, você não deseja usar o modificador DOTALL aqui; você está contando com o fato de que o ponto corresponde a tudo, exceto às novas linhas.

Alan Moore
fonte
Você pode querer substituir o segundo ponto na regex por [AZ] se não quiser que essa expressão regular corresponda a praticamente qualquer arquivo de texto com uma segunda linha vazia. ;-)
MiniQuark
Minha impressão é que os arquivos de destino estarão em conformidade com um padrão definido (e repetitivo) de linhas vazias vs. não vazias, então não deve ser necessário especificar [AZ], mas provavelmente não fará mal também.
Alan Moore
Esta solução funcionou perfeitamente. À parte, peço desculpas, já que obviamente não esclareci a situação o suficiente (e também pelo atraso da resposta). Obrigado pela ajuda!
janeiro
21

Isso vai funcionar:

>>> import re
>>> rx_sequence=re.compile(r"^(.+?)\n\n((?:[A-Z]+\n)+)",re.MULTILINE)
>>> rx_blanks=re.compile(r"\W+") # to remove blanks and newlines
>>> text="""Some varying text1
...
... AAABBBBBBCCCCCCDDDDDDD
... EEEEEEEFFFFFFFFGGGGGGG
... HHHHHHIIIIIJJJJJJJKKKK
...
... Some varying text 2
...
... LLLLLMMMMMMNNNNNNNOOOO
... PPPPPPPQQQQQQRRRRRRSSS
... TTTTTUUUUUVVVVVVWWWWWW
... """
>>> for match in rx_sequence.finditer(text):
...   title, sequence = match.groups()
...   title = title.strip()
...   sequence = rx_blanks.sub("",sequence)
...   print "Title:",title
...   print "Sequence:",sequence
...   print
...
Title: Some varying text1
Sequence: AAABBBBBBCCCCCCDDDDDDDEEEEEEEFFFFFFFFGGGGGGGHHHHHHIIIIIJJJJJJJKKKK

Title: Some varying text 2
Sequence: LLLLLMMMMMMNNNNNNNOOOOPPPPPPPQQQQQQRRRRRRSSSTTTTTUUUUUVVVVVVWWWWWW

Alguma explicação sobre essa expressão regular pode ser útil: ^(.+?)\n\n((?:[A-Z]+\n)+)

  • O primeiro caractere ( ^) significa "começando no início de uma linha". Esteja ciente de que não corresponde à nova linha em si (o mesmo para $: significa "logo antes de uma nova linha", mas não corresponde à própria nova linha).
  • Então (.+?)\n\nsignifica "combinar o mínimo de caracteres possível (todos os caracteres são permitidos) até chegar a duas novas linhas". O resultado (sem as novas linhas) é colocado no primeiro grupo.
  • [A-Z]+\nsignifica "combinar tantas letras maiúsculas quanto possível até chegar a uma nova linha. Isso define o que chamarei de linha de texto .
  • ((?:linha de texto)+) significa corresponder a uma ou mais linhas de texto, mas não colocar cada linha em um grupo. Em vez disso, coloque todas as linhas de texto em um grupo.
  • Você pode adicionar um final \nna expressão regular se quiser impor uma nova linha dupla no final.
  • Além disso, se você não tiver certeza sobre o tipo de nova linha que obterá ( \nou \rou \r\n), apenas corrija a expressão regular substituindo todas as ocorrências de \npor (?:\n|\r\n?).
MiniQuark
fonte
1
match () retorna apenas uma correspondência, bem no início do texto de destino, mas o OP disse que haveria centenas de correspondências por arquivo. Acho que você preferiria finditer ().
Alan Moore
6

Se cada arquivo tivesse apenas uma sequência de aminoácidos, eu não usaria expressões regulares. Algo assim:

def read_amino_acid_sequence(path):
    with open(path) as sequence_file:
        title = sequence_file.readline() # read 1st line
        aminoacid_sequence = sequence_file.read() # read the rest

    # some cleanup, if necessary
    title = title.strip() # remove trailing white spaces and newline
    aminoacid_sequence = aminoacid_sequence.replace(" ","").replace("\n","")
    return title, aminoacid_sequence
MiniQuark
fonte
Definitivamente, a maneira mais fácil se houvesse apenas um, e também é viável com mais, se mais lógica for adicionada. No entanto, há cerca de 885 proteínas neste conjunto de dados específico, e achei que um regex deveria ser capaz de lidar com isso.
janeiro
4

encontrar:

^>([^\n\r]+)[\n\r]([A-Z\n\r]+)

\ 1 = algum_texto_variável

\ 2 = linhas de todos os CAPS

Editar (prova de que isso funciona):

text = """> some_Varying_TEXT

DSJFKDAFJKDAFJDSAKFJADSFLKDLAFKDSAF
GATACAACATAGGATACA
GGGGGAAAAAAAATTTTTTTTT
CCCCAAAA

> some_Varying_TEXT2

DJASDFHKJFHKSDHF
HHASGDFTERYTERE
GAGAGAGAGAG
PPPPPAAAAAAAAAAAAAAAP
"""

import re

regex = re.compile(r'^>([^\n\r]+)[\n\r]([A-Z\n\r]+)', re.MULTILINE)
matches = [m.groups() for m in regex.finditer(text)]

for m in matches:
    print 'Name: %s\nSequence:%s' % (m[0], m[1])
Jason Coon
fonte
Infelizmente, essa expressão regular também corresponderá a grupos de letras maiúsculas separadas por linhas vazias. Pode não ser um grande problema.
MiniQuark
Parece que coonj gosta de arquivos FASTA. ;)
Andrew Dalke
4

A seguir está uma expressão regular que corresponde a um bloco de texto de várias linhas:

import re
result = re.findall('(startText)(.+)((?:\n.+)+)(endText)',input)
Punnerud
fonte
1

Minha preferência.

lineIter= iter(aFile)
for line in lineIter:
    if line.startswith( ">" ):
         someVaryingText= line
         break
assert len( lineIter.next().strip() ) == 0
acids= []
for line in lineIter:
    if len(line.strip()) == 0:
        break
    acids.append( line )

Neste ponto, você tem algum texto variável como string e os ácidos como uma lista de strings. Você pode fazer "".join( acids )para fazer uma única corda.

Acho isso menos frustrante (e mais flexível) do que regexes de várias linhas.

S.Lott
fonte