# -*- 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 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()