Projeto Contador de Palavras

A Ana escreveu uma crônica sobre um dia na padaria para o jornalzinho do bairro e ficou curiosa: qual palavra ela mais repetiu? Contar à mão, num texto de 150 palavras, já cansa; num livro, seria impossível. Neste projeto você escreve um programa que lê o texto de um arquivo, separa as palavras, conta quantas vezes cada uma aparece e mostra as 10 mais usadas, com uma barrinha para cada. No caminho aparecem os problemas de verdade: a vírgula grudada na palavra, o "O" maiúsculo que não é igual ao "o", as palavras pequenas que enchem a contagem.

Você vai praticar

O que vamos construir

Este é o resultado final, no computador da Ana, contando a crônica do arquivo cronica.txt:

$ python3 contador.py
Palavras: 148, sem as comuns: 88
As 10 mais usadas:
pão          9 #########
café         5 #####
forno        3 ###
padaria      2 ##
quente       2 ##
seis         2 ##
cheiro       2 ##
rua          2 ##
pede         2 ##
dez          2 ##

"Pão" ganhou com folga, e "café" vem em segundo. Faz sentido para uma crônica de padaria. Vamos chegar lá em quatro versões, cada uma acrescentando uma coisa só:

  1. Ler o arquivo e separar as palavras (open, read, split).
  2. Limpar: minúsculas e sem pontuação (lower, replace).
  3. Contar com um dicionário e ordenar (get, sorted, lambda).
  4. Counter, palavras comuns e barras (Counter, most_common, conjunto).

Antes de começar: o texto

No seu computador, salve o texto abaixo num arquivo chamado cronica.txt, na mesma pasta do programa (com o nano, por exemplo, ou num editor de texto). Nas caixas Experimente desta página, ele já vem pronto numa aba ao lado do programa: clique na aba cronica.txt para ver (e mudar) o texto. Se você mudar o texto, as contagens mudam, e a missão pode deixar de bater.

A Padaria Pão Quente abre às seis. Antes do sol, o pão já está no forno, e o cheiro de pão quente chega à rua inteira.
Dona Rosa é a primeira cliente: pede dez pães, um café e "o de sempre". O café da padaria é forte; o pão, macio.
Depois vem o Seu Joaquim. Ele pede café, pão de queijo e conversa. Conversa sobre o tempo, sobre o time, sobre o preço do café!
— O pão subiu? — pergunta ele.
— O pão não, Seu Joaquim. Subiu o café — responde a Ana, rindo.
Às dez, o bolo de milho sai do forno. Quem passa na rua sente o cheiro e entra. O bolo acaba antes do almoço; o pão, antes do jantar.
No fim do dia, a Ana fecha o caixa, conta o pão que sobrou e anota tudo no caderno... Amanhã, às seis, o forno acende de novo.

Versão 1 · Ler o arquivo e separar as palavras

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

palavras = texto.split()
print(f"Caracteres: {len(texto)}")
print(f"Palavras: {len(palavras)}")
print(palavras[:12])
with open("cronica.txt", encoding="utf-8") as arquivo:
Abre o arquivo para leitura. O with fecha o arquivo sozinho no fim do bloco. O encoding="utf-8" diz como as letras estão gravadas, para os acentos virem certos (veja Quando dá errado).
texto = arquivo.read()
Lê o arquivo inteiro de uma vez, como um texto só, com as quebras de linha dentro (\n).
texto.split()
Corta o texto nos espaços e nas quebras de linha e devolve uma lista com os pedaços.
palavras[:12]
As 12 primeiras, para dar uma olhada (aula Índices e Fatias, que vale também para listas).
$ python3 contador.py
Caracteres: 716
Palavras: 152
['A', 'Padaria', 'Pão', 'Quente', 'abre', 'às', 'seis.', 'Antes', 'do', 'sol,', 'o', 'pão']

152 palavras. Mas olhe a lista: 'seis.' veio com o ponto, 'sol,' com a vírgula, e há 'Pão' e 'pão'. Para o Python, 'pão', 'Pão' e 'pão,' são três textos diferentes, e contariam separado. É o que a versão 2 resolve.

