Segmentador de textos
Si queremos segmentar un texto por espacios podemos utilizar la siguiente sentencia en Python:
import re
input = 'Si queremos segmentar un texto por espacios podemos utilizar la siguiente sentencia en Python'
re.split(r'[ \t\n\r]+', input)
# ['Si', 'queremos', 'segmentar', 'un', 'texto', 'por', 'espacios', 'podemos', 'utilizar', 'la', 'siguiente', 'sentencia', 'en', 'Python']
La expresión [ \t\r\n]+
es el negativo, es decir estamos eligiendo toda
sucesión
+
de caracteres [ ]
para dividir por ellos que sean:
- ' ' espacio o
\t
tabulador o\r
retorno de carro, que si existe antecede a\n
nueva línea.
Pero también podemos hacer el positivo, en vez de dividir, escoger toda sucesión
+
de caracteres [ ]
que no sean [^ ]
espacio, ni tabulador, ni retorno de carro ni
nueva
línea:
import re
input = 'Si queremos segmentar un texto por espacios podemos utilizar la siguiente sentencia en Python'
re.findall(r'[^ \t\n\r]+', input)
Alternativamente a re
podemos usar la librería NLTK
from nltk.tokenize import RegexpTokenizer
input = 'Si queremos segmentar un texto por espacios podemos utilizar la siguiente sentencia en Python'
segmentador = RegexpTokenizer(r'[^ \t\n\r]+')
segmentador.tokenize(input)
Para segmentar buscaremos ciertos caracteres. Algunos como ¿?¡!…
o nueva línea
no
son
ambiguos y nos facilitan la labor. Otros no obstante como el punto .
están involucrados en
otras
funciones como acronimia o separador numérico. Utilizaremos estos ambiguos como indicios y decidiremos según
el
contexto.
caracter | ambigüedad |
---|---|
espacio | unidades multipalabra ('escalera de caracol') o entidades ('Isaac Asimov', 'Unión Europea') |
- . , : / | también en números (-5 3.1415 1,000,001 23:55 1/3) |
'´’ | también en nombres (O'Neill) |
. | puntos suspensivos (... [...]), acrónimos (O.N.U.) |
Empecemos a segmentar:
Para segmentar por párrafos usaremos [^\r\n]+
, es decir, tomamos toda sucesión
+
de caracteres [ ]
que no sean [^ ]
retorno de carro ni nueva línea. Si quisiéramos mantener los separadores añadiríamos a la expresión |[\r\n]]+
en caso de que
quisiéramos que tal separador fuera un token o bien (?:[\r\n]]+)?
si quisiéramos que apareciera
asociado al último token.
Para segmentar por blancos usaremos [^ \t]+
, es decir, tomamos toda sucesión
+
de caracteres [ ]
que no sean [^ ]
espacio ni tabulador.
Para segmentar por dos puntos usaremos (?:[^:]+|(?<=\d):(?=\d))+
, es decir,
tomamos toda sucesión +
de caracteres [ ]
que no sean los dos puntos
[^:]
o bien |
tomamos dos puntos si están flanqueados (?<=)(?=)
por dos cifras
(forman
parte de un número).
La misma excepción numérica estará presente si separamos por comas (?:[^,]+|(?<=\d),(?=\d))+
o
barras (?:[^\/\\]+|(?<=\d)[\/\\](?=\d)
. De hecho podríamos compilar las tres expresiones en
una:
r'(?:[^:,\/\\]+|(?<=\d)[:,\/\\](?=\d)'
Para segmentar por guiones partimos de [^-]+
. Pero si la primera opción no se
verifica también aceptaremos aquellos guiones que precedan a un número (signo del número, -10) o flanqueados
por
letras ('Norte-sur', 'físico-químico').
r'(?:[^-]+|-(?=\d)|(?<=\w)-(?=\w))+'
Para segmentar por apóstrofos igualmente admitimos aquellos flanqueados por letras:
r"(?:[^'´’]+|(?<=\w)['´’](?=\w))+"
Segmentamos por caracteres no ambiguos fácilmente [^¿?¡!…]+
. Otra cuestión sería comprobar que a
cada delimitador inicial le corresponde uno final para lo que necesitaríamos una gramática independiente de
contexto y las expresiones regulares serían insuficientes.
El caso del punto es más complejo por ambiguo.
r'(?:[^.]+' \
r'|(?<=\d)\.(?=\d)' \
r'|(?<=\[)\.(?=\.\.\])' \
r'|(?<=\[\.)\.(?=\.\])' \
r'|(?<=\[\.\.)\.(?=\])' \
r'|(?<=\.[A-ZÑ])\.' \
r'|\.(?=[A-ZÑ]\.)' \
r'|(?<=\.[A-ZÑ]{2,2})\.' \
r'|\.(?=[A-ZÑ]{2,2}\.))+'
Tenemos que hacer tres excepciones:
- Admitiremos puntos entre cifras
(?<=\d)\.(?=\d)
al igual que con las comas y los dobles puntos. - Hemos de capturar cada uno de los tres puntos en
[...]
y para eso hay que observar sus contextos
r'|(?<=\[)\.(?=\.\.\])' \
r'|(?<=\[\.)\.(?=\.\])' \
r'|(?<=\[\.\.)\.(?=\])' \
- Hemos de capturar los puntos que forman parte de acrónimos
r'|(?<=\.[A-ZÑ])\.' \
r'|\.(?=[A-ZÑ]\.)' \
r'|(?<=\.[A-ZÑ]{2,2})\.' \
r'|\.(?=[A-ZÑ]{2,2}\.))+'
La primera alternativa escoge los puntos precedidos de punto y letra mayúscula, la siguiente los puntos sucedidos de letra mayúscula y punto. Las últimos dos alternativas son equivalentes para dos letras y permiten escoger acrónimos plurales como EE.UU. (Estados Unidos) o CC.OO. (Comisiones Obreras)
Dificultades adicionales no resueltas aquí
las que requieren una lista particularizada como unidades multipalabra ('escalera de caracol') o entidades ('Isaac Asimov', 'Unión Europea')
Las abreviaturas (admon., fís., ...) que además son una clase abierta y el usuario es libre de crear abreviaturas nuevas.
Algoritmo en Python
#! /usr/bin/python3
# -*- coding: utf-8 -*-
from nltk.tokenize import RegexpTokenizer
import sys
regexes = {
'segmenta_por_blancos': [
r'[^ \t]+', # r'(?:[^ \t]+)'
r'|[ \t]+',
r'(?:[ \t]+)?'
],
'segmenta_por_parrafos': [
r'[^\n\r]+',
r'|[\n\r]+',
r'(?:[\n\r]+)?'
],
'segmenta_por_puntos': [
r'(?:[^.]+' \
r'|(?<=\d)\.(?=\d)' \
r'|(?<=\[)\.(?=\.\.\])' \
r'|(?<=\[\.)\.(?=\.\])' \
r'|(?<=\[\.\.)\.(?=\])' \
r'|(?<=\.[A-ZÑ])\.' \
r'|\.(?=[A-ZÑ]\.)' \
r'|(?<=\.[A-ZÑ]{2,2})\.' \
r'|\.(?=[A-ZÑ]{2,2}\.))+',
r'|\.(?:\.\.)?',
r'(?:\.(?:\.\.)?)?'
],
'segmenta_por_dos_puntos': [
r'(?:[^:]+|(?<=\d):(?=\d))+',
r'|:',
r':?'
],
'segmenta_por_comas': [
r'(?:[^,]+|(?<=\d),(?=\d))+',
r'|,',
r',?'
],
'segmenta_por_barras': [
r'(?:[^\/\\]+|(?<=\d)[\/\\](?=\d))+',
r'|[\/\\]',
r'(?:[\/\\]+)?'
],
'segmenta_por_guiones': [
r'(?:[^-]+|-(?=\d)|(?<=\w)-(?=\w))+',
r'|-',
r'-?'
],
'segmenta_por_apostrofo': [
r"(?:[^'´’]+|(?<=\w)['´’](?=\w))+",
r'|[\'´’]',
r'(?:[\'´’]+)?'
],
'segmenta_por_varios': [
r'(?:[^"“”«»<>@%()\[\]{}‘`•·]+' \
r'|\[(?=\.\.\.\])' \
r'|(?<=\[\.\.\.)\]' \
r')+',
# [ ...]
r'|["“«”»<>@%()\[\]{}‘`•·]',
r'(?:["“«”»<>@%()\[\]{}‘`•·]+)?'
],
'segmenta_por_no_ambiguos': [
r'[^¿?¡!…]+',
r'[^¿?¡!…]+|[¿?¡!…]',
r'(?:(?:[¿¡]+)?(?:(?:[^¿?¡!…])+)?(?:[?!…]+)?)'
]
}
def procesa_expresion(strings_a_segmentar, nombre_regex,
elimina_separadores=False, separadores_aparte=True, elimina_espacios=True):
"""
:param strings_a_segmentar: La entrada, como lista de strings.
:type strings_a_segmentar: List[str]
:param regex:
:type regex: str
:param elimina_separadores: Si está a True, se elimina el separador. Si está a False, el separador se
mantiene (aparecerá como token independiente o a final de frase según el valor de separadores_aparte).
:type elimina_separadores: bool
:param separadores_aparte: Si está a True, los separadores aparecen como tokens aparte. Si no, aparecen
al final de la frase.
:type separadores_aparte: bool
:param elimina_espacios:
:type elimina_espacios: bool
:return: Una lista de strings.
:rtype: List[str]
"""
if nombre_regex == 'segmenta_por_no_ambiguos':
if elimina_separadores:
regex = r'[^¿?¡!…]+'
elif separadores_aparte:
regex = r'[^¿?¡!…]+|[¿?¡!…]'
else:
regex = r'(?:(?:[¿¡]+)?(?:(?:[^¿?¡!…])+)?(?:[?!…]+)?)'
else:
entry = regexes[nombre_regex]
if elimina_separadores:
regex = entry[0]
elif separadores_aparte:
regex = entry[0] + entry[1]
else:
regex = entry[0] + entry[2]
# Se crea el segmentador usando la expresión regular que hayamos creado.
segmentador = RegexpTokenizer(regex)
# Se obtienen los resultados: para cada string de la lista de entrada, se crea una lista de strings (que
# habitualmente será un único string salvo que incluya un ':' que separe frases), y todos esos strings
# se devuelven en una lista. Se devuelven al menos tantos strings como tenga la lista de entrada.
if elimina_espacios:
return [string.strip()
for string_inicial in strings_a_segmentar
for string in segmentador.tokenize(string_inicial)
if string.strip()]
else:
return [string
for string_inicial in strings_a_segmentar
for string in segmentador.tokenize(string_inicial)]
def segmenta_por_tokens(strings_a_segmentar, elimina_separadores=False):
"""
Tomamos una lista de strings (un texto previamente segmentado como lista de strings, o un único texto pero
dentro de una lista de un único elemento) y devolvemos otra lista en la que cada string de la lista de
entrada se convierte en una lista de tokens (es decir, se devuelve una lista de listas de strings).
Los tokens que sean signos de puntuación (en general, separadores) se eliminan o no según el parámetro.
El uso general de esta función es la de tomar una lista de strings que representan a frases (que incluyen
los separadores al inicio/final) y devolver una lista de listas de tokens, donde cada lista de tokens
incluidos en la lista de nivel superior representa una frase segmentado en tokens.
:param strings_a_segmentar: La entrada, como lista de strings.
:type strings_a_segmentar: List[str]
:param elimina_separadores: Si está a True, se eliminan los separadores. Si está a False, los separadores
se mantienen (aparecerán como tokens independientes).
:type elimina_separadores: bool
:return: Una lista de strings.
:rtype: List[List[str]]
"""
# Esta expresión dió error, porque toma el valor por referencia. con lo cual modifica el valor del argumento fuera de esta función
# strings_a_segmentar_temp = strings_a_segmentar
# alternativas:
# ¿? strings_a_segmentar_temp = [None] * len(strings_a_segmentar)
strings_a_segmentar_temp = strings_a_segmentar.copy()
for n in range(0, len(strings_a_segmentar_temp)):
# Hay que prestar atención a que el argumento de cada función sea una lista si no queremos que nos liste los caracteres individuales
frase = [strings_a_segmentar_temp[n]]
# funciones que segmentan frases pero también tokens
frase = procesa_expresion(frase, 'segmenta_por_parrafos', elimina_separadores=elimina_separadores,
separadores_aparte=True)
frase = procesa_expresion(frase, 'segmenta_por_dos_puntos', elimina_separadores=elimina_separadores,
separadores_aparte=True)
frase = procesa_expresion(frase, 'segmenta_por_puntos', elimina_separadores=elimina_separadores,
separadores_aparte=True)
frase = procesa_expresion(frase, 'segmenta_por_no_ambiguos', elimina_separadores=elimina_separadores,
separadores_aparte=True)
# funciones que segmentan tokens
frase = procesa_expresion(frase, 'segmenta_por_comas', elimina_separadores=elimina_separadores, separadores_aparte=True)
frase = procesa_expresion(frase, 'segmenta_por_barras', elimina_separadores=elimina_separadores, separadores_aparte=True)
frase = procesa_expresion(frase, 'segmenta_por_guiones', elimina_separadores=elimina_separadores, separadores_aparte=True)
frase = procesa_expresion(frase, 'segmenta_por_apostrofo', elimina_separadores=elimina_separadores, separadores_aparte=True)
frase = procesa_expresion(frase, 'segmenta_por_varios', elimina_separadores=elimina_separadores, separadores_aparte=True)
frase = procesa_expresion(frase, 'segmenta_por_blancos', elimina_separadores=True, elimina_espacios=False)
strings_a_segmentar_temp[n] = frase
return strings_a_segmentar_temp
def segmenta_por_frases(strings_a_segmentar, elimina_separadores=False, segmenta_tokens_de_frase=False):
"""
Tomamos una lista de strings (un texto previamente segmentado como lista de strings, o un único texto pero
dentro de una lista de un único elemento) y dependiendo del parámetro segmenta_tokens_de_frase se devuelve
una de las dos siguientes cosas:
- Si dicho parámetro es False, devolvemos otra lista de strings en la que los strings son frases. Los
separadores de las frases NO SE SEPARAN y dicho caracter (o caracteres) van junto con el resto de
caracteres que forman la frase.
- Si el parámetro de segmenta_tokens_de_frases es True, esas frases se tokenizan a su vez de forma que
cada frase se convierte en una lista de tokens (con lo que se devuelve no una lista de strings -una
lista de frases donde cada frase está representada por un string- sino una lista donde cada elemento es
una lista de strings -con lo que se devuelve una lista de listas de tokens-).
Obviamente, al tokenizar así, los segmentadores aparecen como tokens aparte (y como parte de la lista de
tokens que representa a la frase).
:param strings_a_segmentar: La entrada, como lista de strings.
:type strings_a_segmentar: List[str]
:param elimina_separadores: Si está a True, se elimina el separador. Si está a False, el separador se
mantiene. En cualquier caso, los separadores blancos siempre se eliminan.
:type elimina_separadores: bool
:param segmenta_tokens_de_frase: Si este parámetro está a True, una vez que se haya segmentado en frases
dichas frases se subdividen a su vez en tokens. Si los separadores no se han eliminado, entonces
aparecerán como tokens independientes. Así pues, si este parámetro es False se devuelve una lista de
strings, y si está a True se devuelve una lista de listas de strings.
:type segmenta_tokens_de_frase: bool
:return: Una lista de strings o una lista de listas de strings (según el valor de
segmenta_tokens_de_frase).
:rtype: List[str] o List[List[str]]
"""
# funciones que segmentan frases pero también tokens
strings_a_segmentar_temp = procesa_expresion(strings_a_segmentar, 'segmenta_por_parrafos', elimina_separadores=elimina_separadores,
separadores_aparte=False)
strings_a_segmentar_temp = procesa_expresion(strings_a_segmentar_temp, 'segmenta_por_dos_puntos', elimina_separadores=elimina_separadores,
separadores_aparte=False)
strings_a_segmentar_temp = procesa_expresion(strings_a_segmentar_temp, 'segmenta_por_puntos', elimina_separadores=elimina_separadores,
separadores_aparte=False)
strings_a_segmentar_temp = procesa_expresion(strings_a_segmentar_temp, 'segmenta_por_no_ambiguos', elimina_separadores=elimina_separadores,
separadores_aparte=False)
if segmenta_tokens_de_frase:
strings_a_segmentar_temp = segmenta_por_tokens(strings_a_segmentar_temp, elimina_separadores)
return strings_a_segmentar_temp
# desde consola
# python segmentador.py
if __name__ == "__main__":
texto = """El túmulo alargado Coldrum (en inglés Coldrum Long Barrow), también conocido como piedras Coldrum (Coldrum Stones) o piedras Adscombe (Adscombe Stones), es un túmulo alargado con cámara ubicado cerca del pueblo de Trottiscliffe del condado de Kent, en el sudeste de Inglaterra. Probablemente construido en el cuarto milenio antes de Cristo, durante el período Neolítico inicial de Gran Bretaña, se encuentra en estado de ruina. """
if len(sys.argv) == 2:
if sys.argv[1]:
texto = sys.argv[1]
else:
print("Ejemplo: ")
resultado = segmenta_por_frases([texto], segmenta_tokens_de_frase=True)
[print(token) for token in resultado]