Expressão regular rápida que verifica o número de vezes que um caractere aparece em uma linha no Vim?

1

Suponha que eu tenha um arquivo de texto delimitado por um pipe. Eu suspeito que uma das colunas pode ter um caractere de pipe incorporado ('|'). Eu sei que há 8 colunas no arquivo e cada linha deve ter 8-1 = 7 caracteres de canal. Por isso, preciso encontrar todas as linhas que tenham 8 ou mais '|' caracteres.

O regex a seguir deve encontrar todos esses casos, mas demora muito tempo para retornar ao meu arquivo de 200.000 registros:

^\(.*|.*\)\{8,}$

Existe um regex mais rápido que eu deveria usar? Por muito tempo , quero dizer mais tempo do que eu esperaria - pelo menos alguns minutos. Este não é um arquivo tão grande (200 mil registros), então estou assumindo que o próprio regex não é eficiente.

Alguns dados de amostra:

SAMPLE_ID|GROUPS|ADDRESSSTRING|LATITUDE|LONGITUDE|COUNTRYCODE|LANGUAGECODE|ISO_2_LTR_CODE
7304094||Rhein-Galerie;Baden-Württemberg|49.48334|8.45007|DEU|ger|DE
7303851||Steigenberger Insel;Baden-Württemberg|47.69005|9.18812|DEU|ger|DE
7303850||Si-Suites;Baden-Württemberg|48.72309|9.16138|DEU|ger|DE

(Estou executando o gVim no WinXP)

    
por drapkin11 30.03.2011 / 00:51

2 respostas

2

Seu regex é propenso a se deparar com algum comportamento O (N ^ 2) do mecanismo regex "backtracking" usado no Vim (e em muitos outros idiomas e ambientes).

Felizmente, existem maneiras de escrever expressões equivalentes que não causam retrocesso excessivo. Por exemplo:

/^\([^|]*|\)\{8}.*$

Em geral, você não precisa corresponder a “oito ou mais”, pois se você já sabe que a linha é problemática se tiver oito (se tem mais ou não).

Se você realmente precisar corresponder a linha inteira (por exemplo, porque é parte de uma operação :s ), será necessário manter a última parte ( .*$ ); Se você está apenas usando o regex para encontrar as "oito ou mais" linhas, então você pode deixar .*$ do final.

Além disso, aconselho apenas tentar combinar um "lado" do tubo dentro do grupo que você repita. Isso simplifica tanto o pensamento sobre como a regex combina linhas, quanto o próprio mecanismo de regex é executado (elimina uma fonte de retrocesso).

Agora, para explicar o pouco sobre "retrocesso". Considere que você tem uma linha com oito caracteres de pipe:

aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh

A passagem a seguir descreve como o mecanismo regex tenta combinar sua expressão com a linha acima (adicionei espaço em branco extra às linhas regex para mostrar (aproximadamente) onde partes da regex correspondem aos caracteres da própria linha).

O primeiro .* é ganancioso e combinará tudo com o final da linha, deixando o caractere de canal inigualável.

  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                                            |

A mais recente correspondência "encolhível" desiste de partes do seu jogo e tenta o resto do regex novamente. Isso acontece um caractere por vez neste caso (já que . corresponderá a qualquer caractere único). Esse backtracking continua até que o restante da expressão possa corresponder (ou até voltar ao início - que é a única maneira pela qual a linha não corresponde à expressão!).

  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                                     |.*    )(.*|

Assim, o primeiro .* recuou o suficiente para deixar o restante do grupo igualar, mas não havia nada para o segundo grupo igualar. Hora de recuar um pouco mais.

  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                              |.*           )(.*|

O backtracking encontrou um novo ponto de "parada", mas agora o segundo .* no primeiro grupo está fazendo sua correspondência gulosa. O segundo grupo não corresponde. Backtracking do segundo .* no primeiro grupo começa.

  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                              |.*)(.*|.*    )(.*|

O segundo grupo encontrou uma correspondência, mas o terceiro não correspondeu. Retroceda novamente, começando com a correspondência mais recente. O segundo .* do segundo grupo volta atrás para nada. O primeiro .* do segundo grupo retrocede para nada. O segundo .* do primeiro grupo retrocede para nada. O primeiro .* do primeiro grupo recua com sucesso.

  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                       |.*                  )(.*|

Mas, novamente, o segundo .* é ganancioso, por isso não deixa nada para o segundo grupo.

  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                       |.*       )(.*|.*    )(.*|
  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                       |.*)(.*|.*)(.*|.*    )(.*|

Eventualmente, todos os três grupos correspondem, mas a quarta instância do grupo falha. Comece a recuar.

  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                |.*                         )(.*|
  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                |.*              )(.*|.*    )(.*|
  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                |.*       )(.*|.*)(.*|.*    )(.*|
  aaaaaa|bbbbbb|cccccc|dddddd|eeeeee|ffffff|  gg  |  gg  |hhhhhh
^(.*                                |.*)(.*|.*)(.*|.*)(.*|.*    )(.*|

Você pode ver como isso queima muito tempo (os diagramas até pulam o backtracking de caractere por caractere que realmente ocorre; somente os “pontos altos” são mostrados acima). O problema vem de ter um bit anterior do regex correspondentemente com algo que a parte posterior do regex eventualmente terá que combinar para obter o número apropriado de repetições do grupo.

Na minha expressão, cada repetição ( [^|]* ) nunca corresponde a nada que o elemento a seguir ( | ) corresponderia, portanto, o retrocesso é puramente linear. Depois que o retrocesso iniciar para cada correspondência "encolhível", ele descobrirá (em tempo linear) que não há lugares anteriores nos quais a expressão a seguir possa corresponder; isso força o retrocesso a continuar com a correspondência anterior "encolhível" até que nada seja igualado e a linha inteira seja decidida como não correspondendo.

Em vez de "zero ou mais não pipe, em seguida, pipe" ( [^|]*| ), também é possível usar . com uma repetição explicitamente não voraz ( \{-} no Vim, mas isso varia; outros linguagens regex usam *? ).

^\(.\{-}|\)\{8}.*$
    
por 30.03.2011 / 04:58
1

Bem, no meu computador, isso é mais rápido:

:%s/\(|.\{-}\)\{8,}//n
    
por 30.03.2011 / 01:21