947 41K 5

Canarim Dataset

Como construí um corpus de larga escala com páginas em português do Common Crawl convertidas para Markdown.

Comparação entre uma página jornalística original e o texto extraído para o dataset Canarim

Canarim Dataset: do Common Crawl a 342 milhões de documentos em português

A escassez de dados web em português, abertos e imediatamente consumíveis, motivou a criação do Canarim. Publiquei um corpus com 342.818.651 registros, distribuídos em 192 shards Apache Arrow e cerca de 1,09 TB. Sua principal contribuição é oferecer uma camada de aquisição e extração em larga escala que preserva mais estrutura documental do que texto plano.[1]

Na primeira versão, apliquei deduplicação por URL e converti HTML para Markdown com Trafilatura; ainda não implementei deduplicação textual nem filtragem abrangente de conteúdo potencialmente nocivo.[1] Por isso, apresento o Canarim como uma camada de aquisição e extração em larga escala, não como um corpus pronto para treinamento irrestrito.

PropriedadeValor publicado
Registros no split train342.818.651
Tamanho publicadoaproximadamente 1,09 TB
Shards de dados192 arquivos .arrow
Colunas7
FormatoApache Arrow streaming
DOI10.57967/hf/1605

Motivação e linhagem do corpus

O Common Crawl captura bilhões de recursos da web e publica respostas HTTP brutas em arquivos WARC, metadados derivados em WAT e texto simples em WET. Parti da camada WARC e preservei warc_filename, warc_record_offset e warc_record_length, além da URL e do instante de captura. Assim, mantive a ligação entre o Markdown distribuído e a captura arquivada, algo que um corpus contendo somente texto perderia.[2]

Renderizando diagrama...

Usei múltiplas iterações do Common Crawl, selecionei documentos em português, dedupliquei inicialmente por URL e converti o HTML em Markdown com Trafilatura.[1] Mantive no esquema os campos do URL Index do Common Crawl: content_languages vem da predição do CLD2, enquanto arquivo, offset e comprimento localizam o membro WARC comprimido.[3]

Na publicação inicial, não detalhei a lista completa de crawls, a consulta exata de seleção, os códigos HTTP e MIME aceitos, o limiar de identificação linguística, as regras de normalização de URL, a versão e os parâmetros do Trafilatura, as contagens intermediárias nem a política para escolher uma captura entre URLs repetidas.

Seleção dos crawls e identificação de idioma

No exemplo que publiquei, usei CC-MAIN-2023-06, capturado em fevereiro de 2023. Também informei que reuni múltiplas iterações, mas não preservei no card a lista completa de crawls.[1] O identificador de cada linha pode ser derivado de warc_filename, cujo prefixo segue crawl-data/CC-MAIN-AAAA-NN/, e então contado. Fazer isso sobre um prefixo do stream produz somente uma amostra ordenada; obter um censo exige percorrer os 342 milhões de registros.

Preservei content_languages=por, código ISO 639-3 para português, a partir do índice de origem, cujo campo languages é previsto pelo CLD2.[3] Não registrei a execução de um segundo identificador e não publiquei probabilidades nem matriz de confusão. Identificação automática erra em páginas curtas, multilíngues, boilerplate, português próximo do galego e texto com muito código. Trabalhos como CCNet mostram por que identificação de idioma, deduplicação e avaliação de qualidade precisam ser etapas mensuradas separadamente em corpora monolíngues.[4]

WARC, offsets e recuperação por intervalo

WARC é um formato de contêiner: cada registro concatena cabeçalhos WARC e um bloco arbitrário de octetos. Uma resposta web normalmente inclui cabeçalhos HTTP e o corpo recebido. O padrão exige campos como WARC-Type, WARC-Date, WARC-Record-ID e Content-Length; ele também admite digests, truncamento e registros revisit para conteúdo duplicado.[5]

No índice do Common Crawl, o trio filename + offset + length torna possível recuperar um registro, sem baixar um WARC de aproximadamente 1 GB. O intervalo inclusivo é:

inicio = warc_record_offset

fim = warc_record_offset + warc_record_length - 1

