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