pesquisa.ai / app.py
DavidSB's picture
Update app.py
b66f562 verified
# Importações
import gradio as gr
import requests
from bs4 import BeautifulSoup
import re
import pandas as pd
import validators
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
import openpyxl
import googlemaps
import folium
from folium.plugins import MarkerCluster
import webbrowser
from datetime import datetime
# Constantes e Dicionários
# Constantes
ESTADOS_BR = ["AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"]
# Dicionários
dict_topo = {
'plano <5%': 1,
'aclive_leve 5% e 30%': 0.95,
'declive_leve 5% e 30%': 0.90,
'aclive_acentuado >30%': 0.85,
'declive_acentuado >30%': 0.80,
'-': '-'
}
dict_rel = {
'plana': 1.1,
'ondulada': 1.00,
'montanhosa/acidentada': 0.80,
'-': '-'
}
dict_sup = {
'Seca': 1.00,
'Região inundável mas não atingida': 0.90,
'Região inundável mas atingida periodicamente': 0.70,
'Alagada': 0.60,
'-': '-'
}
dict_apr = {
'Loteamento': 1.00,
'Indústria': 0.90,
'Culturas': 0.80,
'-': '-'
}
dict_ace = {
'Ótima': 1.00,
'Muito boa': 0.95,
'Boa': 0.90,
'Desfavorável': 0.80,
'Má': 0.75,
'Péssima': 0.70,
'-': '-'
}
dict_ec = {
'Em construção ou na planta': 1.00,
'Bom (aparência de novo)': 0.95,
'Bom (aparência de usado)': 0.90,
'Regular (reparos simples)': 0.80,
'Regular (reparos importantes)': 0.75,
'Ruim': 0.70,
'-': '-'
}
dict_id = {
'Na planta': 1.00,
'id <= 5': 0.95,
'5 < id <= 10': 0.90,
'10 < id <= 20': 0.85,
'20 < id <= 50': 0.80,
'50 < id <= 100': 0.75,
'id > 100': 0.70,
'-': '-'
}
dict_pad = {
'Alto (superior, luxo)': 1.00,
'Alto (por predominância)': 0.95,
'Normal (c/ aspectos de alto)': 0.90,
'Normal (forte predominância)': 0.80,
'Normal (c/ aspectos de baixo)': 0.75,
'Baixo (selecionado)': 0.70,
'Mínimo': 0.65,
'-': '-'
}
dict_loc = {
'Região central': 1.00,
'Região não central': 0.85,
'Ocupação periférica/suburbana': 0.75,
'Não inserido na malha urbana': 0.60,
'-': '-'
}
# Funções auxiliares
def extract_address(text):
estados_regex = "|".join(ESTADOS_BR)
match = re.search(rf'Endereço\s*(.*?\b({estados_regex})\b)', text, re.DOTALL)
return match.group(1).strip() if match else None
def extract_testada(text):
match = re.search(r'(\d{1,3}(?:[.,]\d{1,2})?)m?\s*[xX]\s*\d', text)
return float(match.group(1).replace(',', '.')) if match else None
def clean_text_for_testada(page_text):
return re.sub(r'^\s*\*?\s*\!\[Image[^\n]*\n?', '', page_text, flags=re.MULTILINE)
# Função de busca
def fetch_url_info(user_input):
url = f"https://r.jina.ai/{user_input}"
if not validators.url(url):
return pd.DataFrame(), "URL inválida. Verifique e tente novamente.", None, None
headers = {"User-Agent": "Mozilla/5.0"}
try:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
def get_main_content(soup):
elements = soup.find_all(["article", "main", "div"])
return max(elements, key=lambda el: len(el.get_text()), default=None)
title = soup.title.string if soup.title else ""
main_content = get_main_content(soup)
page_text = main_content.get_text(separator='\n', strip=True) if main_content else soup.get_text(separator='\n', strip=True)
cleaned_text = clean_text_for_testada(page_text)
valor_match = re.search(r'R\$\s*(\d[\d\.,]*)', page_text)
valor = float(valor_match.group(1).replace('.', '').replace(',', '.')) if valor_match else None
area_match = re.search(r'(\d+)\s*m²', page_text)
area = float(area_match.group(1)) if area_match else None
dorm_match = re.search(r'(\d+)\s*(quarto|quartos|dormit[oó]rio|dormit[oó]rios)', page_text, re.IGNORECASE)
dorm = int(dorm_match.group(1)) if dorm_match else None
banheiro_match = re.search(r'(\d+)\s*(banheiro|banheiros)', page_text, re.IGNORECASE)
banheiros = int(banheiro_match.group(1)) if banheiro_match else None
vagas_match = re.search(r'(\d+)\s*(vaga|vagas)', page_text, re.IGNORECASE)
vagas = int(vagas_match.group(1)) if vagas_match else None
suite_match = re.search(r'(\d+)\s*(su[ií]te|su[ií]tes)', page_text, re.IGNORECASE)
suites = int(suite_match.group(1)) if suite_match else None
endereco = extract_address(page_text)
testada = extract_testada(cleaned_text)
result_text = f"**{title}**\n\n{page_text[:30000]}..." if title else f"{page_text[:30000]}..."
df = pd.DataFrame([{
"Endereço": endereco,
"Área": area,
"Testada": testada,
"Valor": valor,
"Dorm": dorm,
"Banheiros": banheiros,
"Vagas": vagas,
"Suítes": suites,
"URL": url,
}])
return df, result_text, endereco, valor
except requests.RequestException as e:
return pd.DataFrame(), f"Erro ao acessar a URL: {e}", None, None
# Substitua 'YOUR_API_KEY' pela chave de API real que você obteve
api_key = 'xxxxx'
# Função para obter latitude e longitude a partir de um endereço
def obter_coordenadas(endereco, gmaps):
try:
geocode_result = gmaps.geocode(endereco)
if geocode_result:
location = geocode_result[0]['geometry']['location']
return location['lat'], location['lng']
except Exception as e:
print(f"Erro ao obter coordenadas para {endereco}: {e}")
return None, None
# Acumulador
def adicionar_ao_acumulado(df_atual, df_acumulado, topo, rel, sup, apr, ace, pav, ec, id, pad, iso, pos, loc):
gmaps = googlemaps.Client(key=api_key)
if df_atual.empty:
return df_acumulado, df_acumulado, "", ""
df_novo = df_atual.copy()
start_index = len(df_acumulado) + 1
df_novo.index = range(start_index, start_index + len(df_novo))
df_novo.index.name = "Dado"
# Adicionar data atual no formato dd/mm/aa
data_hoje = datetime.now().strftime('%d/%m/%y')
df_novo.insert(1, "Data", data_hoje)
# Adicionar valores dos dropdowns
df_novo["Topografia"] = topo
df_novo["Relevo"] = rel
df_novo["Superfície"] = sup
df_novo["Aproveitamento"] = apr
df_novo["Acessibilidade"] = ace
df_novo["Pavimentação"] = pav
df_novo["Estado de conservação"] = ec
df_novo["Idade estimada"] = id
df_novo["Padrão construtivo"] = pad
df_novo["Tipo de Construção"] = iso
df_novo["Posição"] = pos
df_novo["Localização"] = loc
# Calcular VU (Valor / Área), evitando divisão por zero ou nulos
df_novo["VU"] = df_novo.apply(
lambda row: round(row["Valor"] / row["Área"], 2) if row["Área"] and row["Valor"] else None,
axis=1)
# Reordenar colunas para colocar VU depois de Valor
cols = df_novo.columns.tolist()
valor_index = cols.index("Valor")
vu_index = cols.index("VU")
cols.insert(valor_index + 1, cols.pop(vu_index))
df_novo = df_novo[cols]
# Adicionar colunas lat e lon
df_novo['lat'] = None
df_novo['lon'] = None
# Obter coordenadas para cada endereço
for index, row in df_novo.iterrows():
endereco = row['Endereço']
lat, lon = obter_coordenadas(endereco, gmaps)
print(f"Endereço: {endereco}, Lat: {lat}, Lon: {lon}") # Debug print
df_novo.at[index, 'lat'] = lat
df_novo.at[index, 'lon'] = lon
df_novo.reset_index(inplace=True)
df_acumulado = pd.concat([df_acumulado, df_novo], ignore_index=True)
df_acumulado.to_excel("Banco de dados.xlsx", index=False)
# Calcular estatísticas
quantidade_dados = len(df_acumulado)
valor_max = df_acumulado["Valor"].max()
valor_min = df_acumulado["Valor"].min()
valor_medio = df_acumulado["Valor"].mean()
valor_mediana = df_acumulado["Valor"].median()
vu_max = df_acumulado["VU"].max()
vu_min = df_acumulado["VU"].min()
vu_medio = df_acumulado["VU"].mean()
vu_mediana = df_acumulado["VU"].median()
# Criar texto com estatísticas
def format_brl(valor):
return f"R$ {valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
stats_text = (
f"**Quantidade de dados:** {quantidade_dados}\n\n"
f"**Valor:** Máximo: {format_brl(valor_max)}, Mínimo: {format_brl(valor_min)}, "
f"Média: {format_brl(valor_medio)}, Mediana: {format_brl(valor_mediana)}\n\n"
f"**VU:** Máximo: {format_brl(vu_max)}, Mínimo: {format_brl(vu_min)}, "
f"Média: {format_brl(vu_medio)}, Mediana: {format_brl(vu_mediana)}"
)
return df_acumulado, df_acumulado, "Banco de dados.xlsx", stats_text
# Função para gerar um mapa
def gerar_mapa(df_acumulado):
df_acumulado['lat'] = df_acumulado['lat'].astype(str).str.replace(',', '.').astype(float)
df_acumulado['lon'] = df_acumulado['lon'].astype(str).str.replace(',', '.').astype(float)
df_filtrado = df_acumulado[pd.notnull(df_acumulado['lat']) & pd.notnull(df_acumulado['lon'])]
if df_filtrado.empty:
return "<p style='color:red;'>Nenhum ponto com coordenadas válidas.</p>"
centro = [df_filtrado.iloc[0]['lat'], df_filtrado.iloc[0]['lon']]
mapa = folium.Map(location=centro, zoom_start=13)
marker_cluster = MarkerCluster().add_to(mapa)
for _, row in df_filtrado.iterrows():
popup_text = f"""
<b>Endereço:</b> {row.get('Endereço', 'N/A')}<br>
<b>Área:</b> {row.get('Área', 'N/A')} m²<br>
<b>Valor:</b> R$ {row.get('Valor', 'N/A')}<br>
<b>URL:</b> <a href="{row.get('URL', '#')}" target="_blank">Link</a>
"""
folium.CircleMarker(
location=[row['lat'], row['lon']],
radius=7,
color='#000000',
fill=True,
fill_color='#E0C200',
fill_opacity=0.9,
popup=folium.Popup(popup_text, max_width=300)
).add_to(marker_cluster)
# Salva como string HTML (sem gravar em arquivo)
mapa_html = mapa.get_root().render()
# Retorna como iframe para renderizar no Gradio
return f"<iframe srcdoc='{mapa_html}' width='100%' height='600px' style='border:none;'></iframe>"
# Limpeza do anúncio atual
def clear_fields():
# Define um DataFrame com uma linha em branco
empty_df = pd.DataFrame({
"Endereço": [""],
"Área": [None],
"Testada": [None],
"Valor": [None],
"VU": [None],
"Dorm": [None],
"Banheiros": [None],
"Vagas": [None],
"Suítes": [None],
"URL": [""],
"Topografia": ["-"],
"Relevo": ["-"],
"Superfície": ["-"],
"Aproveitamento": ["-"],
"Acessibilidade": ["-"],
"Pavimentação": ["-"],
"Estado de conservação": ["-"],
"Idade estimada": ["-"],
"Padrão construtivo": ["-"],
"Tipo de construção": ["-"],
"Posição": ["-"],
"Localização": ["-"]
})
return "", empty_df, "", False, None, '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-', '-'
# Função para excluir linhas com "Dado" vazio
def excluir_dados_vazios(df_acumulado):
# Remove linhas onde a coluna "Dado" está vazia ou contém apenas espaços em branco
df_acumulado = df_acumulado[df_acumulado["Dado"].str.strip() != ""]
# Reindexar o DataFrame para manter a sequência correta
df_acumulado = df_acumulado.reset_index(drop=True)
return df_acumulado, df_acumulado
# Função para mostrar o anúncio na tela
def toggle_output_text(show_text, result_text):
return gr.update(visible=show_text), result_text
# Função para fazer o print de tela
def take_screenshot_with_api(url, api_key, filename="screenshot.png"):
api_url = f"https://shot.screenshotapi.net/screenshot?token={api_key}&url={url}&output=image"
response = requests.get(api_url)
if response.status_code == 200:
with open(filename, 'wb') as file:
file.write(response.content)
print(f"Screenshot salvo como {filename}")
return filename
else:
print(f"Erro ao capturar screenshot: {response.status_code}")
return None
# Interface
# Tema
theme = gr.themes.Citrus(primary_hue="yellow")
# App principal
with gr.Blocks(theme=theme, css="""
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;700&display=swap');
.small-file-upload {
height: 65px;
text-align: center;
color: black;
border: 2px solid black !important;
box-sizing: border-box;
}
.small-file-upload span {
display: none;
}
.small-file-upload input[type="file"] {
color: black;
}
.small-file-upload label {
color: black;
}
.small span {
font-size: 1.2em;
white-space: nowrap;
width: auto;
display: inline-block;
}
.small span dados {
font-size: 0.8em;
white-space: nowrap;
width: auto;
display: inline-block;
}
h1 {
text-align: center;
font-family: 'Quicksand', sans-serif;
font-weight: 700;
margin: 20px 0;
color: black;
}
.map-container {
height: 600px !important;
margin: 0;
padding: 0;
}
""") as app:
gr.Markdown(
"<div style='font-size: 1.5em;'>"
"<span style='color: gray;'>Pesquisa.AI - </span>"
"<span style='color: gray;'>aval</span>"
"<span style='color: #FFD700;'>ia</span>"
"<span style='color: gray;'>.se</span>"
"</div>"
)
df_acumulado_state = gr.State(pd.DataFrame())
with gr.Row():
with gr.Column(scale=1):
user_input = gr.Textbox(label="Copie a url do anúncio e cole aqui")
gr.Markdown("**DADO**")
submit_button = gr.Button("Carregar anúncio", variant="primary")
screenshot_button = gr.Button("Print anúncio", variant="primary")
clear_button = gr.Button("Limpar anúncio", variant="primary")
gr.Markdown("**BANCO DE DADOS**")
add_data = gr.Button("Adicionar dado", variant="primary")
delete_data = gr.Button("Excluir dado", variant="primary")
generate_map_button = gr.Button("Gerar Mapa", variant="primary")
# Avisos com toggle de exibição
with gr.Column():
disclaimer_checkbox = gr.Checkbox(label="Avisos importantes!", value=False)
disclaimer_text = gr.Markdown(
"""
### Avisos Importantes
- **Aplicativo Online: **
Este é um aplicativo 100% online, que depende de conexão com a internet para funcionar corretamente.
- **Coleta de Informações: **
O aplicativo se destina a coletar e organizar dados de sites de anúncios e imobiliárias.
Não garantimos que os dados sejam verdadeiros, completos ou atualizados, pois são de responsabilidade dos sites de origem.
- **Diversidade de Fontes: **
O sistema acessa vários sites diferentes para buscar informações.
Por isso, pode haver erros no preenchimento automático dos campos.
- ** Instabilidade dos Sites de Origem: **
Os sites de origem podem apresentar instabilidade, lentidão ou ficar temporariamente fora do ar, o que pode causar atrasos no carregamento ou falhas momentâneas na coleta de dados.
- **Atenção do Usuário: **
Recomendamos que o usuário revise e corrija os dados, se necessário.
Para facilitar isso, todos os campos da planilha "Banco de Dados" na interface são editáveis.
- **Boas Práticas de Uso: **
Como o aplicativo é online, sujeito à instabilidade na conexão de internet ou falta de energia, é recomendável fazer o download periódico da pesquisa para garantir uma cópia segura do trabalho.
""",
visible=False
)
disclaimer_checkbox.change(
fn=lambda visible: gr.update(visible=visible),
inputs=disclaimer_checkbox,
outputs=disclaimer_text
)
with gr.Column(scale=5):
output_table = gr.Dataframe(label="Anúncio",
headers=["Endereço", "Área", "Testada", "Valor", "VU", "Dorm", "Banheiros", "Vagas", "Suítes", "URL", "Topografia", "Relevo", "Superfície", "Aproveitamento", "Acessibilidade", "Pavimentação", "Estado de conservação", "Idade estimada", "Padrão construtivo", "Tipo de construção", "Posição", "Localização"],
datatype=["str", "number", "number", "number", "number", "number", "number", "number", "number", "str", "str", "str", "str", "str", "str", "str", "str", "str", "str", "str", "str", "str"],
column_widths=[200, 100, 120, 120, 100, 100, 120, 100, 100, 300, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
interactive=True,
max_height=250
)
with gr.Group("Características do imóvel"):
with gr.Row():
topo_drop = gr.Dropdown(label="Topografia", choices=list(dict_topo.keys()), value='-', interactive=True)
rel_drop = gr.Dropdown(label="Relevo", choices=list(dict_rel.keys()), value='-', interactive=True)
sup_drop = gr.Dropdown(label="Superfície", choices=list(dict_sup.keys()), value='-', interactive=True)
apr_drop = gr.Dropdown(label="Aproveitamento", choices=list(dict_apr.keys()), value='-', interactive=True)
ace_drop = gr.Dropdown(label="Acessibilidade", choices=list(dict_ace.keys()), value='-', interactive=True)
pav_drop = gr.Dropdown(label="Pavimentação", choices=["Sim", "Não", "-"], value='-', interactive=True)
with gr.Row():
ec_drop = gr.Dropdown(label="Estado de conservação", choices=list(dict_ec.keys()), value='-', interactive=True)
id_drop = gr.Dropdown(label="Idade estimada", choices=list(dict_id.keys()), value='-', interactive=True)
pad_drop = gr.Dropdown(label="Padrão construtivo", choices=list(dict_pad.keys()), value='-', interactive=True)
iso_drop = gr.Dropdown(label="Tipo de construção", choices=["Isolada", "Não isolada", "-"], value='-', interactive=True)
pos_drop = gr.Dropdown(label="Posição", choices=["Meio de quadra", "Esquina", "-"], value='-', interactive=True)
loc_drop = gr.Dropdown(label="Localização", choices=list(dict_loc.keys()), value='-', interactive=True)
with gr.Row():
show_text_checkbox = gr.Checkbox(label="Mostrar anúncio", value=False, scale=4)
screenshot_output = gr.Image(label="Print", type="filepath", height=50, scale=1)
output_text = gr.Markdown(label="Resultado", visible=False)
acumulado_table = gr.Dataframe(
label="Banco de dados",
headers=[
"Dado", "Data", "Endereço", "Área", "Testada", "Valor", "VU", "Dorm", "Banheiros", "Vagas", "Suítes", "URL",
"Topografia", "Relevo", "Superfície", "Aproveitamento", "Acessibilidade", "Pavimentação",
"Estado de conservação", "Idade estimada", "Padrão construtivo", "Tipo de construção",
"Posição", "Localização", "lat", "lon"
],
datatype=[
"str", "str", "str", "number", "number", "number", "number", "number", "number", "number", "number", "str",
"str", "str", "str", "str", "str", "str", "str", "str", "str", "str", "str", "str", "number", "number"
],
column_widths=[
80, 90, 200, 100, 120, 120, 100, 100, 120, 100, 100, 300,
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100
],
interactive=True,
max_height=500
)
bd = gr.File(label="Exportar para excel", height=50, elem_classes=["small-file-upload"])
stats_output = gr.Markdown(label="Estatísticas dos Dados Acumulados")
map_output = gr.HTML(label="Mapa")
# Lógica dos botões
submit_button.click(fetch_url_info, inputs=user_input, outputs=[output_table, output_text, gr.State(), gr.State()])
clear_button.click(clear_fields, outputs=[user_input, output_table, output_text, show_text_checkbox, screenshot_output, topo_drop, rel_drop, sup_drop, apr_drop, ace_drop, pav_drop, ec_drop, id_drop, pad_drop, iso_drop, pos_drop, loc_drop])
show_text_checkbox.change(toggle_output_text, inputs=[show_text_checkbox, output_text], outputs=[output_text, output_text])
screenshot_button.click(
lambda url: take_screenshot_with_api(url, api_key),
inputs=user_input,
outputs=screenshot_output
)
add_data.click(adicionar_ao_acumulado, inputs=[output_table, df_acumulado_state, topo_drop, rel_drop, sup_drop, apr_drop, ace_drop, pav_drop, ec_drop, id_drop, pad_drop, iso_drop, pos_drop, loc_drop], outputs=[df_acumulado_state, acumulado_table, bd, stats_output])
delete_data.click(excluir_dados_vazios, inputs=df_acumulado_state, outputs=[df_acumulado_state, acumulado_table])
generate_map_button.click(gerar_mapa, inputs=df_acumulado_state, outputs=map_output)
app.launch(share=True)
# MELHORIAS (a fazer)
# Dataframes: Adicionar a origem do anúncio (Inserir uma coluna com o nome da imobiliária que originou o anúncio).
# Dataframe acumulado: Excluir dado e reorganizar planilha.
# Adaptar à outras imobiliárias e portais. Obs. Foram criados dois códigos alternativos ao Jina. O Primeiro com texto limpo. O segundo com os marcadores, talvez este seja o caminho.
# Dataframe acumulado: Adicionar os coeficientes (utilizar os dicionários).
# O print do anúncio, quando exportado, deverá ser nomeado com endereço e valor.
# Testar o fluxo e estabilidade do sistema.
# Quando clicar em "adicionar dado" limpar automaticamente o anúncio;
# Dispor as colunas na seguinte ordem: Dado / Endereço / Área / Demais características utilizadas/ V_Unit / Valor Total / URL / Características não utilizadas;
# Disclaimer (diminuir a fonte)
# Retirar o jina.ai
# Informação de utilização
# Fale conosco