warc_record_length é o comprimento do membro comprimido apontado pelo índice, não o Content-Length do payload HTML. Um pedido HTTP com Range: bytes=inicio-fim deve retornar 206 Partial Content; o fragmento pode então ser descomprimido como gzip.[3]

import gzip
import requests
 
def recuperar_warc(registro: dict) -> bytes:
    inicio = registro["warc_record_offset"]
    fim = inicio + registro["warc_record_length"] - 1
    url = "https://data.commoncrawl.org/" + registro["warc_filename"]
 
    resposta = requests.get(
        url,
        headers={"Range": f"bytes={inicio}-{fim}", "Accept-Encoding": "identity"},
        timeout=60,
    )
    if resposta.status_code != 206:
        raise RuntimeError(f"Range não atendido: HTTP {resposta.status_code}")
    return gzip.decompress(resposta.content)

Esse mecanismo permite revisar a extração, verificar o encoding e reprocessar o conteúdo com outro extrator. O conteúdo recuperado é entrada não confiável: não se deve executar JavaScript, carregar recursos remotos ou renderizar HTML sem sandbox. Uma captura também pode estar truncada pelo crawler; o próprio padrão prevê WARC-Truncated, mas o esquema Canarim não expõe esse campo diretamente.[5]

Deduplicação por URL e por conteúdo

Descrevi a deduplicação publicada apenas como “using URLs”.[1] Não registrei se a chave era a string literal, uma URL normalizada ou uma forma SURT, nem se a operação ocorreu dentro de cada crawl ou globalmente. Também não registrei qual captura preservei quando a mesma URL apareceu em datas diferentes. Essas escolhas importam: preservar a captura mais recente favorece atualidade; preservar a primeira favorece corte temporal; manter todas preserva evolução, mas aumenta repetição.

Mesmo uma deduplicação perfeita por URL não detecta o mesmo artigo em domínios espelho, parâmetros de rastreamento, caminhos alternativos, versões para impressão ou páginas diferentes preenchidas por um template comum. Para uma futura deduplicação textual, a base matemática clássica é transformar cada documento normalizado em um conjunto de shingles de k tokens. Para documentos A e B, a similaridade de Jaccard é:

J(A, B) = |A ∩ B| / |A ∪ B|.

Calcular todas as comparações custa ordem quadrática e é inviável em 342 milhões de documentos. MinHash cria uma assinatura compacta cuja propriedade central é P[minhash(A) = minhash(B)] = J(A, B); várias permutações aproximam Jaccard, e LSH organiza as assinaturas em bandas para gerar apenas pares candidatos.[6] Um pipeline prudente faria hash exato primeiro, MinHash/LSH depois e registraria limiar, tamanho de shingle, normalização e regra de retenção. Deduplicação deve ocorrer antes da separação entre treino e avaliação para evitar vazamento. Lee et al. associaram near-duplicates a memorização, sobreposição com validação e treinamento menos eficiente; RefinedWeb demonstrou a relevância prática de filtragem e deduplicação em Common Crawl.[7][8]

Isso é uma recomendação para uma próxima versão, não uma alegação de que MinHash foi aplicado ao Canarim publicado.

Extração de HTML para Markdown

Trafilatura procura o conteúdo textual principal e separa-o de navegação, anúncios e outros elementos periféricos. Sua API oferece saídas TXT, Markdown, JSON, HTML e XML, controles de precisão/recall, inclusão de comentários, tabelas, links e formatação, além de deduplicação interna opcional. O artigo da ferramenta reporta avaliação comparativa em páginas reais; ainda assim, desempenho médio não garante acerto em todo gênero ou idioma.[9]

Configurei a extração para preservar títulos, subtítulos, negrito e itálico em Markdown e descartar comentários e informação não essencial.[10] No exemplo assets/extracted_text.md, extraído de uma reportagem do G1, preservei parágrafos, citações, legendas e cabeçalhos como ## Homenagens e ## Atraso; no preview, coloquei a página original ao lado do resultado. Esse é um caso selecionado, não uma amostra aleatória nem uma métrica de qualidade. Como não registrei versão, argumentos de chamada e conjunto de avaliação, ainda não consigo reproduzir exatamente toda a coluna text.

