Strings y texto

Introducción

Los strings (cadenas de texto) son simplemente el tipo de dato para el texto. Hasta ahora has usado strings, pero sin aprender mucho sobre sus detalles. Es momento de profundizar en ellos, aprender cómo funcionan y dominar algunas de las potentes herramientas de manipulación de strings que tienes a tu disposición.

Este capítulo se ha beneficiado del Python String Cook Book y del Python Data Science Handbook de Jake VanderPlas.

Ten en cuenta que existen métodos más potentes para trabajar con strings llamados expresiones regulares, pero se tratarán en otro capítulo.

Crear strings

Ya hemos creado strings de pasada en capítulos anteriores, pero sin entrar en detalles. En primer lugar, puedes crear un string usando comillas simples (') o dobles ("). Conviene ser consistente, aunque no importe cuál uses, pero los formateadores automáticos de código suelen preferir ". Si tienes una comilla dentro de un string, usa ' dentro de él.

string_one = "This is a string"
string_two = (
    "If I want to include a 'quote' inside a string, I use double quotes on the outside"
)

Los strings son de tipo str:

type(string_one)
str

Los strings en Python se pueden indexar, así que podemos extraer ciertos caracteres usando corchetes para indicar qué posiciones queremos.

var = "banana"
var[:3]
'ban'

Los trucos habituales de slicing (rebanado) que se aplican a las listas también funcionan con strings, es decir, puedes obtener las posiciones que quieras usando la sintaxis var[start:stop:step]. Aquí tienes un ejemplo que obtiene un carácter sí y otro no del string, empezando por la 2.ª posición.

var[1::2]
'aaa'

Ten en cuenta que los strings, al igual que las tuplas como (1, 2, 3) pero a diferencia de las listas como [1, 2, 3], son inmutables. Esto significa que comandos como var[0] = "B" producirán un error. Si quieres cambiar un solo carácter, tendrás que reemplazar el string completo. En este ejemplo, el comando para hacerlo sería var = "Banana".

Al igual que con las listas, puedes obtener la longitud de un string con len():

len(var)
6

Puedes concatenar strings con el operador +:

string_one + ". " + string_two + "."
"This is a string. If I want to include a 'quote' inside a string, I use double quotes on the outside."

Fíjate en que añadimos caracteres extra para que la frase tuviera sentido. Otra forma de lograr lo mismo, que escala de manera más eficiente a muchas palabras o frases (si las tienes en una lista), es:

". ".join([string_one, string_two])
"This is a string. If I want to include a 'quote' inside a string, I use double quotes on the outside"

Tres funciones útiles que conviene conocer son upper(), lower() y title(). Veamos qué hacen

var = "input TEXT"
var_list = [var.upper(), var.lower(), var.title()]
print(var_list)
['INPUT TEXT', 'input text', 'Input Text']

Ten en cuenta que hay muchas funciones integradas para trabajar con strings en Python; puedes encontrar una lista completa aquí.

TipEjercicio

Invierte el string "gnirts desrever a si sihT" usando operaciones de indexación.

Ya que estamos usando print(), tiene algunos trucos. Si tenemos una lista, podemos imprimir sus elementos con un separador dado:

print(*var_list, sep="; and \n")
INPUT TEXT; and 
input text; and 
Input Text

(Pronto veremos qué hace ‘’.) Para convertir variables de otros tipos en strings, usa la función str(), por ejemplo

(
    "A boolean is either "
    + str(True)
    + " or "
    + str(False)
    + ", there are only "
    + str(2)
    + " options."
)
'A boolean is either True or False, there are only 2 options.'

En este ejemplo, dos variables boolean y una variable entera se convirtieron en strings. str() generalmente deduce de forma inteligente cómo quieres convertir tu variable de tipo no string en un string. Puedes pasarle a str() una variable o un valor literal.

f-strings

El ejemplo anterior es bastante verboso. Otra forma de combinar strings con variables es mediante f-strings. Un f-string sencillo tiene este aspecto:

variable = 15.32399
print(f"You scored {variable}")
You scored 15.32399

Esto es similar a llamar a str sobre la variable y usar + para concatenar, pero mucho más corto de escribir. También puedes añadir expresiones a los f-strings:

print(f"You scored {variable**2}")
You scored 234.8246695201

Esto también funciona con funciones; al fin y al cabo, **2 es solo una función con su propia sintaxis especial.

En este ejemplo, el número de la puntuación que obtuvimos tenía muchos decimales (probablemente) poco interesantes. ¿Cómo pulimos la salida impresa? Puedes pasar más información al f-string para obtener la salida con el formato exacto que quieras. Supongamos que queremos dos decimales y un signo (aunque siempre escribes + en el formato, el signo sale como + o - según el valor):

print(f"You scored {variable:+.2f}")
You scored +15.32

Existe toda una gama de opciones de formato para números, como se muestra en la siguiente tabla:

Número Formato Salida Descripción
15.32347 {:.2f} 15.32 Float con 2 decimales
15.32347 {:+.2f} +15.32 Float con 2 decimales y signo
-1 {:+.2f} -1.00 Float con 2 decimales y signo
15.32347 {:.0f} 15 Float sin decimales
3 {:0>2d} 03 Rellenar número con ceros (relleno a la izquierda, ancho 2)
3 {:*<4d} 3*** Rellenar número con * (relleno a la derecha, ancho 4)
13 {:*<4d} 13** Rellenar número con * (relleno a la derecha, ancho 4)
1000000 {:,} 1,000,000 Número con coma como separador
0.25 {:.1%} 25.0% Formato de porcentaje
1000000000 {:.2e} 1.00e+09 Notación exponencial
12 {:10d} 12 Alineado a la derecha (por defecto, ancho 10)
12 {:<10d} 12 Alineado a la izquierda (ancho 10)
12 {:^10d} 12 Centrado (ancho 10)

Además de usar esta página de forma interactiva mediante los enlaces de Colab y Binder en la parte superior, o de descargarla y usarla en tu propio ordenador, puedes experimentar con algunas de estas opciones en este enlace.

Caracteres especiales y cómo escapar strings

Python tiene un módulo string que incluye algunos strings y caracteres integrados útiles. Por ejemplo

import string

string.punctuation
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'

te da todos los signos de puntuación,

string.ascii_letters
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

devuelve todas las letras básicas de la codificación ‘ASCII’ (con las variantes .ascii_lowercase y .ascii_uppercase), y

string.digits
'0123456789'

te da los números del 0 al 9. Por último, aunque menos impresionante visualmente, string.whitespace da un string que contiene todos los distintos tipos (¡hay más de uno!) de espacio en blanco.

Hay otros caracteres especiales; de hecho, ya conocimos el más famoso de ellos: “” para nueva línea. Para imprimir realmente “” tenemos que ‘escapar’ la barra invertida añadiendo otra barra invertida:

print("Here is a \n new line")
print("Here is an \\n escaped new line ")
Here is a 
 new line
Here is an \n escaped new line 

La siguiente tabla muestra los comandos de escape más importantes:

Código Resultado
\' Comilla simple (útil si usas ' para los strings)
\" Comilla doble (útil si usas " para los strings)
\\ Barra invertida
\n Nueva línea
\r Retorno de carro
\t Tabulación

