Contar palabras en un archivo¶
El último programa que estudiaremos cuenta cuántas veces aparecen en un archivo de texto un conjunto de palabras indicadas por el usuario.
Copie y pegue este programa en su editor de texto y compílelo.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_LARGO_PALABRA 50
int main(int argc, char **argv) {
int n;
char **palabras;
int *cuentas;
FILE *f;
char palabra_actual[MAX_LARGO_PALABRA];
int i;
if (argc < 3) {
fprintf(stderr, "Uso: %s ARCHIVO PALABRA1 PALABRA2 ...\n", argv[0]);
exit(EXIT_FAILURE);
}
n = argc - 2;
palabras = argv + 2;
cuentas = malloc(argc * sizeof(int));
if (cuentas == NULL) {
fprintf(stderr, "Memoria insuficiente para ejecutar el programa.\n");
exit(EXIT_FAILURE);
}
f = fopen(argv[1], "r");
if (f == NULL) {
fprintf(stderr, "No se pudo abrir el archivo %s\n", argv[1]);
exit(EXIT_FAILURE);
}
for (i = 0; i < n; ++i) {
cuentas[i] = 0;
}
while (!feof(f)) {
fscanf(f, "%s", palabra_actual);
for (i = 0; i < n; ++i) {
if (strcmp(palabra_actual, palabras[i]) == 0) {
cuentas[i]++;
}
}
}
for (i = 0; i < n; ++i) {
printf("%6d\t %s\n", cuentas[i], palabras[i]);
}
fclose(f);
free(cuentas);
exit(EXIT_SUCCESS);
}
El programa contar-palabras
está diseñado
para recibir parámetros por línea de comandos.
Al momento de ejecutar el programa,
usted debe indicar inmediatamente después del nombre del programa
cuál es el archivo que quiere leer,
y cuáles son las palabras que quiere contar:
$ ./contar-palabras archivo.txt perro gato
Para probar el programa,
descargue El Quijote de la Mancha
en formato de texto plano.
El archivo se llama pg2000.txt
;
guárdelo en el mismo directorio
donde está el programa compilado.
Contemos cuántas veces aparecen los nombres del
Quijote, de Sancho Panza y de Dulcinea en el libro:
$ ./contar-palabras pg2000.txt Sancho Dulcinea Quijote
950 Sancho
165 Dulcinea
894 Quijote
Contemos también cuántas veces aparecen los artículos del idioma español en toda la obra:
$ ./contar-palabras pg2000.txt el la los las
7957 el
10200 la
4680 los
3423 las
Pruebe qué ocurre al ejecutar el programa si:
no se le entrega ningún parámetro:
$ ./contar-palabras
se le pasa el archivo pero ninguna palabra:
$ ./contar-palabras pg2000.txt
se le pasa un archivo que no existe:
$ ./contar-palabras no-existe.txt perro gato
Lectura de archivos de texto¶
Ya aprendimos a escribir en un archivo de texto, y ahora veremos cómo leer datos de él.
Primero que todo, hay que abrir el archivo en modo lectura:
FILE *f = fopen("archivo.txt", "r");
Por supuesto,
hay que verificar que f
no es NULL
para asegurarnos que el archivo sí pudo ser abierto.
La manera más sencilla de leer datos desde el archivo
es usar la función fscanf
de la misma manera que usamos scanf
para leer de la entrada estándar.
Como en nuestro programa nos interesa leer palabra por palabra,
usamos el descriptor de formato %s
.
Para comprobar si ya se llegó al final del archivo,
y por lo tanto ya no queda nada más que leer,
se usa la función feof
.
Una manera típica de leer todo el archivo
es hacerlo como lo hicimos en nuestro programa:
un ciclo while
que va verificando antes de cada lectura
si quedan o no cosas por leer:
while (!feof(f)) {
fscanf(f, "%s", s);
/* ... */
}
Arreglos son punteros, punteros son arreglos¶
Parámetros del programa por línea de comandos¶
Para que nuestro programa reciba parámetros
al momento de ejecutarlo,
debemos modificar la declaración de main
para que incluya dos parámetros:
int main(int argc, char **argv) {
}
La variable argc
tomará como valor
la cantidad de argumentos pasados en la línea de comandos,
incluyendo el nombre del programa.
El puntero argv
apunta a un arreglo de argc
strings,
que son precisamente estos parámetros.
(Recordemos que un string es un arreglo de char
,
y que un arreglo es en la práctica un puntero)
Por eso argv
es un puntero a puntero a char
).
Por ejemplo, cuando ejecutamos el programa de la siguiente manera:
$ ./contar-palabras abc.txt azul rojo verde "amarillo patito"
entonces argc
tendrá el valor 6
y los valores del arreglo argv
serán:
argv[0] → "./contar-palabras"
argv[1] → "abc.txt"
argv[2] → "azul"
argv[3] → "rojo"
argv[4] → "verde"
argv[5] → "amarillo patito"
Aritmética de punteros¶
Un puntero es una dirección de memoria, y una dirección de memoria no es más que un entero. ¿Estará permitido entonces aplicar operaciones aritméticas a los punteros para obtener otros punteros?
En C sí es posible hacerlo. Sin embargo, los punteros tienen sus propias reglas para hacer aritmética.
La única operación permitida es «puntero + entero», y el resultado es un puntero del mismo tipo,
Para verificarlo con sus propios ojos, puede ejecutar el siguiente programa:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_LARGO_PALABRA 50
int main(int argc, char **argv) {
int n;
char **palabras;
int *cuentas;
FILE *f;
char palabra_actual[MAX_LARGO_PALABRA];
int i;
if (argc < 3) {
fprintf(stderr, "Uso: %s ARCHIVO PALABRA1 PALABRA2 ...\n", argv[0]);
exit(EXIT_FAILURE);
}
n = argc - 2;
palabras = argv + 2;
cuentas = malloc(argc * sizeof(int));
if (cuentas == NULL) {
fprintf(stderr, "Memoria insuficiente para ejecutar el programa.\n");
exit(EXIT_FAILURE);
}
f = fopen(argv[1], "r");
if (f == NULL) {
fprintf(stderr, "No se pudo abrir el archivo %s\n", argv[1]);
exit(EXIT_FAILURE);
}
for (i = 0; i < n; ++i) {
cuentas[i] = 0;
}
while (!feof(f)) {
fscanf(f, "%s", palabra_actual);
for (i = 0; i < n; ++i) {
if (strcmp(palabra_actual, palabras[i]) == 0) {
cuentas[i]++;
}
}
}
for (i = 0; i < n; ++i) {
printf("%6d\t %s\n", cuentas[i], palabras[i]);
}
fclose(f);
free(cuentas);
exit(EXIT_SUCCESS);
}
Un char
ocupa un byte en la memoria.
Por lo tanto,
p + 1
apuntará a un byte más que p
.
Un float
ocupa cuatro bytes.
Luego,
q + 1
apuntará a cuatro bytes más allá de q
.
La aritmética de punteros es útil
cuando hay arreglos involucrados.
Si p
apunta a arreglo[0]
,
entonces p + 1
apunta a arreglo[1]
,
independientemente del tipo del arrego.
En otras palabras,
p + 1
siempre apunta a lo que hay en la memoria
inmediatamente después de lo apuntado por p
.
En nuestro contador de palabras,
contamos desde el principio con un arreglo con todos los parámetros del programa.
Pero las palabras que interesan
están sólo desde el tercer parámetro en adelante.
En vez de declarar un nuevo arreglo
(con el consiguiente uso extra de memoria)
y copiar allí las palabras,
simplemente introducimos el puntero palabras
que apunta al tercer elemento de argv
.
Hacer esto es muy fácil gracias a la aritmética de punteros:
palabras = argv + 2;
Desde esta línea en adelante,
palabras
y argv
se ven como dos arreglos que comparten su memoria.
palabras[0]
es lo mismo que argv[2]
:
┌──────────┐ ┌──────────┐
│ │◂─────┼────● │ argv
├──────────┤ └──────────┘
│ │
├──────────┤ ┌──────────┐
│ │◂─────┼────● │ palabras
├──────────┤ └──────────┘
│ │
├──────────┤
│ │
├──────────┤
│ │
├──────────┤
│ │
└──────────┘
En C siempre se cumple que a[i]
es lo mismo que *(a + i)
.
¿Puede darse cuenta de por qué?
Esta relación debería resultarle natural
después de estudiar arreglos, punteros y su aritmética.
Reserva de memoria dinámica¶
Ejercicios¶
El programa es incapaz de distinguir cuando una palabra que está siendo buscada aparece en mayúsculas, o tiene pegada a ella un signo de puntuación.
Por ejemplo, para este archivo test.txt
:
Da da da? Da da da da (da da) da. Da Da!
vamos a obtener esta salida:
$ ./contar-palabras test.txt da
4 da
- Modifique el programa para que cuente cada palabra independiente de si aparece con mayúsculas o minúsculas en el archivo.
- Modifique el programa para que cuente cada palabra incluso si aparece precedida o sucedida de un signo de puntuación.