Pitch: 5 pegadinhas ao converter XML de NF-e em planilha (com código Python)
Se você já somou nota cancelada no faturamento ou viu a chave de acesso virar 3,52609E+43 no Excel, este post é para você.
Aviso logo de cara: nós, da Brasil Utils, criamos a ferramenta paga que aparece no fim do post. Mas a maior parte do texto é o que aprendemos escrevendo o parser, e o código abaixo resolve o básico sem ferramenta nenhuma.
O problema
Escritório contábil, e-commerce, integrador de ERP: todo mundo recebe XML de NF-e aos milhares (ZIP do ERP, portal da SEFAZ, e-mail do fornecedor), e alguém precisa disso numa planilha ou num banco. Normalmente em dois níveis: uma linha por nota (totais e impostos) e uma linha por item (NCM, CFOP, quantidades).
1. Sem namespace, o find() não acha nada
Todo elemento está no namespace http://www.portalfiscal.inf.br/nfe. Este script lê uma pasta de XMLs, ignora arquivos quebrados, valida a chave e gera um CSV que abre direto no Excel brasileiro:
import csv, glob
import xml.etree.ElementTree as ET
NS = {"nfe": "http://www.portalfiscal.inf.br/nfe"}
def txt(el, path):
return el.findtext(path, default="", namespaces=NS) if el is not None else ""
def dv_chave(ch43: str) -> int:
# módulo 11, pesos 2 a 9 da direita para a esquerda
pesos = [2, 3, 4, 5, 6, 7, 8, 9]
s = sum(int(d) * pesos[i % 8] for i, d in enumerate(reversed(ch43)))
r = s % 11
return 0 if r < 2 else 11 - r
linhas = []
for caminho in sorted(glob.glob("xmls/*.xml")):
try:
raiz = ET.parse(caminho).getroot()
except ET.ParseError as e:
print("XML inválido, ignorado:", caminho, e)
continue
inf = raiz.find(".//nfe:infNFe", NS)
if inf is None: # evento (cancelamento, CC-e...) ou outro documento
continue
ide, emit = inf.find("nfe:ide", NS), inf.find("nfe:emit", NS)
tot = inf.find("nfe:total/nfe:ICMSTot", NS)
chave = inf.get("Id", "")[3:] # "NFe" + 44 dígitos
linhas.append({
"chave": chave,
"chave_valida": len(chave) == 44 and chave.isdigit()
and dv_chave(chave[:43]) == int(chave[43]),
"modelo": txt(ide, "nfe:mod"), # 55 = NF-e, 65 = NFC-e
"serie": txt(ide, "nfe:serie"),
"numero": txt(ide, "nfe:nNF"),
"emissao": txt(ide, "nfe:dhEmi"),
"emitente_cnpj": txt(emit, "nfe:CNPJ"),
"emitente": txt(emit, "nfe:xNome"),
"valor_nota": txt(tot, "nfe:vNF").replace(".", ","),
})
# "Excel Brasil": ponto e vírgula, vírgula decimal e BOM UTF-8
if linhas:
with open("notas.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=list(linhas[0]), delimiter=";")
w.writeheader()
w.writerows(linhas)
2. Nem todo XML tem a mesma raiz
Aparecem nfeProc (nota com protocolo de autorização), NFe "pelado" (sem protocolo, que pode nem ter sido autorizada) e enviNFe (lote com várias notas). Por isso o script usa .//nfe:infNFe em vez de um caminho fixo. Para separar autorizada, denegada e rejeitada, leia o cStat dentro de protNFe/infProt.
3. Valide a chave de acesso
Os 44 dígitos terminam num DV em módulo 11 (a função dv_chave acima). Vale também conferir a chave contra o conteúdo (UF, AAMM, CNPJ, modelo, série e número), porque XML editado à mão existe.
4. O cancelamento chega em outro arquivo
O cancelamento (evento 110111) vem num procEventoNFe separado. Se você só olha as notas, a cancelada entra no faturamento. Cruze os eventos com as notas pela chave.
5. Os detalhes que mais dão trabalho
- Os impostos por item ficam em grupos variáveis (
ICMS00,ICMS10,ICMSSN102...), então é preciso tratar o grupo de forma genérica, sem fixar o nome da tag. - A reforma tributária já colocou IBS/CBS no layout (NT 2025.002).
- LGPD: NFC-e e nota para consumidor trazem CPF. Pense se você realmente precisa guardá-lo.
- O Excel come zeros à esquerda e transforma a chave em notação científica. O BOM e o
;ajudam, mas na importação marque a coluna da chave como Texto.
A ferramenta (o pitch)
Juntamos tudo isso num Actor na Apify, o NF-e XML Parser to Excel/CSV — Converter XML de Nota Fiscal: https://apify.com/brasil_utils/nfe-xml-parser
Você envia XMLs ou ZIPs (inclusive ZIP dentro de ZIP) e recebe JSON e os CSVs INVOICES.csv, ITEMS.csv e EVENTS.csv, no formato padrão ou no Excel Brasil. Ele traz o status do protocolo, os impostos por item (com IBS/CBS, ST e Simples Nacional), DIFAL, pagamentos, troco e duplicatas, e mascara o CPF por padrão. Lê arquivos em UTF-8, ISO-8859-1 e UTF-16 e processa cerca de 1.000 notas em uns 4 segundos. Custa US$0,003 por nota processada; erros, eventos (cancelamento, CC-e, inutilização) e chaves duplicadas não são cobrados, e se a execução for reiniciada ela continua de onde parou sem cobrar de novo. Roda pelo painel, pela API ou pelo Make, Zapier e n8n. Quer só ver a saída antes de mandar suas notas? O campo de colar XML já vem com uma nota fictícia de demonstração: rodar só com ela cobra 1 nota (US$0,003), e se você enviar arquivos a demo é ignorada.
Se você só precisa de algumas colunas, o script acima resolve. E ficamos curiosos: qual foi o XML mais estranho que já apareceu para vocês? Casos que ainda não tratamos são muito bem-vindos nos comentários.
Quem escreve é o Eduardo, desenvolvedor da Brasil Utils. Respondo pessoalmente aqui nos comentários.