¿Cuál es la forma Python de hacer un bucle de análisis anclado \ G?

Lo que sigue es una función de Perl que escribí hace años. Es un tokenizador inteligente que reconoce algunos casos de cosas pegadas que quizás no deberían ser. Por ejemplo, dada la entrada de la izquierda, divide la cadena como se muestra a la derecha:

abc123 -> abc|123 abcABC -> abc|ABC ABC123 -> ABC|123 123abc -> 123|abc 123ABC -> 123|ABC AbcDef -> Abc|Def (eg CamelCase) ABCDef -> ABC|Def 1stabc -> 1st|abc (recognize valid ordinals) 1ndabc -> 1|ndabc (but not invalid ordinals) 11thabc -> 11th|abc (recognize that 11th - 13th are different than 1st - 3rd) 11stabc -> 11|stabc 

Ahora estoy haciendo algunos experimentos de aprendizaje automático, y me gustaría hacer algunos experimentos que usan este tokenizer. Pero primero, necesitaré portarlo de Perl a Python. La clave de este código es el bucle que usa el ancla \ G, algo que escucho no existe en python. He intentado buscar en Google cómo se hace esto en Python, pero no estoy seguro de qué buscar exactamente, así que tengo problemas para encontrar una respuesta.

¿Cómo escribirías esta función en Python?

 sub Tokenize # Breaks a string into tokens using special rules, # where a token is any sequence of characters, be they a sequence of letters, # a sequence of numbers, or a sequence of non-alpha-numeric characters # the list of tokens found are returned to the caller { my $value = shift; my @list = (); my $word; while ( $value ne '' && $value =~ m/ \G # start where previous left off ([^a-zA-Z0-9]*) # capture non-alpha-numeric characters, if any ([a-zA-Z0-9]*?) # capture everything up to a token boundary (?: # identify the token boundary (?=[^a-zA-Z0-9]) # next character is not a word character | (?=[AZ][az]) # Next two characters are upper lower | (?<=[az])(?=[AZ]) # lower followed by upper | (?<=[a-zA-Z])(?=[0-9]) # letter followed by digit # ordinal boundaries | (?<=^1(?i:st)) # first | (?<=[^1][1](?i:st)) # first but not 11th | (?<=^2(?i:nd)) # second | (?<=[^1]2(?i:nd)) # second but not 12th | (?<=^3(?i:rd)) # third | (?<=[^1]3(?i:rd)) # third but not 13th | (?<=1[123](?i:th)) # 11th - 13th | (?<=[04-9](?i:th)) # other ordinals # non-ordinal digit-letter boundaries | (?<=^1)(?=[a-zA-Z])(?!(?i)st) # digit-letter but not first | (?<=[^1]1)(?=[a-zA-Z])(?!(?i)st) # digit-letter but not 11th | (?<=^2)(?=[a-zA-Z])(?!(?i)nd) # digit-letter but not first | (?<=[^1]2)(?=[a-zA-Z])(?!(?i)nd) # digit-letter but not 12th | (?<=^3)(?=[a-zA-Z])(?!(?i)rd) # digit-letter but not first | (?<=[^1]3)(?=[a-zA-Z])(?!(?i)rd) # digit-letter but not 13th | (?<=1[123])(?=[a-zA-Z])(?!(?i)th) # digit-letter but not 11th - 13th | (?<=[04-9])(?=[a-zA-Z])(?!(?i)th) # digit-letter but not ordinal | (?=$) # end of string ) /xg ) { push @list, $1 if $1 ne ''; push @list, $2 if $2 ne ''; } return @list; } 

Intenté usar re.split () con una variación de lo anterior. Sin embargo, split () se niega a dividirse en una coincidencia de ancho cero (una habilidad que debería ser posible si uno realmente sabe lo que está haciendo).

Encontré una solución para este problema específico, pero no para el problema general de “cómo uso el análisis basado en \ G”. Tengo un código de ejemplo que se regexea dentro de los bucles que están anclados usando \ G y luego en el body usa otra coincidencia anclada en \ G para ver de qué manera proceder con el análisis. Así que todavía estoy buscando una respuesta.