Aquí tienes un ejemplo más complicado:

print("a\tb\nA\tB")
a   b
A   B

Raw strings

Los strings con el prefijo r, como r’…’ y r”…“, se llaman raw strings (strings sin procesar) y tratan las barras invertidas  como caracteres literales en lugar de caracteres especiales.

print(r"a\tb\nA\tB")
a\tb\nA\tB

Limpiar texto

A menudo querrás hacer cambios en el texto con el que trabajas. En esta sección veremos las distintas opciones para hacerlo.

Reemplazar substrings

Una tarea habitual con texto es reemplazar un substring dentro de un string más largo. Supongamos que tienes una variable de tipo string var. Puedes usar .replace(old_text, new_text) para hacerlo.

"Value is objective".replace("objective", "subjective")
'Value is subjective'

Como con cualquier variable de un tipo específico (aquí, string), esto también funcionaría con variables:

text = "Value is objective"
old_substr = "objective"
new_substr = "subjective"
text.replace(old_substr, new_substr)
'Value is subjective'

Ten en cuenta que .replace() realiza un reemplazo exacto y, por tanto, distingue entre mayúsculas y minúsculas.

Reemplazar caracteres con translate

Un carácter es un elemento individual dentro de un string, como la ‘l’ en ‘equilibrium’. Siempre puedes contar el número de caracteres de una variable string llamada var usando len(var). Un método muy rápido para reemplazar caracteres individuales en un string es str.translate().