Uma medida útil para futuras versões é o rendimento de extração:

rendimento_documental = N(textos não vazios publicados) / N(candidatos WARC selecionados).

Também convém medir quanto conteúdo útil permanece depois da extração e quais tipos de página falham. Nenhum denominador intermediário foi publicado, portanto o rendimento não pode ser calculado para a versão atual.

Esquema e escala

CampoTipoSemântica e cautela
urlstringURL capturada; não implica disponibilidade atual nem licença uniforme.
content_languagesstringCódigo linguístico, demonstrado como por; classificação automática pode errar.
warc_filenamestringCaminho relativo do WARC no Common Crawl e fonte do identificador do crawl.
warc_record_offsetint64Primeiro byte do membro WARC comprimido.
warc_record_lengthint64Número de bytes a solicitar no range HTTP.
textstringConteúdo principal convertido para Markdown.
crawl_timestampstringInstante de captura em UTC; é string, não timestamp tipado no schema.

O tamanho publicado de aproximadamente 1,09 TB descreve a tabela Arrow completa, não o volume do texto UTF-8 nem uma contagem de tokens. Documentos e shards variam de tamanho, e o streaming evita tratar esse total como requisito de memória.[1]

Armazenamento, streaming e Parquet

Os dados publicados são Arrow, formato colunar usado internamente pela biblioteca datasets. A documentação do Hugging Face distingue o streaming Arrow do formato IPC/Feather e explica que streaming=True produz um IterableDataset: registros chegam durante a iteração, sem materializar o terabyte inteiro localmente. Não há acesso aleatório barato; filter() é preguiçoso e, em Arrow, um predicado Python pode precisar ler todos os registros anteriores até encontrar resultados.[11]

Parquet seria uma alternativa interessante para uma republicação, não uma descrição do estado atual. Estatísticas por row group permitem predicate pushdown, e projeção de colunas pode evitar ler text quando a consulta quer apenas URLs ou crawls. A documentação de streaming do Hugging Face expõe columns e filters especificamente para Parquet.[11] A conversão, porém, exigiria armazenamento adicional, dimensionamento de row groups e uma versão documentada; simplesmente chamar Arrow de Parquet apagaria uma diferença operacional importante.

Carregamento e filtros reproduzíveis

Para reprodutibilidade, registre a versão da biblioteca e a data de acesso ao dataset. O exemplo abaixo limita a leitura a um shard. Ele ainda pode transferir blocos substanciais antes de produzir a primeira linha.

python -m pip install "datasets==4.8.4"
from datasets import load_dataset
 
amostra = load_dataset(
    "dominguesm/canarim",
    split="train",
    data_files="train/data-00000-of-00192.arrow",
    streaming=True,
)
 
for registro in amostra.take(3):
    print(registro["crawl_timestamp"], registro["url"])
    print((registro["text"] or "")[:300])

Para o stream completo, remova data_files. O filtro seguinte é deliberadamente uma política local de exploração, não um filtro atribuído à construção do Canarim. urlsplit() evita aceitar, por engano, hosts como g1.globo.com.exemplo.org.

from urllib.parse import urlsplit
from datasets import load_dataset
 
dados = load_dataset(
    "dominguesm/canarim",
    split="train",
    streaming=True,
)
 
def exemplo_util(registro):
    texto = (registro["text"] or "").strip()
    host = (urlsplit(registro["url"]).hostname or "").lower()
    dominio_ok = host == "g1.globo.com" or host.endswith(".g1.globo.com")
    return registro["content_languages"] == "por" and dominio_ok and len(texto) >= 500
 
for registro in dados.filter(exemplo_util).take(10):
    print(registro["url"], len(registro["text"]))

O limite de 500 caracteres serve somente para tornar o exemplo verificável. Em pesquisa, registre a data de acesso, a versão da biblioteca, o código do filtro, a ordem dos shards, a seed e as contagens de entrada e saída. shuffle(seed=..., buffer_size=...) embaralha shards e uma janela, não cria uma permutação uniforme global; a escolha do buffer altera a amostragem observada.[11]

