Python: cuenta el número de ocurrencias de subcadenas en una cadena

En este artículo, veremos cómo contar la cantidad de ocurrencias, así como las posiciones de las ocurrencias de subcadenas dentro de otra cadena en Python.

Introducción

Una subcadena es una secuencia continua de caracteres dentro de una cadena. Por ejemplo, "subcadena" es una subcadena de "Buscar una subcadena dentro de una cadena".

Las cadenas en Python son matrices de bytes que representan caracteres Unicode y uno de los tipos de datos más utilizados para representar datos en un formato legible por humanos.

En este artículo aprenderemos cómo contar el número de ocurrencias de una subcadena específica dentro de una cadena en Python.

Encuentra todas las apariciones de una subcadena en una cadena usando count()

El método count() de la clase de cadena en realidad hace esto. Devuelve el número de veces que aparece un valor especificado (subcadena) en la cadena. También tiene dos parámetros opcionales: start y end, que indican el inicio y el final del espacio de búsqueda:

1
string.count(value, start, end)

{.icon aria-hidden=“true”}

Nota: El ‘inicio’ predeterminado es ‘0’, y el ‘final’ predeterminado es la longitud de la cadena.

Echemos un vistazo al uso del método, con una oración representativa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Define string and substring
str1 = 'John has 1 apple, Sarah has 2 apples, Mike has 5 apples.'
substr = 'apples'

# Occurences of substring 'apples' in the string
result = str1.count(substr)
print("Number of substring occurrences:", result)

# Occurences of substring 'apples' from index 0 to index 40
start, end = 0, 40
result2 = str1.count(substr, start, end)
print("Number of substring occurrences:", result2)

Esto resulta en:

1
2
Number of substring occurrences: 2
Number of substring occurrences: 1

Es un método muy simple y directo que funciona bien en la mayoría de los casos. Es eficiente y puede ampliarse bien a tamaños de entrada grandes. Por ejemplo, podríamos cargar una gran parte del texto y buscar una palabra común o una palabra vacía que está destinada a estar presente.

También puede simplemente obtener un gran espacio de búsqueda para tener una idea de la eficiencia. Descarguemos 'Romeo and Juliet' de William Shakespeare, del Proyecto Gutenberg, y recuperemos el número de veces que se menciona 'Romeo':

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import time
import requests

txt = requests.get('https://www.gutenberg.org/cache/epub/1513/pg1513.txt').text
print(f"Downloaded {len(txt)} bytes of text...")

start_time = time.time()
count = txt.count('Romeo')
end_time = time.time()

print(f"Time to find all occurences of 'Romeo': {end_time - start_time}s with {count} results")

Esto resulta en:

1
2
Downloaded 167333 bytes of text...
Time to find all occurences of 'Romeo': 0.0s with 153 results

O, incluso si encontramos una palabra mucho más común, como 'a':

1
2
3
4
5
start_time = time.time()
count = txt.count('a')
end_time = time.time()

print(f"Time to find all occurences of 'a': {end_time - start_time}s with {count} results")

El resultado es el mismo:

1
2
Downloaded 167333 bytes of text...
Time to find all occurences of 'Romeo': 0.0s with 8308 results

La mayor parte del tiempo de ejecución lo toma el tiempo que lleva descargar el texto.

{.icon aria-hidden=“true”}

Nota: Este método no devuelve la posición en la cadena en la que se encuentra la subcadena.

Si necesita este conocimiento, ya sea para realizar operaciones de transformación adicionales en las ocurrencias además de contarlas, querrá usar Expresiones regulares para encontrar sus posiciones o verificar casos individuales con startsWith().

Echaremos un vistazo a estos dos casos en las siguientes secciones.

Buscar todas las apariciones y posiciones de una subcadena en una cadena en Python

El método startswith() devuelve True si la cadena comienza con el valor especificado (subcadena) y False si no lo hace. De manera similar al método count(), este método también tiene parámetros opcionales start y end que especifican las posiciones de inicio y final del espacio de búsqueda:

1
string.startswith(value, start, end)

El valor predeterminado de inicio es 0 y el valor predeterminado de fin es la longitud de la cadena.

Usar este método es un poco más complejo, ya que requiere que usemos la comprensión de listas junto con el método en sí, o un bucle for más tradicional. El método startswith() devuelve los índices iniciales de la subcadena. Después de eso, utilizamos la comprensión de listas para iterar a través de todo el espacio de búsqueda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Define string and substring
str1 = 'John has 1 apple, Sarah has 2 apples, Mike has 5 apples.'
substr = 'apples'

# Print original string and substring
print("Original string is:", str1)
print("Substring is:", substr)

# Sse startswith() and list comprehension
# Find all occurrences of a substring within a string
result = [i for i in range(len(str1)) if str1.startswith(substr, i)]

# Print the number of substring occurrences
print("Number of substring occurrences is:", len(result))

# We can also find the starting indices of substrings
print("Starting indices of substrings are: " + str(result))