Reemplazar caracteres es extremadamente útil en ciertas situaciones, sobre todo cuando quieres eliminar toda la puntuación antes de hacer otros análisis de texto. Para ello puedes usar el string integrado string.punctuation.

Veamos cómo usarlo para eliminar todas las vocales de un texto. Con disculpas a la economista Lisa Cook, usaremos el resumen de Cook (2011) como el texto que modificaremos, y primero crearemos un diccionario de traducciones de vocales a nada, es decir, "".

example_text = "Much recent work has focused on the influence of social capital on innovative outcomes. Little research has been done on disadvantaged groups who were often restricted from participation in social networks that provide information necessary for invention and innovation. Unique new data on African American inventors and patentees between 1843 and 1930 permit an empirical investigation of the relation between social capital and economic outcomes. I find that African Americans used both traditional, i.e., occupation-based, and nontraditional, i.e., civic, networks to maximize inventive output and that laws constraining social-capital formation are most negatively correlated with economically important inventive activity."
vowels = "aeiou"
translation_dict = {x: "" for x in vowels}
translation_dict
{'a': '', 'e': '', 'i': '', 'o': '', 'u': ''}

Ahora convertimos nuestro diccionario en un traductor de strings y lo aplicamos a nuestro texto:

translator = example_text.maketrans(translation_dict)
example_text.translate(translator)
'Mch rcnt wrk hs fcsd n th nflnc f scl cptl n nnvtv tcms. Lttl rsrch hs bn dn n dsdvntgd grps wh wr ftn rstrctd frm prtcptn n scl ntwrks tht prvd nfrmtn ncssry fr nvntn nd nnvtn. Unq nw dt n Afrcn Amrcn nvntrs nd ptnts btwn 1843 nd 1930 prmt n mprcl nvstgtn f th rltn btwn scl cptl nd cnmc tcms. I fnd tht Afrcn Amrcns sd bth trdtnl, .., ccptn-bsd, nd nntrdtnl, .., cvc, ntwrks t mxmz nvntv tpt nd tht lws cnstrnng scl-cptl frmtn r mst ngtvly crrltd wth cnmclly mprtnt nvntv ctvty.'
TipEjercicio

Usa translate para reemplazar con espacios todos los signos de puntuación de la siguiente oración: “The well-known story I told at the conferences [about hypocondria] in Boston, New York, Philadelphia,…and Richmond went as follows: It amused people who knew Tommy to hear this; however, it distressed Suzi when Tommy (1982–2019) asked, "How can I find out who yelled, ‘Fire!’ in the theater?" and then didn’t wait to hear Missy give the answer—‘Dick Tracy.’”

En general, str.translate es muy rápido para reemplazar caracteres individuales en strings. Pero también puedes hacerlo con una list comprehension y un join() de la lista resultante, así:

"".join(
    [
        ch
        for ch in "Example. string. with- excess_ [punctuation]/,"
        if ch not in string.punctuation
    ]
)
'Example string with excess punctuation'

Dividir strings

Si quieres dividir un string en una posición concreta, hay dos formas rápidas de hacerlo. La primera es usar métodos de indexación, que funcionan bien si sabes en qué posición quieres dividir el texto, p. ej.

"This is a sentence and we will split it at character 18"[:18]
'This is a sentence'

A continuación, podemos usar la función integrada split(), que devuelve una lista con las partes separadas en los lugares donde aparece un substring dado:

"This is a sentence. And another sentence. And a third sentence".split(".")
['This is a sentence', ' And another sentence', ' And a third sentence']

Ten en cuenta que el carácter usado para dividir el string se elimina de la lista de strings resultante. Veamos un ejemplo usando un string para dividir en lugar de un solo carácter:

"This is a sentence. And another sentence. And a third sentence".split("sentence")
['This is a ', '. And another ', '. And a third ', '']

Otra función útil que conviene conocer es splitlines(), que divide un string en los saltos de línea y devuelve las partes como una lista.

count y find

Hagamos un conteo sencillo de palabras dentro de un texto usando str.count(). Usaremos como texto la primera estrofa de la sextina ‘A Miracle for Breakfast’ de Elizabeth Bishop.

text = "At six o'clock we were waiting for coffee, \n waiting for coffee and the charitable crumb \n that was going to be served from a certain balcony \n --like kings of old, or like a miracle. \n It was still dark. One foot of the sun \n steadied itself on a long ripple in the river."
word = "coffee"
print(f'The word "{word}" appears {text.count(word)} times.')
The word "coffee" appears 2 times.

