Calificación:
  • 0 voto(s) - 0 Media
  • 1
  • 2
  • 3
  • 4
  • 5
uso de __iter__ y __next__ en ficheros
#1
hola,

estoy revisando el codigo de un parser de partidas de ajedrez a fin de reducir cantidad de codigo y mejorar la rapidez del proceso.
se trata de leer un fichero con cierto numero de partidas, que están grabadas en ascii de la siguiente manera:


Código:
[Event "Partida 1"]
[Site "Sitio 1"]
[Date "1992.11.04"]
[Round "1"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6
4. Ba4 Nf6 5. O-O Be7 1/2-1/2

[Event "Partida 2"]
[Site "Sitio 2"]
[Date "1992.11.04"]
[Round "2"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6
4. Ba4 Nf6 5. O-O Be7 1/2-1/2
en este caso hay solo dos partidas, pero pueden existir miles de ellas.

El problema que tengo es que no encuentro el fin del fichero (EOF) y me repite indefinidamente la última partida.
Pongo el código:

Código:
# python 3.6.1  
     
class IteradorPartidaCadena(object):
   """
       Iterador que contiene cadenas multilínea que
       representan partidas de un archivo PGN
   """
   def __init__(self, nom_fich):
       """
           Args:
               nom_fich (str): PGN nombre fichero
       """
       self.nom_fich = nom_fich
       self.fich_iter = iter(open(self.nom_fich))
       self.lineas_partida = []
       self.fin = False
   
   def __iter__(self):
       """iterador"""
       return self
   
   def __next__(self):
       """siguiente del iterador"""
       if self.fin is True:
           # raise StopIteration   # python 2.7
           return                  # python 3.5
           
       try:
           while True:
               linea = next(self.fich_iter)
               if linea.startswith("[Event"):
                   if len(self.lineas_partida) == 0:
                       self.lineas_partida.append(linea)
                       continue
                   else:
                       lineas_partida = self.lineas_partida[:]
                       self.lineas_partida = []
                       self.lineas_partida.append(linea)
                       partida_cadena = "".join(lineas_partida)
                       return partida_cadena
               else:
                   self.lineas_partida.append(linea)
       except StopIteration:
           lineas_partida = self.lineas_partida[:]
           partida_cadena = "".join(lineas_partida)
           #print(117, partida_cadena)
           self.end = True
           return partida_cadena
   
   
   

class IteradorPartida(object):
   """
       Iterador que contiene partidas de un archivo PGN
   """
   def __init__(self, nom_fich):
       """
           Args:
               nom_fich (str): fichero.pgn (nombre del fichero)
       """
       self.iterador_partida_str = IteradorPartidaCadena(nom_fich)

   def __iter__(self):
       """iterador"""
       return self
       
   def __next__(self):
       """siguiente"""
       for parti_str in self.iterador_partida_str:
           return parti_str
       
       
if __name__ == '__main__':
   for parti in IteradorPartida("comentarios.pgn"):
       print(parti)

Estoy casi seguro de que es solo cuestión de algún detalle tonto, pero llevo dos días con esto y no logro atinar. Cry
¿Alguien me puede indicar donde puede esta el error?. Gracias anticipadas.

edito:

Lo de la repetición infinita de la ultima partida está arreglado (lo he visto justo al revisar el post  Big Grin 

se trataba de sustituir self.end por self.fin (cosa de manejar indistintos idiomas)

ahora la salida que me reporta es un bucle infinito con None:

Código:
[Event "Partida 1"]
[Site "Sitio 1"]
[Date "1992.11.04"]
[Round "1"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6
4. Ba4 Nf6 5. O-O Be7 1/2-1/2


[Event "Partida 2"]
[Site "Sitio 2"]
[Date "1992.11.04"]
[Round "2"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6
4. Ba4 Nf6 5. O-O Be7 1/2-1/2

None
...
None
...
se puede solucionar dentro del for en main con un 

if parti is not None:
        print (parti)
    else:
        break

pero tengo que tener un error en las clases que no encuentro
Responder
#2
Hola, no quedó muy claro cuál es el problema ahora, pero en estos casos usar el depurador es bastante conveniente. Si usas algún IDE que soporte la depuración en Python, genial, si no puedes usar manualmente el módulo pdb.

Saludos!
Responder
#3
(14-07-2018, 10:41 PM)Francisco escribió: Hola, no quedó muy claro cuál es el problema ahora, pero en estos casos usar el depurador es bastante conveniente. Si usas algún IDE que soporte la depuración en Python, genial, si no puedes usar manualmente el módulo pdb.

Saludos!

El problema es que, teóricamente, el iterador, al alcanzar el final del fichero debiere parar y salir del bucle for por si solo.
No sé si debe ser así, o ,bien, tengo que controlarlo obligatoriamente con un try / except así:

-dentro de la clase IteradorPartida, en el método __next__:

for parti_str in self.iterador_partida_str:
            try:
                return parti_str
            except AttributeError:
                break

Nota: de este modo me funciona correctamente.

Pongo el programa completo, ya con un funcionamiento correcto:


Código:
# python 3.6.1  
import re


class PartidaPGN(object):
   def __init__(self, event=None, site=None, date=None, round=None,
                                                        white=None,
                                                        black=None,
                                                        result=None):
       '''
       Inicializamos una partida en formato PGN,
       recibimos los siete tags minimos requeridos
       para su inicializacion desde el propio pgn
       '''
       # inicializamos los tag estandar minimos
       self.Event = event
       self.Site = site
       self.Date = date        # formato ????-??-??
       self.Round = round      # "Num."
       self.White = white
       self.Black = black
       self.Result = result    # 1-0, 0-1, 1/2-1/2, *
       
       # ahora añadimos los tags no estandar segun pag. oficial
       # http://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm#c9.2.1
       # estos tags los suelen añadir los programas comerciales
       # No se añaden los especificos de USA (terminados en USFC)
       
       # 1. Informacion relativa a los jugadores
       self.WhiteTitle = None  # se usa un - para un jug. sin titulo
       self.BlackTitle = None  # los posibles valores son "FM", "IM", y "GM"
       self.WhiteElo = None    # rating FIDE de cada jugador.
       self.BlackElo = None    # se usa un - para un jugador sin puntuacion
       self.WhiteType = None   # los valores son:
       self.BlackType = None   # 'human' o 'program'
       
       # 2. Informacion relacion con el evento
       self.EventDate = None   # fecha del inicion del evento ????-??-??
       self.EventSponsor = None    # string. el patrocinador del torneo
       self.Section = None     # seccion torneo (p.ej. Open, Senior, ...)
       self.Stage = None       # estadio del torneo (p.ej. semifinal, etc...)
       self.Board = None       # num. tablero en equipo
       
       # 3. Informacion de aperturas
       self.Opening = None
       self.Variation = None
       self.SubVariation = None
       self.ECO = None         # encyclopedia Chess Openings
       self.NIC = None         # New in Chess
       
       # 4. Informacion adicional fecha y hora
       self.Time = None        # hora inicio partida 'HH:MM:SS'
       self.TimeControl = None
       
       # 5. Posiciones iniciales alternativas
       self.SetUp = None       # "0" = inic. estandar, "1" = otra FEN distinta
       self.FEN = None         # posic. inicial no estandar
       
       # 4. Tags varios
       self.Termination = None # string explicativo de como termina la partida
       self.Annotator = None   # comentarista
       self.Mode = None    
       # Mode =
       # "OTB" (over the board), "PM" (paper mail), "EM" (electronic mail),
       # "ICS" (Internet Chess Server), and "TC" (general telecommunication).
       self.PlyCount = None    # num. movimientos partida
       
       self.jugadas = []
   
   def __repr__(self):
       return '<PartidaPGN "%s" vs "%s">' % (self.White, self.Black)
   
   
     
class IteradorPartidaCadena(object):
   """
       Iterador que contiene cadenas multilínea que
       representan partidas de un archivo PGN
   """
   def __init__(self, nom_fich):
       """
           Args:
               nom_fich (str): PGN nombre fichero
       """
       self.nom_fich = nom_fich
       self.fich_iter = iter(open(self.nom_fich))
       self.lineas_partida = []
       self.fin = False
   
   def __iter__(self):
       """iterador"""
       return self
   
   def __next__(self):
       """siguiente del iterador"""
       if self.fin is True:
           # raise StopIteration   # python 2.7
           return                  # python 3.5
           
       try:
           while True:
               linea = next(self.fich_iter)
               if linea.startswith("[Event "):
                   if len(self.lineas_partida) == 0:
                       self.lineas_partida.append(linea)
                       continue
                   else:
                       lineas_partida = self.lineas_partida[:]
                       self.lineas_partida = []
                       self.lineas_partida.append(linea)
                       partida_cadena = "".join(lineas_partida)
                       return partida_cadena
               else:
                   self.lineas_partida.append(linea)
       except StopIteration:
           lineas_partida = self.lineas_partida[:]
           partida_cadena = "".join(lineas_partida)
           self.fin = True
           return partida_cadena
   
   
   

class IteradorPartida(object):
   """
       Iterador que contiene partidas de un archivo PGN
   """
   def __init__(self, nom_fich):
       """
           Args:
               nom_fich (str): fichero.pgn (nombre del fichero)
       """
       self.iterador_partida_str = IteradorPartidaCadena(nom_fich)

   def __iter__(self):
       """iterador"""
       return self
       
   def __next__(self):
       """siguiente"""
       
       for parti_str in self.iterador_partida_str:
           try:
               partida = carga_str(parti_str)[0]
               return partida
           except AttributeError:
               break


def _pre_procesa_texto(texto):
   '''
       Esta función es responsable de eliminar los comentarios
       de la línea final (; comentario), líneas en blanco y
       espacios adicionales. Además, convierte
       ``\\r \\n`` en ``\\n``.
       Asimismo separa los inicios `(`y finales `)` de variantes.
       p.ej.: `(29. Ra2)`en `( 29. Ra2 )` para el array final
       de la partida; quedando asi: `(`, `Ra2`, `)`
   '''
   texto = texto.replace('(', '( ')
   texto = texto.replace(')', ' )')
   texto1 = re.sub(r'\s*(\\r)?\\n\s*', '\n', texto.strip())
   lineas = []
   for linea in texto1.split('\n'):
       linea1 = re.sub(r'(\s*;.*|^\s*)', '', linea)
       if linea1:
           lineas.append(linea1)
   
   return lineas
   

def _siguiente_token(lineas):
   '''
   Obtenga el siguiente token de las líneas
   (lista de líneas de archivos pgn de texto).

   Hay 2 tipos de tokens: etiquetas y movimientos.
   Los tokens de etiquetas comienzan con `` [`` char, p.ej.
   `` [TagName "Tag Value"] ``. Las etiquetas Moves siguen el ejemplo:
   `` 1. e4 e5 2. d4``.
   '''
   if not lineas:
       return None

   token = lineas.pop(0).strip()
   if token.startswith('['):
       return token

   while lineas and not lineas[0].startswith('['):
       token += ' '+lineas.pop(0).strip()
   
   return token.strip()


def _parsea_tag(token):
   '''
   Analiza un token de la cabecer y devuelve una tupla
   con (tagName, tagValor).
   '''
   tag, valor = re.match(r'\[(\w*)\s*(.+)', token).groups()
   #return tag.lower(), valor.strip('"[] ')
   return tag, valor.strip('"[] ')


def _parsea_jugadas(token):
   '''
   Analiza un token de jugadas y devuelve una lista con movimientos
   '''
   movims = []
   while token:
       token = re.sub(r'^\s*(\d+\.+\s*)?', '', token)
       
       if token.startswith('{'):
           pos = token.find('}')+1
       else:
           pos1 = token.find(' ')
           pos2 = token.find('{')
           if pos1 <= 0:
               pos = pos2
           elif pos2 <= 0:
               pos = pos1
           else:
               pos = min([pos1, pos2])

       if pos > 0:
           movims.append(token[:pos])
           token = token[pos:]
       else:
           movims.append(token)
           token = ''
   
   return movims
   
       
def carga_str(texto):
   '''
       Convierte la cadena "texto" en una lista de PartidasPGN
   '''
   partidas = []
   partida = None
   lineas = _pre_procesa_texto(texto)
   
   while True:
       token = _siguiente_token(lineas)
       
       if not token:
           break
       
       if token.startswith('['):
           tag, valor = _parsea_tag(token)
           if not partida or (partida and partida.jugadas):
               partida = PartidaPGN()
               partidas.append(partida)

           setattr(partida, tag, valor)
       else:
           partida.jugadas = _parsea_jugadas(token)
   
   return partidas
       
       
if __name__ == '__main__':
   for parti in IteradorPartida("grande.pgn"):
       try:
           print(parti.jugadas)
           print(parti.White)
       except AttributeError:
           break


así como una pequeña aplicación de como usarlo:


Código:
# Python 3.6.1

# parsea_sql.py
# forma de uso:
# python parsea_sql.py fich_entrada.pgn > salida.sql

import parseapgn
import sys

# elegimos solo estos tags de cabecera para prueba
cabeceras = ['Site', 'Date', 'White', 'Black', 'Result']

def valores_planos (game):
   ret = '('
   for field in cabeceras:
       if hasattr(game, field):
           # para escapar los apostrofes (especialmente en ingles)
           # p.ej.: O\'Connor
           ret += ' \'' + getattr(game, field).replace('\'', '\\\'') + '\', '
       else:
           ret += ' \'\', '

   if hasattr(game, 'jugadas'):
       ret += str(len(game.jugadas)) + ', '
       ret += ' \'' + ' '.join(game.jugadas) + '\''
       """
       lo mas conveniente sería guardar game.jugadas como json y base64
       para evitar inyecciones (sql estandar). Por ejemplo:
       
       import json
       import base64
       
       ...
       # serializar la lista en una cadena, y luego codificarla en formato base64
       cadena_de_lista = json.dumps(mi_lista)
       dato_a_grabar = base64.b64encode(cadena_de_lista)
       ...
       
       Para recuperar la lista:
       
       mi_lista = json.loads( base64.b64decode(dato_a_grabar))
       
       """
   else:
       ret += '0, \'\''

   ret += ')'
   return ret

if len(sys.argv) != 2:
    print ('Uso: python parsea_sql.py input.pgn > out.sql')

i = 0
for partida in parseapgn.IteradorPartida(sys.argv[1]):
   if not(hasattr(partida, 'Result')):
       break;

   if (i % 500) == 0:
       print ('INSERT INTO parties (' + ', '.join(cabeceras) + ', moves, game) VALUES ')

   print (valores_planos(partida))

   if (i % 500) == 499:
       print (';')
   else:
       print (',')

   i += 1

print (';')


"""
Creacón de la tabla SQL

CREATE TABLE `parties` (
 `ID` int(11) NOT NULL,
 `date` varchar(10) NOT NULL,
 `site` varchar(32) NOT NULL,
 `white` varchar(32) NOT NULL,
 `black` varchar(32) NOT NULL,
 `result` varchar(10) NOT NULL,
 `moves` int(11) NOT NULL DEFAULT '0',
 `game` longtext NOT NULL
);

ALTER TABLE `parties` ADD PRIMARY KEY (`ID`);

ALTER TABLE `parties` MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT;
"""
Responder
#4
Con respecto a este código:

  1. if self.fin is True:
  2. # raise StopIteration # python 2.7
  3. return # python 3.5


¿Cuál es el sentido de retornarn None? El método __next__() siempre debe lanzar StopIteration, incluso en Python 3.5.
Responder
#5
(15-07-2018, 11:45 AM)Francisco escribió: Con respecto a este código:

  1. if self.fin is True:
  2.    # raise StopIteration   # python 2.7
  3.    return                  # python 3.5


¿Cuál es el sentido de retornarn None? El método __next__() siempre debe lanzar StopIteration, incluso en Python 3.5.

si quito el condicional if self.fin is True, me repite la ultima partida indefinidamente

---------
si dejo :

  1. if self.fin is True:
  2.     raise StopIteration   # python 2.7


sin capturar la excepcion en el bucle for del main me lanza la siguiente excepción:

  1. Traceback (most recent call last):
  2.   File "D:\VisorJS_v2\parseapgn.py", line 264, in <module>
  3.     print(parti.jugadas)
  4. AttributeError: 'NoneType' object has no attribute 'jugadas'


El bucle for del __next__ en class IteradorPartidaCadena no lanza excepción

--------
si dejo:

  1. if self.fin is True:
  2.     return


me lanza la excepciones del mismo tipo de arriba en _pre_procesa_texto

--------
si lo cargo como modulo en otro programa:

me funciona correctamente sin lanzar excepciones con:

  1. if self.fin is True:
  2.     raise StopIteration


asi que he decidido capturar las posibles excepciones en el bucle for de la clase IteradorPartidaCadena.

pero sigo sin entender por qué no para la iteración despues de :

  1. except StopIteration:
  2.     lineas_partida = self.lineas_partida[:]
  3.     partida_cadena = "".join(lineas_partida)
  4.     self.fin = True
  5.     return partida_cadena


en la linea 118
Responder
#6
Al iterar sobre tu clase no deberías estar chequeando nunca si parti is None o bien si lanza AttributeError por la misma razón; y esto ocurre porque en algún momento __next__() (en cualquiera de las dos clases) está retornando None en lugar de lanzar StopIteration que es la encargada de terminar el bucle.
Responder
#7
Francisco, gracias por tu interés.
Despues de darle muchas vueltas al problema, creo que ya se cual es la causa. La definición del propio fichero es el origen del problema.
Estos ficheros pgn de partidas de ajedrez tienen la siguiente estructura:

cabeceras-1
\n
jugadas-1
\n

...

cabeceras-n
\n
jugadas-n
\n

si observamos al final del fichero siempre hay como mínimo un linea vacía (la cual debe existir) segun el estandar de este tipo de archivos. Algunos programas comerciales tratan con dos \n al final del fichero.

Por lo que presumo que siempre se añadirá un None al final, se quiera o no.
Teniendo casi la certeza de que este es el problema; la solución viene sola (sin establecer un manejador de excepciones):


Código:
import parseapgn

for parti in parseapgn.IteradorPartida("comentarios.pgn"):
  if hasattr(parti, 'jugadas'):
      print(parti.jugadas)
      #print(parti.White)
Responder
#8
Bueno ... al final he solucionado mis pequeños problemas. He optado por el uso de generadores.
Pongo una versión resumida para no repetir todo:


Código:
def abre_fichero(nom_fich):
   lineas_partida = []
   partida_cadena = ''
   
   # abrimos el fichero para lectura
   fich = open(nom_fich, 'r')  
   while True:
       # read a single line
       line = fich.readline()
       if not line:
           # recolectamos las lineas de la ultima partida
           # que están todavia colgando
           partida_cadena = ''.join(lineas_partida)
           yield partida_cadena
           break
       if line.startswith("[Event "):
           if len(lineas_partida) == 0:
               lineas_partida.append(line)
               continue
           else:
               lineas_partida_1 = lineas_partida[:]
               lineas_partida = []
               lineas_partida.append(line)
               partida_cadena = "".join(lineas_partida_1)
               yield partida_cadena
       else:
           lineas_partida.append(line)
   
   # cerramos el puntero al fichero
   fich.close()
   
for valor in abre_fichero('comentarios.pgn'):
   print(valor)

para un fichero de datos en utf-8 sin BOM llamado comentarios.pgn, así:

Código:
[Round "1"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6
4. Ba4 Nf6 5. O-O Be7 1/2-1/2

[Event "Partida 2"]
[Site "Sitio 2"]
[Date "fecha 2"]
[Round "2"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6
4. Ba4 Nf6 5. O-O Be7 1/2-1/2

[Event "Partida 3"]
[Site "Sitio 3"]
[Date "fecha 3"]
[Round "3"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6
4. Ba4 Nf6 5. O-O Be7 1/2-1/2
Responder
#9
Perfecto! Saludos.
Responder


Salto de foro:


Usuarios navegando en este tema: 2 invitado(s)