Python: Determina el prefijo de un conjunto de cadenas (similares)


Tengo un conjunto de cadenas, por ejemplo,

my_prefix_what_ever
my_prefix_what_so_ever
my_prefix_doesnt_matter

Simplemente quiero encontrar la porción común más larga de estas cadenas, aquí el prefijo. En lo anterior, el resultado debe ser

my_prefix_

Las cadenas

my_prefix_what_ever
my_prefix_what_so_ever
my_doesnt_matter

Debe resultar en el prefijo

my_

¿Existe una forma relativamente indolora en Python para determinar el prefijo (sin tener que iterar sobre cada carácter manualmente)?

PD: Estoy usando Python 2.6.3.

Author: Kawu, 2011-07-16

8 answers

Nunca reescriba lo que se le proporciona: os.path.commonprefix hace exactamente esto:

Devuelve el prefijo de ruta más largo (tomado carácter por carácter) que es un prefijo de todas las rutas en la lista. Lista If está vacía, devuelve la cadena vacía (''). Tenga en cuenta que esto puede volver rutas no válidas porque funciona un carácter a la vez.

Para la comparación con las otras respuestas, aquí está el código:

# Return the longest prefix of all list elements.
def commonprefix(m):
    "Given a list of pathnames, returns the longest common leading component"
    if not m: return ''
    s1 = min(m)
    s2 = max(m)
    for i, c in enumerate(s1):
        if c != s2[i]:
            return s1[:i]
    return s1
 101
Author: Ned Batchelder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-19 10:38:38

Ned Batchelder probablemente tiene razón. Pero por diversión, aquí hay una versión más eficiente de la respuesta de phimuemue usando itertools.

import itertools

strings = ['my_prefix_what_ever', 
           'my_prefix_what_so_ever', 
           'my_prefix_doesnt_matter']

def all_same(x):
    return all(x[0] == y for y in x)

char_tuples = itertools.izip(*strings)
prefix_tuples = itertools.takewhile(all_same, char_tuples)
''.join(x[0] for x in prefix_tuples)

Como una afrenta a la legibilidad, aquí hay una versión de una línea:)

>>> from itertools import takewhile, izip
>>> ''.join(c[0] for c in takewhile(lambda x: all(x[0] == y for y in x), izip(*strings)))
'my_prefix_'
 12
Author: senderle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:25

Aquí está mi solución:

a = ["my_prefix_what_ever", "my_prefix_what_so_ever", "my_prefix_doesnt_matter"]

prefix_len = len(a[0])
for x in a[1 : ]:
    prefix_len = min(prefix_len, len(x))
    while not x.startswith(a[0][ : prefix_len]):
        prefix_len -= 1

prefix = a[0][ : prefix_len]
 5
Author: MRAB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-16 15:35:39

La siguiente es una solución que funciona, pero probablemente bastante ineficiente.

a = ["my_prefix_what_ever", "my_prefix_what_so_ever", "my_prefix_doesnt_matter"]
b = zip(*a)
c = [x[0] for x in b if x==(x[0],)*len(x)]
result = "".join(c)

Para conjuntos pequeños de cadenas, lo anterior no es un problema en absoluto. Pero para conjuntos más grandes, yo personalmente codificaría otra solución manual que comprueba cada carácter uno tras otro y se detiene cuando hay diferencias.

Algorítmicamente, esto produce el mismo procedimiento, sin embargo, uno podría ser capaz de evitar la construcción de la lista c.

 2
Author: phimuemue,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-16 15:15:28

Solo por curiosidad descubrí otra manera de hacer esto:

def common_prefix(strings):

    if len(strings) == 1:#rule out trivial case
        return strings[0]

    prefix = strings[0]

    for string in strings[1:]:
        while string[:len(prefix)] != prefix and prefix:
            prefix = prefix[:len(prefix)-1]
        if not prefix:
            break

    return prefix

strings = ["my_prefix_what_ever","my_prefix_what_so_ever","my_prefix_doesnt_matter"]

print common_prefix(strings)
#Prints "my_prefix_"

Como Ned señaló, probablemente sea mejor usar os.path.commonprefix, que es una función bastante elegante.

 1
Author: ThePhysicist,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-30 15:57:32

La segunda línea de esto emplea la función reducir en cada carácter en las cadenas de entrada. Devuelve una lista de N + 1 elementos donde N es la longitud de la cadena de entrada más corta.

Cada elemento en lote es (a) el carácter de entrada, si todas las cadenas de entrada coinciden en esa posición, o (b) Ninguna. lot.index (None) es la posición del primer None en lote: la longitud del prefijo común. out es ese prefijo común.

val = ["axc", "abc", "abc"]
lot = [reduce(lambda a, b: a if a == b else None, x) for x in zip(*val)] + [None]
out = val[0][:lot.index(None)]
 1
Author: Mano Bastardo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-02 19:37:31

Aquí hay otra forma de hacer esto usando OrderedDict con código mínimo.

import collections
import itertools

def commonprefix(instrings):
    """ Common prefix of a list of input strings using OrderedDict """

    d = collections.OrderedDict()

    for instring in instrings:
        for idx,char in enumerate(instring):
            # Make sure index is added into key
            d[(char, idx)] = d.get((char,idx), 0) + 1

    # Return prefix of keys while value == length(instrings)
    return ''.join([k[0] for k in itertools.takewhile(lambda x: d[x] == len(instrings), d)])
 0
Author: user2758579,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-14 07:23:29

Aquí está una solución limpia simple. La idea es usar la función zip() para alinear todos los caracteres poniéndolos en una lista de caracteres 1, lista de caracteres 2,...lista de enésimos caracteres. Luego, itere cada lista para verificar si contienen solo 1 valor.

a = ["my_prefix_what_ever", "my_prefix_what_so_ever", "my_prefix_doesnt_matter"]

list = [all(x[i] == x[i+1] for i in range(len(x)-1)) for x in zip(*a)]

print a[0][:list.index(0) if list.count(0) > 0 else len(list)]

Salida: my_prefix_

 -1
Author: Patmanizer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-12 13:36:11