Controles de qualidade e relação com o Canarim-7B

Antes de usar o corpus em treinamento, recomendo que a pipeline consumidora meça, por domínio e crawl: texto vazio, comprimento em caracteres ou tokens, razão alfabética, repetição de linhas, boilerplate, encoding inválido, idioma por um segundo classificador, URLs dominantes e duplicatas exatas ou aproximadas. Deve ainda separar benchmarks e documentos de avaliação antes da deduplicação final. Esses controles são recomendações reproduzíveis inspiradas na literatura, não filtros inéditos atribuídos ao dataset.[4][7][8]

No Canarim-7B, usei 16 bilhões de tokens do subconjunto português de CC-MAIN-2023-23 para continuar o pré-treinamento do Llama 2 7B.[12] Esse corpus não é a publicação dominguesm/canarim de 342 milhões de registros. Os dois projetos compartilham a origem geral no Common Crawl, mas não apresento o dataset Canarim como corpus direto daquele treinamento.

Privacidade, toxicidade, direitos autorais e licença

Conteúdo acessível na web não é automaticamente livre de direitos nem seguro. Pode conter nomes, emails, telefones, endereços, credenciais acidentais, dados sensíveis, difamação, discurso de ódio, pornografia, desinformação e material envolvendo menores. A política do Common Crawl reconhece que coleta dados pessoais disponíveis publicamente, e seus termos exigem respeito a direitos autorais, privacidade e termos dos proprietários do conteúdo; também proíbem colher informação pessoal para uso separado do conteúdo rastreado.[13] Na primeira versão do Canarim, não registei remoção de PII, classificador de toxicidade, bloqueio de domínios ou mecanismo próprio de exclusão.

Também publiquei uma inconsistência de licença: declarei CC BY 4.0 nos metadados e no card do Hugging Face, mas incluí CC BY-SA 4.0 no arquivo LICENSE do GitHub.[1][10] BY exige atribuição; BY-SA acrescenta compartilhamento pela mesma licença para material adaptado. Nenhuma das duas pode conceder direitos que eu não possua sobre páginas de terceiros, e ambas ressalvam direitos de privacidade, personalidade e outros direitos.[14] Até eu harmonizar essas camadas e esclarecer o alcance sobre estrutura, metadados e conteúdo, recomendo tratar a licença como ambígua, preservar proveniência por URL, consultar assessoria jurídica e não supor autorização universal para redistribuição ou uso comercial.

Decisões, práticas de reúso e próximas versões

Publiquei esquema, escala, shards, um exemplo de extração e a proveniência WARC. Na versão inicial, não registei a lista completa de crawls, população candidata, rendimento, limitações de download/parser, versão e configuração do Trafilatura, normalização e escopo da deduplicação de URL, distribuição de idiomas, domínios e comprimentos, duplicação textual, PII, toxicidade, licenças por documento, remoções ou sobreposição com benchmarks.

Em uma próxima versão, pretendo manter um manifesto versionado por estágio e publicar contagens de aceitação e rejeição, sem ocultar a política aplicada:

Renderizando diagrama...

O valor duradouro do Canarim está na combinação de escala, português, texto estruturado e ponte de volta ao WARC. Transformá-lo em dado de treinamento responsável requer preservar essa rastreabilidade enquanto se acrescentam medições, deduplicação textual, governança e clareza jurídica.

Conclusão

Com o Canarim, materializei uma camada de aquisição e extração de páginas em português em escala de centenas de milhões de registros. Ao publicar esse volume em Arrow, aprendi a separar claramente aquisição, curadoria e os filtros que pretendo ampliar em versões futuras.

Referências

1. Domingues, M. Canarim. Hugging Face Dataset, DOI 10.57967/hf/1605: card, API, arquivos e histórico. Consulta em 24 jul. 2026.

2. Common Crawl Foundation. Get Started: WARC, WAT and WET e Merity, S. Navigating the WARC file format, 2014.

3. Common Crawl Foundation. CDXJ Index, URL Index e Index to WARC Files and URLs in Columnar Format.

4. Wenzek, G. et al. CCNet: Extracting High Quality Monolingual Datasets from Web Crawl Data. LREC 2020, p. 4003-4012.