Por su parte, find() devuelve la posición en la que aparece una palabra o un carácter concreto.

text.find(word)
35

Podemos comprobarlo usando el número que obtenemos y un poco de indexación de strings:

text[text.find(word) : text.find(word) + len(word)]
'coffee'

Pero este no es el único lugar donde aparece la palabra ‘coffee’. Si queremos encontrar la última aparición, se hace así:

text.rfind(word)
57

Trabajar con múltiples strings

Hemos visto cómo trabajar con strings individuales. Pero a menudo queremos trabajar con un grupo de strings, lo que también se conoce como corpus, es decir, una colección de textos. Puede ser una colección de palabras, oraciones, párrafos o alguna agrupación basada en un dominio (por ejemplo, descripciones de puestos de trabajo). Como cualquier otro objeto de Python, puedes poner strings en una lista (u otro iterable).

Y, por suerte, muchos de los métodos que hemos visto aplicados a un único string pueden escalarse directamente a cientos, miles o millones de strings usando pandas u otras herramientas. Este escalado se logra mediante la vectorización, por analogía con pasar de un único valor (un escalar) a múltiples valores en una lista (un vector).

Como ejemplo muy mínimo, aquí tienes la conversión a mayúscula inicial de nombres vectorizada mediante una comprensión de listas:

[name.capitalize() for name in ["ada", "adam", "elinor", "grace", "jean"]]
['Ada', 'Adam', 'Elinor', 'Grace', 'Jean']

Se puede usar una serie de pandas en lugar de una lista. Primero creemos la serie:

import pandas as pd

dfs = pd.Series(
    ["ada lovelace", "adam smith", "elinor ostrom", "grace hopper", "jean bartik"],
    dtype="string",
)
dfs
0     ada lovelace
1       adam smith
2    elinor ostrom
3     grace hopper
4      jean bartik
dtype: string

Ahora usamos la sintaxis series.str.function para modificar la serie de texto:

dfs.str.title()
0     Ada Lovelace
1       Adam Smith
2    Elinor Ostrom
3     Grace Hopper
4      Jean Bartik
dtype: string

Si tuviéramos un dataframe y no una serie, la sintaxis cambiaría para referirse solo a la columna de interés, así:

df = pd.DataFrame(dfs, columns=["names"])
df["names"].str.title()
0     Ada Lovelace
1       Adam Smith
2    Elinor Ostrom
3     Grace Hopper
4      Jean Bartik
Name: names, dtype: string

La siguiente tabla muestra una lista no exhaustiva de los métodos de strings disponibles en pandas.

Función (precedida de .str.) Qué hace
len() Longitud del string.
lower() Pone el string en minúsculas.
upper() Pone el string en mayúsculas.
capitalize() Pone en mayúscula la primera letra del string.
swapcase() Intercambia mayúsculas y minúsculas en un string.
translate() Devuelve una copia del string en la que cada carácter se ha transformado mediante una tabla de traducción dada.
ljust() Rellena un string por la izquierda (por defecto, con espacios)
rjust() Rellena un string por la derecha (por defecto, con espacios)
center() Rellena de modo que el string quede centrado (por defecto, con espacios)
zfill() Rellena con ceros
strip() Elimina los espacios en blanco iniciales y finales
rstrip() Elimina los espacios en blanco finales
lstrip() Elimina los espacios en blanco iniciales
find() Devuelve el índice más bajo en los datos donde aparece un substring
split() Divide el string usando como delimitador el substring indicado
isupper() Comprueba si el string está en mayúsculas
isdigit() Comprueba si el string está compuesto por dígitos
islower() Comprueba si el string está en minúsculas
startswith() Comprueba si el string empieza por un substring dado

Las expresiones regulares también se pueden escalar con pandas. La siguiente tabla muestra expresiones regulares vectorizadas.

Función Qué hace
match() Llama a re.match() sobre cada elemento y devuelve un boolean.
extract() Llama a re.match() sobre cada elemento y devuelve los grupos coincidentes como strings.
findall() Llama a re.findall() sobre cada elemento
replace() Reemplaza las apariciones del patrón por otro string
contains() Llama a re.search() sobre cada elemento y devuelve un boolean
count() Cuenta las apariciones del patrón
split() Equivalente a str.split(), pero acepta regex
rsplit() Equivalente a str.rsplit(), pero acepta regex