Dicho esto, aquí está mi código de trabajo final para traducir lo anterior a Python:

 import re IsA = lambda s: '[' + s + ']' IsNotA = lambda s: '[^' + s + ']' Upper = IsA( 'AZ' ) Lower = IsA( 'az' ) Letter = IsA( 'a-zA-Z' ) Digit = IsA( '0-9' ) AlphaNumeric = IsA( 'a-zA-Z0-9' ) NotAlphaNumeric = IsNotA( 'a-zA-Z0-9' ) EndOfString = '$' OR = '|' ZeroOrMore = lambda s: s + '*' ZeroOrMoreNonGreedy = lambda s: s + '*?' OneOrMore = lambda s: s + '+' OneOrMoreNonGreedy = lambda s: s + '+?' StartsWith = lambda s: '^' + s Capture = lambda s: '(' + s + ')' PreceededBy = lambda s: '(?<=' + s + ')' FollowedBy = lambda s: '(?=' + s + ')' NotFollowedBy = lambda s: '(?!' + s + ')' StopWhen = lambda s: s CaseInsensitive = lambda s: '(?i:' + s + ')' ST = '(?:st|ST)' ND = '(?:nd|ND)' RD = '(?:rd|RD)' TH = '(?:th|TH)' def OneOf( *args ): return '(?:' + '|'.join( args ) + ')' pattern = '(.+?)' + \ OneOf( # ABC | !!! - break at whitespace or non-alpha-numeric boundary PreceededBy( AlphaNumeric ) + FollowedBy( NotAlphaNumeric ), PreceededBy( NotAlphaNumeric ) + FollowedBy( AlphaNumeric ), # ABC | Abc - break at what looks like the start of a word or sentence FollowedBy( Upper + Lower ), # abc | ABC - break when a lower-case letter is followed by an upper case PreceededBy( Lower ) + FollowedBy( Upper ), # abc | 123 - break between words and digits PreceededBy( Letter ) + FollowedBy( Digit ), # 1st | oak - recognize when the string starts with an ordinal PreceededBy( StartsWith( '1' + ST ) ), PreceededBy( StartsWith( '2' + ND ) ), PreceededBy( StartsWith( '3' + RD ) ), # 1st | abc - contains an ordinal PreceededBy( IsNotA( '1' ) + '1' + ST ), PreceededBy( IsNotA( '1' ) + '2' + ND ), PreceededBy( IsNotA( '1' ) + '3' + RD ), PreceededBy( '1' + IsA( '123' ) + TH ), PreceededBy( IsA( '04-9' ) + TH ), # 1 | abcde - recognize when it starts with or contains a non-ordinal digit/letter boundary PreceededBy( StartsWith( '1' ) ) + FollowedBy( Letter ) + NotFollowedBy( ST ), PreceededBy( StartsWith( '2' ) ) + FollowedBy( Letter ) + NotFollowedBy( ND ), PreceededBy( StartsWith( '3' ) ) + FollowedBy( Letter ) + NotFollowedBy( RD ), PreceededBy( IsNotA( '1' ) + '1' ) + FollowedBy( Letter ) + NotFollowedBy( ST ), PreceededBy( IsNotA( '1' ) + '2' ) + FollowedBy( Letter ) + NotFollowedBy( ND ), PreceededBy( IsNotA( '1' ) + '3' ) + FollowedBy( Letter ) + NotFollowedBy( RD ), PreceededBy( '1' + IsA( '123' ) ) + FollowedBy( Letter ) + NotFollowedBy( TH ), PreceededBy( IsA( '04-9' ) ) + FollowedBy( Letter ) + NotFollowedBy( TH ), # abcde | $ - end of the string FollowedBy( EndOfString ) ) matcher = re.compile( pattern ) def tokenize( s ): return matcher.findall( s ) 

Emular \G al comienzo de una expresión regular con re.RegexObject.match

Puede emular el efecto de \G al comienzo de una expresión regular con re re.RegexObject.match módulo haciendo un seguimiento de y proporcionando la posición inicial a re.RegexObject.match , que obliga a que la coincidencia comience en la posición especificada en pos .

 def tokenize(w): index = 0 m = matcher.match(w, index) o = [] # Although index != m.end() check zero-length match, it's more of # a guard against accidental infinite loop. # Don't expect a regex which can match empty string to work. # See Caveat section. while m and index != m.end(): o.append(m.group(1)) index = m.end() m = matcher.match(w, index) return o 

Advertencia

Una advertencia a este método es que no juega bien con expresiones regulares que coincidan con una cadena vacía en la coincidencia principal, ya que Python no tiene ninguna facilidad para forzar a la expresión regular a reintentar la coincidencia mientras evita la coincidencia de longitud cero.

Como ejemplo, re.findall(r'(.??)', 'abc') devuelve una matriz de 4 cadenas vacías ['', '', '', ''] , mientras que en PCRE, puedes encontrar 7 coincidencias ['', 'a', '', 'b', '', 'c' ''] donde las coincidencias segunda, cuarta y sexta comienzan en los mismos índices que las coincidencias primera, tercera y quinta respectivamente. Las coincidencias adicionales en PCRE se encuentran al reintentar en los mismos índices con un indicador que evita la coincidencia de cadena vacía.

Sé que la pregunta es sobre Perl, no PCRE, pero el comportamiento de coincidencia global debería ser el mismo. De lo contrario, el código original no podría haber funcionado.

Reescribir ([^a-zA-Z0-9]*)([a-zA-Z0-9]*?) (.+?) , Como se hace en la pregunta, evita este problema, aunque es posible que desee utilizar bandera re.S

Otros comentarios sobre la expresión regular.

Dado que la bandera que distingue entre mayúsculas y minúsculas en Python afecta a todo el patrón, los sub-patrones insensibles a las mayúsculas y minúsculas deben reescribirse. Reescribiría (?i:st) como [sS][tT] para preservar el significado original, pero vaya con (?:st|ST) si es parte de su requisito.

Como Python admite el modo de espacio libre con la re.X , puedes escribir tu expresión regular de forma similar a lo que hiciste en el código Perl:

 matcher = re.compile(r''' (.+?) (?: # identify the token boundary (?=[^a-zA-Z0-9]) # next character is not a word character | (?=[AZ][az]) # Next two characters are upper lower | (?<=[az])(?=[AZ]) # lower followed by upper | (?<=[a-zA-Z])(?=[0-9]) # letter followed by digit # ordinal boundaries | (?<=^1[sS][tT]) # first | (?<=[^1][1][sS][tT]) # first but not 11th | (?<=^2[nN][dD]) # second | (?<=[^1]2[nN][dD]) # second but not 12th | (?<=^3[rR][dD]) # third | (?<=[^1]3[rR][dD]) # third but not 13th | (?<=1[123][tT][hH]) # 11th - 13th | (?<=[04-9][tT][hH]) # other ordinals # non-ordinal digit-letter boundaries | (?<=^1)(?=[a-zA-Z])(?![sS][tT]) # digit-letter but not first | (?<=[^1]1)(?=[a-zA-Z])(?![sS][tT]) # digit-letter but not 11th | (?<=^2)(?=[a-zA-Z])(?![nN][dD]) # digit-letter but not first | (?<=[^1]2)(?=[a-zA-Z])(?![nN][dD]) # digit-letter but not 12th | (?<=^3)(?=[a-zA-Z])(?![rR][dD]) # digit-letter but not first | (?<=[^1]3)(?=[a-zA-Z])(?![rR][dD]) # digit-letter but not 13th | (?<=1[123])(?=[a-zA-Z])(?![tT][hH]) # digit-letter but not 11th - 13th | (?<=[04-9])(?=[a-zA-Z])(?![tT][hH]) # digit-letter but not ordinal | (?=$) # end of string ) ''', re.X)