Missão: o programa só mostra quantos caracteres o texto tem. Separe as palavras com split() e mostre também Palavras: … com a quantidade.

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

print(f"Caracteres: {len(texto)}")
A Padaria Pão Quente abre às seis. Antes do sol, o pão já está no forno, e o cheiro de pão quente chega à rua inteira.
Dona Rosa é a primeira cliente: pede dez pães, um café e "o de sempre". O café da padaria é forte; o pão, macio.
Depois vem o Seu Joaquim. Ele pede café, pão de queijo e conversa. Conversa sobre o tempo, sobre o time, sobre o preço do café!
— O pão subiu? — pergunta ele.
— O pão não, Seu Joaquim. Subiu o café — responde a Ana, rindo.
Às dez, o bolo de milho sai do forno. Quem passa na rua sente o cheiro e entra. O bolo acaba antes do almoço; o pão, antes do jantar.
No fim do dia, a Ana fecha o caixa, conta o pão que sobrou e anota tudo no caderno... Amanhã, às seis, o forno acende de novo.
with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

palavras = texto.split()
print(f"Caracteres: {len(texto)}")
print(f"Palavras: {len(palavras)}")

Versão 2 · Limpar: minúsculas e sem pontuação

Duas limpezas, antes de separar: tudo em minúsculas, para "Pão" e "pão" virarem a mesma palavra; e cada sinal de pontuação trocado por um espaço, para a vírgula desgrudar da palavra. No fim, o programa confere se sobrou alguma "palavra" com algo que não é letra.

PONTUACAO = '.,;:!?"—'

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

texto = texto.lower()
for sinal in PONTUACAO:
    texto = texto.replace(sinal, " ")

palavras = texto.split()
print(f"Palavras: {len(palavras)}")
print(palavras[:12])
com_sinais = [p for p in palavras if not p.isalpha()]
print(f"Palavras com sinais: {com_sinais}")
PONTUACAO = '.,;:!?"—'
Os sinais que aparecem na crônica, num texto só. O último é o travessão dos diálogos (—), que é diferente do hífen (-). O texto está entre aspas simples porque tem uma aspa dupla dentro.
texto.lower()
Devolve o texto em minúsculas. Funciona com acentos: "PÃO" vira "pão".
for sinal in PONTUACAO:
Um texto também pode ser percorrido com for, letra por letra (aqui, sinal por sinal).
texto.replace(sinal, " ")
Troca cada ocorrência do sinal por um espaço. Por que espaço, e não nada? Porque em "seis.Antes" (sem espaço depois do ponto) as duas palavras grudariam. O split() depois ignora os espaços a mais.
[p for p in palavras if not p.isalpha()]
As palavras que não são feitas só de letras. isalpha() aceita letras acentuadas: "pão".isalpha() é True.
$ python3 contador.py
Palavras: 148
['a', 'padaria', 'pão', 'quente', 'abre', 'às', 'seis', 'antes', 'do', 'sol', 'o', 'pão']
Palavras com sinais: []

Agora são 148: os quatro travessões soltos, que o split() contava como "palavras", sumiram. E a lista de palavras com sinais está vazia: a limpeza pegou tudo. Veja o que acontece quando a lista de sinais esquece o ! e o —:

$ python3 contador.py
Palavras: 152
['a', 'padaria', 'pão', 'quente', 'abre', 'às', 'seis', 'antes', 'do', 'sol', 'o', 'pão']
Palavras com sinais: ['café!', '—', '—', '—', '—']

O 'café!' contaria separado do 'café', e os travessões virariam "palavras". A conferência no fim do programa serve justamente para pegar isso.

E o string.punctuation?

O módulo string tem uma lista pronta de sinais:

import string

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