Veamos un par de ellas en acción. Primero, dividir según un substring dado:

df["names"].str.split(" ")
0     [ada, lovelace]
1       [adam, smith]
2    [elinor, ostrom]
3     [grace, hopper]
4      [jean, bartik]
Name: names, dtype: object

Es bastante habitual querer dividir strings y guardar los resultados en nuevas columnas del dataframe. Puedes especificar un número (máximo) de divisiones mediante el kwarg n= y obtener las columnas usando expand

df["names"].str.split(" ", n=2, expand=True)
0 1
0 ada lovelace
1 adam smith
2 elinor ostrom
3 grace hopper
4 jean bartik
TipEjercicio

Usando operaciones vectorizadas, crea una nueva columna con la posición del índice donde aparece la primera vocal en cada fila de la columna names.

Aquí tienes un ejemplo de uso de una función regex con pandas:

df["names"].str.extract("(\w+)", expand=False)
<>:1: SyntaxWarning: invalid escape sequence '\w'
<>:1: SyntaxWarning: invalid escape sequence '\w'
/var/folders/8n/n8x6tb9n5tz72hhnwdf7l04w0000gn/T/ipykernel_75795/3354940721.py:1: SyntaxWarning: invalid escape sequence '\w'
  df["names"].str.extract("(\w+)", expand=False)
0       ada
1      adam
2    elinor
3     grace
4      jean
Name: names, dtype: string

Hay algunas operaciones vectorizadas más sobre strings que resultan útiles.

Método Descripción
get() Indexa cada elemento
slice() Extrae una porción de cada elemento
slice_replace() Reemplaza una porción de cada elemento por el valor indicado
cat() Concatena strings
repeat() Repite valores
normalize() Devuelve la forma Unicode del string
pad() Añade espacios en blanco a la izquierda, a la derecha o a ambos lados de los strings
wrap() Divide strings largos en líneas de longitud menor que un ancho dado
join() Une los strings de cada elemento de la Series con el separador indicado
get_dummies() Extrae variables dummy como un dataframe

Los métodos get() y slice() dan acceso a los elementos de las listas devueltas por split(). Aquí tienes un ejemplo que combina split() y get():

df["names"].str.split().str.get(-1)
0    lovelace
1       smith
2      ostrom
3      hopper
4      bartik
Name: names, dtype: object

Si tenemos una columna con etiquetas separadas por un símbolo, podemos usar la función get_dummies() para separarlas. Por ejemplo, creemos un dataframe con una sola columna que mezcla etiquetas de materia y de nacionalidad:

df = pd.DataFrame(
    {
        "names": [
            "ada lovelace",
            "adam smith",
            "elinor ostrom",
            "grace hopper",
            "jean bartik",
        ],
        "tags": ["uk; cs", "uk; econ", "usa; econ", "usa; cs", "usa; cs"],
    }
)
df
names tags
0 ada lovelace uk; cs
1 adam smith uk; econ
2 elinor ostrom usa; econ
3 grace hopper usa; cs
4 jean bartik usa; cs

Si ahora usamos str.get_dummies() y dividimos por ;, obtenemos un dataframe de variables dummy.

df["tags"].str.get_dummies(";")
cs econ uk usa
0 1 0 1 0
1 0 1 1 0
2 0 1 0 1
3 1 0 0 1
4 1 0 0 1

Leer texto

Archivo de texto

Si tienes simplemente un archivo de texto plano, puedes leerlo así:

fname = 'book.txt'
with open(fname, encoding='utf-8') as f:
    text_of_book = f.read()

También puedes leer un archivo de texto directamente en un dataframe de pandas usando

df = pd.read_csv('book.txt', delimiter = "\n")

En el ejemplo anterior, el delimitador entre las distintas filas del dataframe se establece como “”, que significa nueva línea, pero puedes usar el delimitador que prefieras.

TipEjercicio

Descarga el archivo ‘smith_won.txt’ usando este enlace (haz clic derecho y elige “guardar como”). Luego lee el texto con pandas.

Archivo CSV

Los archivos CSV ya están divididos en filas. La forma más sencilla, con diferencia, de leer archivos csv es usar pandas,

df = pd.read_csv('book.csv')

Recuerda que pandas también puede leer muchos otros tipos de archivo.