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