Só que são apenas os sinais do teclado americano: o travessão —, as reticências … e as aspas curvas “ ” não estão lá. E ela tem o hífen e o apóstrofo, que em português às vezes fazem parte da palavra (guarda-chuva, d'água). Por isso o projeto usa a sua própria lista, com os sinais que o texto tem.

Missão: a lista PONTUACAO esqueceu dois sinais, e sobraram "palavras" com sinais. Rode, veja quais são, e acrescente os sinais que faltam até a lista ficar vazia. (Dica: copie o travessão de dentro do próprio texto, na aba cronica.txt.)

PONTUACAO = '.,;:?"'

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

texto = texto.lower()
for sinal in PONTUACAO:
    texto = texto.replace(sinal, " ")

palavras = texto.split()
print(f"Palavras: {len(palavras)}")
print(palavras[:12])
com_sinais = [p for p in palavras if not p.isalpha()]
print(f"Palavras com sinais: {com_sinais}")
A Padaria Pão Quente abre às seis. Antes do sol, o pão já está no forno, e o cheiro de pão quente chega à rua inteira.
Dona Rosa é a primeira cliente: pede dez pães, um café e "o de sempre". O café da padaria é forte; o pão, macio.
Depois vem o Seu Joaquim. Ele pede café, pão de queijo e conversa. Conversa sobre o tempo, sobre o time, sobre o preço do café!
— O pão subiu? — pergunta ele.
— O pão não, Seu Joaquim. Subiu o café — responde a Ana, rindo.
Às dez, o bolo de milho sai do forno. Quem passa na rua sente o cheiro e entra. O bolo acaba antes do almoço; o pão, antes do jantar.
No fim do dia, a Ana fecha o caixa, conta o pão que sobrou e anota tudo no caderno... Amanhã, às seis, o forno acende de novo.
PONTUACAO = '.,;:!?"—'

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

texto = texto.lower()
for sinal in PONTUACAO:
    texto = texto.replace(sinal, " ")

palavras = texto.split()
print(f"Palavras: {len(palavras)}")
print(palavras[:12])
com_sinais = [p for p in palavras if not p.isalpha()]
print(f"Palavras com sinais: {com_sinais}")

Versão 3 · Contar com um dicionário e ordenar

Agora, a contagem. A ideia é a de uma folha com duas colunas, palavra e quantidade: para cada palavra do texto, a Ana procura a linha dela na folha e soma 1; se ainda não tem linha, cria uma com 1. Um dicionário é exatamente essa folha.

PONTUACAO = '.,;:!?"—'

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

texto = texto.lower()
for sinal in PONTUACAO:
    texto = texto.replace(sinal, " ")
palavras = texto.split()

contagem = {}
for palavra in palavras:
    contagem[palavra] = contagem.get(palavra, 0) + 1

print(f"Palavras: {len(palavras)}, diferentes: {len(contagem)}")
mais_usadas = sorted(contagem.items(), key=lambda par: par[1], reverse=True)
for palavra, vezes in mais_usadas[:10]:
    print(f"{palavra:<10} {vezes:>3}")
contagem = {}
A folha começa vazia: um dicionário sem nenhuma chave.
contagem.get(palavra, 0)
O número que já está anotado para a palavra, ou 0 se ela ainda não apareceu (aula Dicionários). É o que evita o KeyError (veja Quando dá errado).
contagem[palavra] = … + 1
Anota o número novo, criando a chave na primeira vez.
len(contagem)
Quantas chaves o dicionário tem, ou seja, quantas palavras diferentes.
contagem.items()
Os pares (palavra, vezes), como tuplas.
key=lambda par: par[1]
Ordena os pares pelo item 1 de cada um, o número de vezes (aula lambda).
reverse=True
Do maior para o menor.
mais_usadas[:10]
Só os 10 primeiros pares.
{palavra:<10} {vezes:>3}
A palavra à esquerda em 10 posições e o número à direita em 3: as colunas ficam retas.
$ python3 contador.py
Palavras: 148, diferentes: 77
o           19
pão          9
do           6
e            5
de           5
café         5
a            4
às           3
antes        3
no           3

Das 148 palavras, 77 são diferentes. A campeã é o, com 19, seguida de pão, com 9. Quando duas palavras empatam (e, de e café, com 5), o sorted as deixa na ordem em que apareceram pela primeira vez no dicionário, que é a ordem do texto.

Repare que pães conta separado de pão: para o programa, são textos diferentes. Juntar plural e singular é bem mais difícil, e fica fora deste projeto.

E sem o lower()?

Tirando a linha texto = texto.lower(), as maiúsculas do começo das frases separam as contagens:

$ python3 contador.py
Palavras: 148, diferentes: 88
o           15
pão          8
do           6
e            5
de           5
café         5
O            4
forno        3
a            3
sobre        3

São 88 "palavras diferentes" em vez de 77: o (15) e O (4) contaram separado, e o Pão do nome da padaria saiu do pão (8 em vez de 9). Somando, 15 + 4 dá os 19 da versão com lower().

Missão: o sorted está sem critério, e a lista sai em ordem alfabética (a, abre, acaba…). Acrescente o key com uma lambda que pega o número de vezes, e o reverse=True, para aparecerem as 10 mais usadas.

PONTUACAO = '.,;:!?"—'

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

texto = texto.lower()
for sinal in PONTUACAO:
    texto = texto.replace(sinal, " ")
palavras = texto.split()

contagem = {}
for palavra in palavras:
    contagem[palavra] = contagem.get(palavra, 0) + 1

print(f"Palavras: {len(palavras)}, diferentes: {len(contagem)}")
mais_usadas = sorted(contagem.items())
for palavra, vezes in mais_usadas[:10]:
    print(f"{palavra:<10} {vezes:>3}")
A Padaria Pão Quente abre às seis. Antes do sol, o pão já está no forno, e o cheiro de pão quente chega à rua inteira.
Dona Rosa é a primeira cliente: pede dez pães, um café e "o de sempre". O café da padaria é forte; o pão, macio.
Depois vem o Seu Joaquim. Ele pede café, pão de queijo e conversa. Conversa sobre o tempo, sobre o time, sobre o preço do café!
— O pão subiu? — pergunta ele.
— O pão não, Seu Joaquim. Subiu o café — responde a Ana, rindo.
Às dez, o bolo de milho sai do forno. Quem passa na rua sente o cheiro e entra. O bolo acaba antes do almoço; o pão, antes do jantar.
No fim do dia, a Ana fecha o caixa, conta o pão que sobrou e anota tudo no caderno... Amanhã, às seis, o forno acende de novo.
PONTUACAO = '.,;:!?"—'

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()

texto = texto.lower()
for sinal in PONTUACAO:
    texto = texto.replace(sinal, " ")
palavras = texto.split()

contagem = {}
for palavra in palavras:
    contagem[palavra] = contagem.get(palavra, 0) + 1

print(f"Palavras: {len(palavras)}, diferentes: {len(contagem)}")
mais_usadas = sorted(contagem.items(), key=lambda par: par[1], reverse=True)
for palavra, vezes in mais_usadas[:10]:
    print(f"{palavra:<10} {vezes:>3}")

Versão 4 · Counter, palavras comuns e barras

A versão 3 funciona, mas o pódio é das palavras pequenas: o, do, e, de. Elas aparecem em qualquer texto e não dizem nada sobre ele. A versão final ignora essas palavras, troca a contagem manual pelo Counter, que já faz isso pronto, e desenha uma barra para cada palavra.

from collections import Counter

PONTUACAO = '.,;:!?"—'
IGNORAR = {
    "a", "o", "as", "os", "e", "é", "de", "do", "da", "no", "na",
    "à", "às", "um", "uma", "que", "ele", "sobre", "antes",
}


def ler_palavras(nome_arquivo):
    with open(nome_arquivo, encoding="utf-8") as arquivo:
        texto = arquivo.read().lower()
    for sinal in PONTUACAO:
        texto = texto.replace(sinal, " ")
    return texto.split()


palavras = ler_palavras("cronica.txt")
importantes = [p for p in palavras if p not in IGNORAR]
contagem = Counter(importantes)

print(f"Palavras: {len(palavras)}, sem as comuns: {len(importantes)}")
print("As 10 mais usadas:")
for palavra, vezes in contagem.most_common(10):
    print(f"{palavra:<10} {vezes:>3} {'#' * vezes}")
from collections import Counter
Traz o Counter ("contador") do módulo collections, que vem com o Python (aula O Módulo collections).
IGNORAR = {…}
Um conjunto com as palavras comuns (aula Conjuntos). Perguntar p not in IGNORAR num conjunto é rápido, mesmo com milhares de palavras. A lista foi escolhida olhando a saída da versão 3; para outro texto, você ajustaria.
def ler_palavras(nome_arquivo): … return texto.split()
A leitura e a limpeza das versões 1 e 2, agora numa função que devolve a lista pronta.
[p for p in palavras if p not in IGNORAR]
Uma lista nova, só com as palavras que interessam (aula List Comprehension).
Counter(importantes)
Conta tudo de uma vez: é um dicionário palavra → vezes, igual ao da versão 3.
contagem.most_common(10)
"As mais comuns": os 10 pares com mais vezes, já em ordem. Faz o trabalho do sorted com lambda e da fatia [:10].
'#' * vezes
Repete o # tantas vezes quanto a palavra apareceu: a barra (aula Juntar e Repetir Textos). Dentro da f-string, o '#' usa aspas simples porque a f-string usa aspas duplas.
$ python3 contador.py
Palavras: 148, sem as comuns: 88
As 10 mais usadas:
pão          9 #########
café         5 #####
forno        3 ###
padaria      2 ##
quente       2 ##
seis         2 ##
cheiro       2 ##
rua          2 ##
pede         2 ##
dez          2 ##

Tirando as palavras comuns, sobraram 88 das 148, e o pódio agora conta a história do texto: pão, café, forno. A partir do quarto lugar, há várias palavras empatadas com 2; o most_common as mostra na ordem em que apareceram primeiro no texto.

Missão: no fim do programa, mostre também a palavra mais longa (sem as comuns), no formato Mais longa: primeira (8 letras). Dica: max(contagem, key=len) devolve a chave com mais letras; num empate, a primeira que apareceu.

from collections import Counter

PONTUACAO = '.,;:!?"—'
IGNORAR = {
    "a", "o", "as", "os", "e", "é", "de", "do", "da", "no", "na",
    "à", "às", "um", "uma", "que", "ele", "sobre", "antes",
}


def ler_palavras(nome_arquivo):
    with open(nome_arquivo, encoding="utf-8") as arquivo:
        texto = arquivo.read().lower()
    for sinal in PONTUACAO:
        texto = texto.replace(sinal, " ")
    return texto.split()


palavras = ler_palavras("cronica.txt")
importantes = [p for p in palavras if p not in IGNORAR]
contagem = Counter(importantes)

print(f"Palavras: {len(palavras)}, sem as comuns: {len(importantes)}")
print("As 10 mais usadas:")
for palavra, vezes in contagem.most_common(10):
    print(f"{palavra:<10} {vezes:>3} {'#' * vezes}")
A Padaria Pão Quente abre às seis. Antes do sol, o pão já está no forno, e o cheiro de pão quente chega à rua inteira.
Dona Rosa é a primeira cliente: pede dez pães, um café e "o de sempre". O café da padaria é forte; o pão, macio.
Depois vem o Seu Joaquim. Ele pede café, pão de queijo e conversa. Conversa sobre o tempo, sobre o time, sobre o preço do café!
— O pão subiu? — pergunta ele.
— O pão não, Seu Joaquim. Subiu o café — responde a Ana, rindo.
Às dez, o bolo de milho sai do forno. Quem passa na rua sente o cheiro e entra. O bolo acaba antes do almoço; o pão, antes do jantar.
No fim do dia, a Ana fecha o caixa, conta o pão que sobrou e anota tudo no caderno... Amanhã, às seis, o forno acende de novo.
from collections import Counter

PONTUACAO = '.,;:!?"—'
IGNORAR = {
    "a", "o", "as", "os", "e", "é", "de", "do", "da", "no", "na",
    "à", "às", "um", "uma", "que", "ele", "sobre", "antes",
}


def ler_palavras(nome_arquivo):
    with open(nome_arquivo, encoding="utf-8") as arquivo:
        texto = arquivo.read().lower()
    for sinal in PONTUACAO:
        texto = texto.replace(sinal, " ")
    return texto.split()


palavras = ler_palavras("cronica.txt")
importantes = [p for p in palavras if p not in IGNORAR]
contagem = Counter(importantes)

print(f"Palavras: {len(palavras)}, sem as comuns: {len(importantes)}")
print("As 10 mais usadas:")
for palavra, vezes in contagem.most_common(10):
    print(f"{palavra:<10} {vezes:>3} {'#' * vezes}")

mais_longa = max(contagem, key=len)
print(f"Mais longa: {mais_longa} ({len(mais_longa)} letras)")

Formas diferentes de limpar e de contar

Limpar a pontuação

Três jeitos que dão as mesmas 148 palavras com esta crônica:

import re

PONTUACAO = '.,;:!?"—'

with open("cronica.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read().lower()

limpo1 = texto
for sinal in PONTUACAO:
    limpo1 = limpo1.replace(sinal, " ")

tabela = str.maketrans(PONTUACAO, " " * len(PONTUACAO))
limpo2 = texto.translate(tabela)

palavras3 = re.findall(r"\w+", texto)

print(len(limpo1.split()), len(limpo2.split()), len(palavras3))
print(limpo1.split() == limpo2.split() == palavras3)
$ python3 limpar.py
148 148 148
True
FormaQuando usar
for sinal in PONTUACAO: texto = texto.replace(sinal, " ")A mais fácil de ler, e a do projeto. Para textos do tamanho de uma crônica, a velocidade não importa
str.maketrans + translateTroca todos os sinais numa passada só, com uma "tabela de troca". Mais rápida para textos enormes
re.findall(r"\w+", texto)Em vez de tirar o que não é palavra, pega o que é: sequências de letras e algarismos, com acento. Curta e poderosa, mas exige saber expressões regulares (aula Expressões Regulares)

Contar

from collections import Counter

palavras = ["o", "pão", "o", "café", "pão", "o"]

contagem1 = {}
for palavra in palavras:
    contagem1[palavra] = contagem1.get(palavra, 0) + 1

contagem2 = Counter(palavras)

print(contagem1)
print(contagem2)
print(contagem2.most_common(2))
print(contagem1.get("bolo", 0), contagem2["bolo"])
$ python3 contar.py
{'o': 3, 'pão': 2, 'café': 1}
Counter({'o': 3, 'pão': 2, 'café': 1})
[('o', 3), ('pão', 2)]
0 0
FormaQuando usar
Dicionário com get(palavra, 0) + 1Para entender como a contagem funciona, e quando não dá para importar nada
Dicionário com if palavra in contagem: … else: …Faz o mesmo, em mais linhas. Comum em código antigo
Counter(lista)Nos programas de verdade. Conta numa linha, tem o most_common e devolve 0 para uma palavra que não apareceu (veja o desafio)

Quando dá errado

O arquivo não está lá (ou o nome está errado)
with open("cronicas.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()
$ python3 nome_errado.py
Traceback (most recent call last):
  File "/home/ana/nome_errado.py", line 1, in <module>
    with open("cronicas.txt", encoding="utf-8") as arquivo:
         ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'cronicas.txt'

"Arquivo não encontrado: não existe o arquivo ou a pasta 'cronicas.txt'". O arquivo se chama cronica.txt, sem o s. O mesmo erro aparece quando o nome está certo, mas você rodou o programa de outra pasta: o open procura o arquivo na pasta onde o terminal está. Como consertar: confira o nome com ls e rode o programa da pasta onde estão os dois arquivos.

Somar 1 numa chave que ainda não existe
palavras = ["o", "pão", "o", "café"]
contagem = {}
for palavra in palavras:
    contagem[palavra] += 1
print(contagem)
$ python3 keyerror.py
Traceback (most recent call last):
  File "/home/ana/keyerror.py", line 4, in <module>
    contagem[palavra] += 1
    ~~~~~~~~^^^^^^^^^
KeyError: 'o'

KeyError: 'o': "erro de chave". O += 1 precisa ler o valor atual antes de somar, e na primeira vez que o o aparece não há valor nenhum para ler. Como consertar: contagem[palavra] = contagem.get(palavra, 0) + 1, ou o if palavra in contagem, ou o Counter.

split(" ") no lugar de split()
texto = "o pão  quente\nchega"
print(texto.split())
print(texto.split(" "))
$ python3 split_espaco.py
['o', 'pão', 'quente', 'chega']
['o', 'pão', '', 'quente\nchega']

Sem nada nos parênteses, o split() corta em qualquer espaço em branco (espaço, quebra de linha, tabulação) e junta os repetidos. Com " ", ele corta só no espaço, um por um: os dois espaços seguidos geram uma "palavra" vazia (''), e a quebra de linha deixa 'quente\nchega' grudado. Seria a palavra "vazia" mais contada da crônica. Como consertar: split(), sem argumento.

O arquivo foi gravado em outra codificação

Arquivos antigos, de programas do Windows, às vezes não estão em UTF-8, e sim em Latin-1. A Ana gravou um assim, de propósito, para ver o que acontece:

with open("antigo.txt", "w", encoding="latin-1") as arquivo:
    arquivo.write("Pão de queijo e café\n")
with open("antigo.txt", encoding="utf-8") as arquivo:
    texto = arquivo.read()
print(texto)
$ python3 latin1_criar.py
$ python3 latin1_ler.py
Traceback (most recent call last):
  File "/home/ana/latin1_ler.py", line 2, in <module>
    texto = arquivo.read()
  File "<frozen codecs>", line 325, in decode
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe3 in position 1: invalid continuation byte

"O decodificador UTF-8 não consegue entender o byte 0xe3 na posição 1". A posição 1 é o ã de "Pão": em Latin-1 ele é gravado como um byte só (0xe3), que não forma uma letra válida em UTF-8. O trecho <frozen codecs> é código de dentro do próprio Python; o que importa é a linha 2 do seu programa, o read(). Como consertar: abra com a codificação certa, encoding="latin-1":

with open("antigo.txt", encoding="latin-1") as arquivo:
    texto = arquivo.read()
print(texto)
$ python3 latin1_ok.py
Pão de queijo e café

(A linha em branco no fim é o \n do arquivo mais a quebra de linha do próprio print.) Por isso o projeto sempre escreve o encoding="utf-8": deixa claro o que o programa espera, em vez de depender da configuração do computador.

Esquecer as limpezas

Não dá erro: dá uma contagem errada. Sem o lower(), O e o contam separado (o quadro da versão 3); sem a pontuação certa, café! e café também (a conferência da versão 2). Sempre que uma contagem parecer baixa demais, olhe a lista de palavras antes de contar.


Desafio extra: perguntar por uma palavra

A Ana quer digitar uma palavra e saber quantas vezes ela aparece. Com o Counter fica fácil, porque ele responde 0 para uma palavra que não está no texto, em vez de dar KeyError como um dicionário comum:

from collections import Counter

PONTUACAO = '.,;:!?"—'


def ler_palavras(nome_arquivo):
    with open(nome_arquivo, encoding="utf-8") as arquivo:
        texto = arquivo.read().lower()
    for sinal in PONTUACAO:
        texto = texto.replace(sinal, " ")
    return texto.split()


contagem = Counter(ler_palavras("cronica.txt"))
procurada = input("Qual palavra? ").strip().lower()
print(f"{procurada}: {contagem[procurada]} vez(es)")
$ python3 contador.py
Qual palavra? café
café: 5 vez(es)
$ python3 contador.py
Qual palavra?   Pão                                « com espaços e maiúscula
pão: 9 vez(es)
$ python3 contador.py
Qual palavra? biscoito
biscoito: 0 vez(es)

O .strip().lower() limpa o que foi digitado do mesmo jeito que o texto foi limpo. Sem ele, Pão não seria encontrado, porque todas as chaves estão em minúsculas.

Missão: a caixa vai digitar Pão, com P maiúsculo, e o programa responde Pão: 0 vez(es). Limpe o que foi digitado para que a resposta seja pão: 9 vez(es).

from collections import Counter

PONTUACAO = '.,;:!?"—'


def ler_palavras(nome_arquivo):
    with open(nome_arquivo, encoding="utf-8") as arquivo:
        texto = arquivo.read().lower()
    for sinal in PONTUACAO:
        texto = texto.replace(sinal, " ")
    return texto.split()


contagem = Counter(ler_palavras("cronica.txt"))
procurada = input("Qual palavra? ")
print(f"{procurada}: {contagem[procurada]} vez(es)")
A Padaria Pão Quente abre às seis. Antes do sol, o pão já está no forno, e o cheiro de pão quente chega à rua inteira.
Dona Rosa é a primeira cliente: pede dez pães, um café e "o de sempre". O café da padaria é forte; o pão, macio.
Depois vem o Seu Joaquim. Ele pede café, pão de queijo e conversa. Conversa sobre o tempo, sobre o time, sobre o preço do café!
— O pão subiu? — pergunta ele.
— O pão não, Seu Joaquim. Subiu o café — responde a Ana, rindo.
Às dez, o bolo de milho sai do forno. Quem passa na rua sente o cheiro e entra. O bolo acaba antes do almoço; o pão, antes do jantar.
No fim do dia, a Ana fecha o caixa, conta o pão que sobrou e anota tudo no caderno... Amanhã, às seis, o forno acende de novo.
from collections import Counter

PONTUACAO = '.,;:!?"—'


def ler_palavras(nome_arquivo):
    with open(nome_arquivo, encoding="utf-8") as arquivo:
        texto = arquivo.read().lower()
    for sinal in PONTUACAO:
        texto = texto.replace(sinal, " ")
    return texto.split()


contagem = Counter(ler_palavras("cronica.txt"))
procurada = input("Qual palavra? ").strip().lower()
print(f"{procurada}: {contagem[procurada]} vez(es)")

Referência rápida do projeto

EscritaPara que serviu
with open(nome, encoding="utf-8") as arquivo: + arquivo.read()Ler o arquivo inteiro como um texto só
texto.lower()Tudo em minúsculas, para Pão e pão contarem juntos
texto.replace(sinal, " ")Desgrudar a pontuação das palavras
texto.split()Separar as palavras (em qualquer espaço em branco)
contagem.get(p, 0) + 1Contar num dicionário sem KeyError
sorted(d.items(), key=lambda par: par[1], reverse=True)Ordenar os pares pelo número de vezes
Counter(lista) / .most_common(10)Contar numa linha / as 10 mais usadas
[p for p in palavras if p not in IGNORAR]Tirar as palavras comuns
f"{palavra:<10} {vezes:>3} {'#' * vezes}"Colunas alinhadas e a barra
SintomaCausa provável
FileNotFoundError: … 'cronica.txt'Nome errado, ou o programa rodou de outra pasta
KeyError: 'o'contagem[p] += 1 sem a chave existir: use get ou Counter
UnicodeDecodeError: 'utf-8' codec can't decode byte …O arquivo não está em UTF-8: tente encoding="latin-1"
O e o contados separadoFaltou o lower()
café! e café contados separadoFaltou um sinal na lista de pontuação
Uma "palavra" vazia ('') na contagemsplit(" ") no lugar de split()
O pódio é o, de, eFaltou ignorar as palavras comuns

Exercícios

1. Separar as palavras

Complete o método que corta o texto nos espaços:

palavras = texto.

2. Contar sem KeyError

Complete o valor reserva do get:

contagem[palavra] = contagem.get(palavra, ) + 1

3. As cinco mais

Complete para pegar as 5 palavras mais usadas de um Counter:

contagem.

4. Por que a pontuação é trocada por um espaço, e não apagada?

5. Sem o lower(), a crônica deu 88 palavras diferentes; com ele, 77. Por quê?

6. Com contagem = Counter(palavras), o que dá contagem["biscoito"], se "biscoito" não aparece no texto?


Resumo

  • Leia o arquivo com with open(…, encoding="utf-8") e read(); separe as palavras com split(), sem argumento.
  • Antes de contar, limpe: lower() para as maiúsculas, replace (por espaço) para a pontuação. Confira o resultado: palavra com sinal, ou vazia, é sinal de limpeza incompleta.
  • Um dicionário conta com contagem.get(p, 0) + 1; ordene os pares com sorted(…, key=lambda par: par[1], reverse=True).
  • O Counter faz tudo isso numa linha, tem o most_common(n) e devolve 0 para o que não apareceu.
  • Palavras comuns (o, de, e) enchem o pódio: guarde-as num conjunto e filtre com uma list comprehension.
  • FileNotFoundError é nome ou pasta errada; UnicodeDecodeError é arquivo em outra codificação; KeyError é chave que ainda não existe.