brnews/collector.py — orquestração e escrita
Amarra feedlist, fetcher e parsers: baixa
todos os feeds em paralelo, monta os registros finais e grava as três saídas
(news-*.jsonl, .report.json, index.jsonl).
Dataclasses de resultado
FeedReport— diagnóstico de um feed na execução:status(ok/empty/error), nº de itens, rota e proxy/espelho usados, status HTTP, estratégia de parsing, tempo, tentativas e a listatried.RunResult— a execução inteira:run_id, horários, caminhos dos arquivos, totais (ok/vazios/erro/itens/pulados), contadores de rota, stats de proxies e a lista deFeedReports.
Collector.run(feeds, dry_run=False)
run_idtem o formato20261005T000112Z-a1b2c3(timestamp + 6 hex de UUID);- os workers rodam num
ThreadPoolExecutor(--workers, padrão 8); - nenhuma exceção de feed derruba a coleta: um erro inesperado vira
FeedReport(status="error")e a execução segue; - a saída preserva a ordem original de
rss.txt, independentemente da ordem de término das threads; - com
dry_run=Truenada é escrito em disco.
collect_feed(feed, run_id, collected_at) — um feed
- Download validado —
fetcher.get(url, validate=...)com_validate_rss(XML precisa ter itens e não ser HTML/página de bloqueio) ou_validate_html(> 800 bytes e não bloqueada), conforme o tipo do feed. - Parsing com fallback cruzado (
_parse):- resposta de espelho em Markdown →
parse_markdown; - feed
html→parse_html; se vazio, tentaparse_rss(rss-fallback); - feed
rss→parse_rss; se vazio, tentaparse_html(html-fallback) — cobre feeds que "viraram" página HTML.
- resposta de espelho em Markdown →
- Segunda tentativa — se ainda não há itens, refaz o download com a validação
leniente (
_validate_lenient: só exige > 300 bytes e não-bloqueado). Se o primeiro download passou na validação (qualquer rota, não só a direta), restringe a["mirror", "proxy"]: a resposta veio, mas não produziu registros — a nova tentativa busca outra "visão" da página por um espelho ou proxy. - Status final —
ok(tem registros),empty(baixou mas nada passou nos filtros; um eventualbozo_reasondo feedparser vira o motivo) ouerror.
_build_record — o registro final
Monta o dict com os ~27 campos do esquema: schema_version,
identificação do feed (feed_*, de Feed.as_dict()), a notícia em si, e os
metadados language (padrão pt-BR), source_domain (host do link, sem www.),
fetch_route, fetch_via e parser.
Filtros aplicados aqui:
--max-age-days: descarta item compublishedmais antigo que N dias (datas ausentes/inválidas não são descartadas);--skip-seen: descartaidpresente emseen_ids(carregado porload_seen_ids()dos últimos N snapshots), contando emskipped_seen;--no-content: omitecontent_text;--limit-per-feed: corta a lista do feed após o dedupe.
Escrita (append-only)
| Função | Saída |
|---|---|
snapshot_paths | data/<ano>/news-<AAAA-MM-DD>T<HHMMSS>Z.jsonl; se o nome já existir, acrescenta -2, -3, … — nunca sobrescreve. |
write_jsonl | Uma notícia por linha, ensure_ascii=False; com --gzip, .jsonl.gz. |
write_report | .report.json com totais, rotas, stats de proxies e um FeedReport por feed. |
append_index | Acrescenta uma linha a data/index.jsonl com o resumo da execução. |
load_seen_ids | Lê os ids dos últimos N snapshots (.jsonl e .jsonl.gz) para o --skip-seen. |
Integração com o GitHub Actions
| Função | Para onde escreve |
|---|---|
markdown_summary | Resumo em Markdown (totais, top feeds, feeds com problema). |
write_step_summary | Anexa o resumo ao $GITHUB_STEP_SUMMARY. |
write_github_output | run_id, items, feeds_ok, feeds_failed, jsonl, report no $GITHUB_OUTPUT (consumidos pelo job publicar). |
write_annotations | ::notice com o total e até 12 ::warning (um por feed sem notícias, com motivo e tentativas). |