Spaces:
Sleeping
Sleeping
File size: 13,723 Bytes
477ba05 58c5622 90d2399 2442fcd ccfa465 2442fcd 90d2399 dd5deef 2442fcd 90d2399 2442fcd cb892f1 dd5deef cb892f1 477ba05 dd5deef 2442fcd dd5deef 2442fcd dd5deef 2442fcd dd5deef 2442fcd dd5deef 2442fcd dd5deef 2442fcd dd5deef 2442fcd cb892f1 2442fcd 477ba05 2442fcd 477ba05 2442fcd 477ba05 2442fcd cb892f1 2442fcd cb892f1 dd5deef ccfa465 2442fcd cb892f1 2442fcd 477ba05 ccfa465 477ba05 2442fcd cb892f1 2442fcd dd5deef 2442fcd 477ba05 cb892f1 2442fcd cb892f1 2442fcd ccfa465 cb892f1 2442fcd 477ba05 2442fcd cb892f1 dd5deef 477ba05 2442fcd 477ba05 cb892f1 ccfa465 2442fcd cb892f1 2442fcd 90d2399 cb892f1 90d2399 cb892f1 90d2399 2442fcd 477ba05 2442fcd ccfa465 cb892f1 2442fcd 90d2399 2442fcd cb892f1 dd5deef 2442fcd cb892f1 ccfa465 2442fcd cb892f1 2442fcd 477ba05 cb892f1 2442fcd ccfa465 2442fcd 477ba05 cb892f1 90d2399 2442fcd ccfa465 90d2399 cb892f1 fe16c68 2442fcd dd5deef 2442fcd cb892f1 fe16c68 58c5622 cb892f1 69c45ad 90d2399 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# -*- coding: utf-8 -*-
import gradio as gr
import html2text
from bs4 import BeautifulSoup, Comment
import logging
import re
logging.basicConfig(level=logging.INFO)
def extrair_limpar_html_v5(html_bruto):
"""
Extrai o conteúdo principal (priorizando .entry-content), remove
elementos irmãos indesejados (tags, nav, comments, related), limpa
o conteúdo principal e retorna o HTML limpo.
V5: Adaptado para a estrutura HTML fornecida.
:param html_bruto: String contendo o código HTML original.
:return: String contendo o HTML limpo e focado no conteúdo principal.
"""
if not html_bruto:
return ""
soup = BeautifulSoup(html_bruto, 'html.parser')
# --- 0. Remover comentários HTML ---
for comment in soup.find_all(string=lambda text: isinstance(text, Comment)):
comment.extract()
target_element = None
main_container = None # Guarda o elemento que contém o target_element e os irmãos
# --- 1. Encontrar o Contêiner Principal Específico (.entry-content) ---
# Seletores em ordem de preferência para este site
main_content_selectors = [
'.entry-content', # O mais provável para o corpo do post neste HTML
'.wp-block-post-content', # Alternativa
'article', # Fallback
'main', # Fallback mais amplo
# '[role="main"]', # Menos provável neste tema
]
for selector in main_content_selectors:
target_element = soup.select_one(selector)
if target_element:
logging.info(f"Conteúdo principal identificado usando o seletor: '{selector}'")
# Tenta encontrar um pai razoável para procurar irmãos
# Anda alguns níveis acima se necessário, mas não até o body/html se possível
potential_main_container = target_element.parent
levels_up = 0
while potential_main_container and potential_main_container.name in ['div', 'section'] and levels_up < 3:
# Verifica se este pai contém os blocos indesejados como irmãos do target
if potential_main_container.select_one('.wp-block-post-terms, .wp-block-comments, .wp-block-query'):
main_container = potential_main_container
logging.info(f"Container principal para busca de irmãos definido como: <{main_container.name}>")
break
potential_main_container = potential_main_container.parent
levels_up += 1
# Se não encontrou um container com irmãos indesejados, usa o pai direto
if not main_container:
main_container = target_element.parent
if main_container:
logging.info(f"Container principal para busca de irmãos definido como pai direto: <{main_container.name}>")
break # Para ao encontrar o primeiro target
# Fallback se nenhum seletor específico funcionou
if not target_element:
logging.warning("Nenhum seletor de conteúdo principal específico (.entry-content, article, main) encontrado.")
# Tenta usar o body, mas a limpeza de irmãos não será eficaz
if soup.body:
target_element = soup.body
main_container = soup.body # Define main_container como body
logging.info("Usando <body> como target_element e main_container.")
else:
logging.error("Falha crítica: Nenhum elemento de conteúdo ou body encontrado.")
return "" # Não há nada para processar
# Se não conseguiu definir um main_container, não pode remover irmãos
if not main_container:
logging.warning("Não foi possível determinar um container válido para remover irmãos.")
# Prossegue limpando apenas o target_element encontrado
# --- 2. Remover Elementos Irmãos Indesejados (SE main_container foi definido) ---
if main_container and target_element is not main_container: # Só remove irmãos se o target não for o próprio container
logging.info(f"Procurando irmãos indesejados de <{target_element.name}> dentro de <{main_container.name}>...")
siblings_to_remove_selectors = [
'.wp-block-post-terms', # Bloco de Tags
'.wp-container-core-group-is-layout-9b36172e', # Div que contém a navegação Prev/Next (baseado no HTML)
'.wp-block-comments', # Bloco de comentários inteiro
'.wp-block-query', # Bloco "Mais Posts" (Query Loop)
# Poderíamos ser mais específicos para "Mais Posts", mas .wp-block-query parece ok aqui
# Exemplo: 'div.wp-block-group:has(> h2:contains("Mais posts"))' # Requer análise mais complexa
]
removed_siblings_count = 0
# Itera sobre os elementos DENTRO do main_container
for element in main_container.find_all(recursive=False): # Apenas filhos diretos ou netos? Melhor procurar em todo o container
# Verifica se o elemento atual NÃO é o target_element ou um de seus pais
if element is not target_element and not element.find(target_element):
for selector in siblings_to_remove_selectors:
# Verifica se o elemento corresponde a um seletor indesejado
# Usamos select_one para garantir que estamos testando o próprio elemento
# Ou podemos usar element.matches(selector) se a versão do bs4 suportar bem
if element.select_one(f':is({selector})'): # :is() para testar o próprio elemento
logging.info(f" Removendo irmão/elemento indesejado: <{element.name} class='{' '.join(element.get('class',[]))}'> (match com '{selector}')")
element.decompose()
removed_siblings_count += 1
break # Sai do loop de seletores para este elemento
# Abordagem alternativa/complementar: Buscar DEPOIS do target_element
for sibling in target_element.find_next_siblings():
for selector in siblings_to_remove_selectors:
# Verifica se o irmão corresponde a um seletor indesejado
if sibling.select_one(f':is({selector})'): # :is() para testar o próprio irmão
logging.info(f" Removendo irmão SEGUINTE indesejado: <{sibling.name} class='{' '.join(sibling.get('class',[]))}'> (match com '{selector}')")
sibling.decompose()
removed_siblings_count += 1
break # Vai para o próximo irmão
if removed_siblings_count > 0:
logging.info(f"Removidos {removed_siblings_count} elementos/irmãos indesejados.")
else:
logging.info("Nenhum elemento/irmão indesejado conhecido foi encontrado ou removido após o conteúdo principal.")
# --- 3. Limpeza Geral DENTRO do target_element isolado ---
logging.info(f"Iniciando limpeza geral DENTRO do target_element: <{target_element.name}>")
tags_para_remover_geral = [
'script', 'style', 'form', 'input', 'button', 'select', 'textarea', 'label',
'footer', 'header', 'nav', 'aside', 'iframe', 'noscript', 'meta', 'link',
'canvas', 'svg', 'audio', 'video', 'figure', # Remover figure, manter figcaption permitido
# '.wp-block-button', # Remover botões? Pode ser útil manter alguns. Avaliar.
]
removed_general_count = 0
# Importante: usar find_all DENTRO do target_element
for tag_name in tags_para_remover_geral:
for tag in target_element.find_all(tag_name):
tag.decompose()
removed_general_count +=1
if removed_general_count > 0:
logging.info(f"Removidas {removed_general_count} tags gerais indesejadas dentro do target_element.")
# --- 4. Limpar Atributos e Tags Restantes no target_element ---
tags_permitidas = {
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'a', 'strong', 'b',
'em', 'i', 'u', 's', 'strike', 'del', 'ul', 'ol', 'li', 'img',
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'blockquote', 'pre', 'code',
'figcaption'
}
atributos_permitidos = {
'a': ['href', 'title'],
'img': ['src', 'alt', 'title', 'width', 'height'],
'th': ['colspan', 'rowspan', 'scope'],
'td': ['colspan', 'rowspan'],
'blockquote': ['cite'],
'ol': ['start'],
'pre': [], # Geralmente não precisa de atributos
'code': ['class'], # Permitir classe para syntax highlighting (ex: class="language-python")
}
# Iterar sobre uma cópia da lista de tags DENTRO do target_element
for tag in list(target_element.find_all(True)):
if not tag.parent: continue # Ignora tags já removidas
if tag.name not in tags_permitidas:
tag.unwrap() # Remove tag, mantém conteúdo
else:
# Limpa atributos
atributos_para_manter = atributos_permitidos.get(tag.name, [])
attrs_mantidos = {}
# Mantém atributos essenciais primeiro
if tag.name == 'a' and 'href' in tag.attrs: attrs_mantidos['href'] = tag.attrs['href']
if tag.name == 'img' and 'src' in tag.attrs: attrs_mantidos['src'] = tag.attrs['src']
if tag.name == 'img' and 'alt' in tag.attrs: attrs_mantidos['alt'] = tag.attrs['alt'] # Manter ALT
# Adiciona outros permitidos
for attr, value in tag.attrs.items():
if attr in atributos_para_manter:
attrs_mantidos[attr] = value
tag.attrs = attrs_mantidos
# Retorna o HTML limpo e focado como string
html_final = str(target_element)
html_final = html_final.replace(' ', ' ')
# Remover divs vazios que podem sobrar após unwrap
soup_final = BeautifulSoup(html_final, 'html.parser')
for div in soup_final.find_all('div'):
if not div.get_text(strip=True) and not div.find(['img', 'br']): # Se não tem texto nem imagem/br
div.decompose()
html_final = str(soup_final)
logging.info("Limpeza final do HTML concluída.")
return html_final
def html_para_markdown_final_v5(html_input):
"""
Pipeline completo V5: Extrai .entry-content, remove irmãos, limpa, converte.
"""
if not html_input:
return "Por favor, insira algum código HTML."
try:
# 1. Extrai, remove irmãos indesejados e limpa HTML
logging.info("--- Iniciando Extração e Limpeza V5 ---")
html_processado = extrair_limpar_html_v5(html_input)
logging.info("--- Extração e Limpeza V5 Concluída ---")
soup_check = BeautifulSoup(html_processado, 'html.parser')
if not html_processado or not soup_check.get_text(strip=True):
logging.warning("HTML resultante V5 após limpeza está vazio ou sem texto.")
return "HTML resultante após extração e limpeza está vazio ou não contém texto."
# 2. Converte o HTML processado para Markdown (Config V4/V2)
logging.info("--- Iniciando Conversão para Markdown V5 ---")
converter = html2text.HTML2Text()
converter.body_width = 0
converter.ignore_links = False
converter.ignore_images = False
converter.ignore_emphasis = False
converter.use_automatic_links = True
converter.unicode_snob = True
converter.escape_snob = True
markdown_output = converter.handle(html_processado)
logging.info("--- Conversão para Markdown V5 Concluída ---")
# 3. Pós-processamento do Markdown (Simplificado - V4/V2)
logging.info("--- Iniciando Pós-processamento do Markdown V5 ---")
linhas = [line.strip() for line in markdown_output.splitlines()]
linhas_filtradas = [line for line in linhas if line]
markdown_output = "\n\n".join(linhas_filtradas)
# Limpeza final extra
markdown_output = re.sub(r' +', ' ', markdown_output) # Múltiplos espaços
markdown_output = re.sub(r' +\n', '\n', markdown_output) # Espaços antes de \n
# Remover marcadores de lista vazios ou estranhos que podem sobrar
markdown_output = re.sub(r'\n\n[-*+]\s*\n\n', '\n\n', markdown_output)
markdown_output = re.sub(r'^\s*[-*+]\s*\n\n', '', markdown_output) # No início
logging.info("--- Pós-processamento do Markdown V5 Concluído ---")
return markdown_output.strip()
except Exception as e:
logging.error(f"Erro durante o processo V5: {e}", exc_info=True)
try: html_on_error = html_processado
except NameError: html_on_error = "(HTML não disponível)"
return (f"Ocorreu um erro V5: {str(e)}\n\n"
f"Verifique os logs do Space.\n\n"
f"HTML processado antes do erro:\n"
f"{html_on_error[:2000]}...")
# --- Cria a interface Gradio ---
iface = gr.Interface(
fn=html_para_markdown_final_v5, # Usando a função V5
inputs=gr.Textbox(lines=20, label="Insira o HTML bruto aqui", placeholder="Cole o código-fonte HTML completo da página..."),
outputs=gr.Textbox(lines=20, label="Markdown Resultante (Conteúdo Principal Limpo - V5)", show_copy_button=True),
title="Conversor HTML para Markdown (V5 - Específico para Estrutura WP)",
description="Cole o HTML. O script tenta isolar '.entry-content', remove tags/comentários/relacionados/nav que vêm *depois* dele, limpa o HTML restante e converte para Markdown (formatação V2/V4).",
allow_flagging='never'
)
# --- Lança a interface ---
if __name__ == "__main__":
iface.launch() |