Esto nos da el número de ocurrencias, como la última vez, pero también las posiciones iniciales de las cadenas mismas. Dado que conocemos la cadena en cuestión y, por lo tanto, su longitud, podemos deducir fácilmente el espacio que ocupa en la cadena de búsqueda:

1
2
3
4
Original string is: John has 1 apple, Sarah has 2 apples, Mike has 5 apples.
Substring is: apples
Number of substring occurrences is: 2
Starting indices of substrings are: [30, 49]

Encuentre todas las apariciones de una subcadena en una cadena en Python usando re.finditer()

La función finditer() es parte de la biblioteca RegEx de Python - re. Se usa más comúnmente para encontrar la ocurrencia de un patrón particular dentro de una cadena dada.

Para habilitar el uso de este método, junto con muchos otros métodos que manejan expresiones RegEx, primero debemos importar la biblioteca de expresiones regulares:

1
re.finditer(pattern, string, flags=0)

If you would like to learn more about Regular Expressions, read our Guía de expresiones regulares en Python!

La función re.finditer() devuelve un iterador que produce objetos coincidentes sobre todas las coincidencias no superpuestas para el patrón RegEx en una cadena. El escaneo se realiza de izquierda a derecha y las coincidencias se devuelven en el orden en que se encuentran. También se incluyen las coincidencias vacías.

Las banderas se pueden usar para habilitar varias características únicas y variaciones de sintaxis (por ejemplo, la bandera re.I o re.IGNORECASE habilita la coincidencia insensible a mayúsculas y minúsculas, la bandera re.A o re.ASCII solo habilita ASCII coincidencia en lugar de la coincidencia completa UNICODE habitual).

Reemplacemos la lista de comprensión de antes con una expresión regular:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import re

# Define string and substring
str1 = 'John has 1 apple, Sarah has 2 apples, Mike has 5 apples.'
substr = 'apples'

# Print original string and substring
print("Original string is:", str1)
print("Substring is:", substr)

# Use re.finditer() to find all substring occurrences
# Using list comprehension we find the start and end indices of every substring occurence
result = [(_.start(), _.end()) for _ in re.finditer(substr, str1)]

# Print number of substrings found
print("Number of substring occurrences is:", len(result))

# Print start and end indices of substring occurrences
print("The start and end indices of the substrings are: " + str(result))

Esto resulta en:

1
2
3
4
Original string is: John has 1 apple, Sarah has 2 apples, Mike has 5 apples.
Substring is: apples
Number of substring occurrences is: 2
The start and end indices of the substrings are: [(30, 36), (49, 55)]

Ahora, no tenemos que sumar manualmente la longitud de las cadenas a los índices iniciales.

Rendimiento de evaluación comparativa {#rendimiento de evaluación comparativa}

Vale la pena señalar que el rendimiento variará según el método que elija. Si bien en todos los casos, el código terminará con bastante rapidez, vale la pena tener en cuenta el rendimiento en espacios de búsqueda realmente grandes.

Usemos estos tres métodos para encontrar todas las instancias del carácter 'a' en 'Romeo y Julieta':

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import re
import time
import requests

txt = requests.get('https://www.gutenberg.org/cache/epub/1513/pg1513.txt').text
print(f"Downloaded {len(txt)} bytes of text...")

start_time_1 = time.time()
result_1 = txt.count('a')
end_time_1 = time.time()

print(f"String.count(): Time to find all occurences of 'a': {end_time_1 - start_time_1}s")

start_time_2 = time.time()
result_2 = [i for i in range(len(txt)) if txt.startswith('a', i)]
end_time_2 = time.time()

print(f"List Comprehensions: Time to find all occurences of 'a': {end_time_2 - start_time_2}s")

start_time_3 = time.time()
result_3 = [(_.start(), _.end()) for _ in re.finditer('a', txt)]
end_time_3 = time.time()

print(f"Regex: Time to find all occurences of 'a': {end_time_3 - start_time_3}s")

Esto resulta en:

1
2
3
String.count(): Time to find all occurences of 'a': 0.0s
List Comprehensions: Time to find all occurences of 'a': 0.031008481979370117s
Regex: Time to find all occurences of 'a': 0.002000093460083008s

El método count() es definitivamente el más eficiente, pero no nos deja saber dónde están las cadenas. Para el conocimiento adicional, las expresiones regulares siguen siendo extremadamente rápidas para esta tarea y más de 10 veces más eficientes que nuestro ciclo de comprensión de lista manual.

Conclusión

Existen varias formas diferentes de resolver este problema, algunas se usan con más frecuencia que otras, según los datos que desee extraer en el proceso.

En el punto de referencia, el método count() superó a los otros dos, pero no nos brinda información sobre dónde se encuentran las subcadenas. Por otro lado, las Expresiones Regulares, aunque más lentas, nos brindan esta información.

Vale la pena señalar que los tres enfoques son excepcionalmente rápidos y pueden analizar una obra maestra literaria completa en busca de una palabra común en una fracción de segundo.