5. International Internet Preservation Consortium. The WARC Format 1.1, ISO 28500:2017.

6. Broder, A. Z. On the Resemblance and Containment of Documents. Compression and Complexity of SEQUENCES 1997, p. 21-29.

7. Lee, K. et al. Deduplicating Training Data Makes Language Models Better. ACL 2022.

8. Penedo, G. et al. The RefinedWeb Dataset for Falcon LLM, 2023.

9. Barbaresi, A. Trafilatura: A Web Scraping Library and Command-Line Tool for Text Discovery and Extraction. ACL-IJCNLP 2021, DOI 10.18653/v1/2021.acl-demo.15; ver também a documentação de extração.

10. Domingues, M. Repositório Canarim: README, LICENSE, texto extraído e preview.

11. Hugging Face. datasets 4.8.4: Streaming e Loading. A documentação descreve IterableDataset, Arrow, seleção de shards e pushdown em Parquet.

12. Domingues, M. Canarim-7B model card, DOI 10.57967/hf/1356.

13. Common Crawl Foundation. Terms of Use, 7 mar. 2024; Privacy Policy, 3 abr. 2025.

14. Creative Commons. Attribution 4.0 International e Attribution-ShareAlike 4.0 International.

BibTeX

@misc{domingues2024canarim,
  author    = {Maicon Domingues},
  title     = {canarim},
  year      = {2024},
  publisher = {Hugging Face},
  doi       = {10.57967/hf/1605},
  url       = {https://huggingface.co/datasets/dominguesm/canarim}
}
 
@inproceedings{barbaresi2021trafilatura,
  author    = {Adrien Barbaresi},
  title     = {Trafilatura: A Web Scraping Library and Command-Line Tool for Text Discovery and Extraction},
  booktitle = {Proceedings of ACL-IJCNLP 2021: System Demonstrations},
  pages     = {122--131},
  year      = {2021},
  doi       = {10.18653/v1/2021.acl-demo.15},
  url       = {https://aclanthology.org/2021.acl-demo.15/}
}
 
@inproceedings{wenzek2020ccnet,
  author    = {Guillaume Wenzek and Marie-Anne Lachaux and Alexis Conneau and Vishrav Chaudhary and Francisco Guzm\'an and Armand Joulin and Edouard Grave},
  title     = {{CCN}et: Extracting High Quality Monolingual Datasets from Web Crawl Data},
  booktitle = {Proceedings of the Twelfth Language Resources and Evaluation Conference},
  pages     = {4003--4012},
  year      = {2020},
  url       = {https://aclanthology.org/2020.lrec-1.494/}
}
 
@inproceedings{broder1997resemblance,
  author    = {Andrei Z. Broder},
  title     = {On the Resemblance and Containment of Documents},
  booktitle = {Compression and Complexity of SEQUENCES 1997},
  pages     = {21--29},
  year      = {1997},
  doi       = {10.1109/SEQUEN.1997.666900}
}
 
@misc{lee2022deduplicating,
  author        = {Katherine Lee and Daphne Ippolito and Andrew Nystrom and Chiyuan Zhang and Douglas Eck and Chris Callison-Burch and Nicholas Carlini},
  title         = {Deduplicating Training Data Makes Language Models Better},
  year          = {2022},
  eprint        = {2107.06499},
  archivePrefix = {arXiv},
  primaryClass  = {cs.CL},
  url           = {https://arxiv.org/abs/2107.06499}
}
 
@misc{penedo2023refinedweb,
  author        = {Guilherme Penedo and Quentin Malartic and Daniel Hesslow and Ruxandra Cojocaru and Alessandro Cappelli and Hamza Alobeidli and Baptiste Pannier and Ebtesam Almazrouei and Julien Launay},
  title         = {The RefinedWeb Dataset for Falcon LLM: Outperforming Curated Corpora with Web Data, and Web Data Only},
  year          = {2023},
  eprint        = {2306.01116},
  archivePrefix = {arXiv},
  primaryClass  = {cs.CL},
  url           = {https://arxiv.org/abs/2306.01116}
}