Cornell Consultant • Scraper + Traducción

Signs → Diagnósticos diferenciales (Canine)

Este documento explica cómo se construyó el JSON original en inglés a partir de los signos de Cornell Consultant y cómo después se generó la versión traducida al español automáticamente mediante Python. La idea es mostrar de forma clara el flujo completo: extracciónestructuración JSONtraducción automática.

Desarrollado por: Ángel Soto

🏠 volver a página de inicio

Entrada: sings_link.txt Salida EN: consultant_signs_canine.json Salida ES: consultant_signs_canine_ES.json Método: POST + traducción automática

Cómo está hecho

El proceso completo se divide en dos fases principales. Primero se genera el JSON base en inglés mediante scraping estructurado de Cornell Consultant. Después, ese JSON se recorre y se traduce automáticamente al español manteniendo la misma estructura.

Fase 1
Construcción de consultant_signs_canine.json leyendo signos desde un TXT y obteniendo sus diagnósticos diferenciales desde la web.
Fase 2
Traducción automática del JSON mediante deep_translator para generar consultant_signs_canine_ES.json.
1

Lectura del archivo de signos

El script abre sings_link.txt, donde cada línea contiene un código (por ejemplo A00, C0A, DE3) y el nombre del proceso asociado.

2

Construcción dinámica de la URL

Para cada código se genera la URL de Cornell Consultant correspondiente al signo: https://consultant.vet.cornell.edu/?Fun=F_Sign&spc=Canine&dxkw=&sxkw=&signs=1-XXX.

3

Simulación real del botón “Search for Diagnoses”

La lista de diagnósticos no aparece con un simple GET. Por eso el scraper usa un POST al mismo endpoint, enviando el payload que simula la pulsación del botón y el checkbox del signo.

4

Extracción de los diagnósticos diferenciales

Tras recibir la página “post-submit”, se analiza el HTML con BeautifulSoup y se recogen los enlaces <a class="news"> cuyo href contiene Fun=Cause_. Cada texto se guarda como diagnóstico diferencial.

5

Generación del JSON base en inglés

Se crea un JSON estructurado con los campos Proceso, especialidades, Diagnósticos diferenciales y una sección meta con el código y la URL utilizada.

6

Traducción automática del JSON

Después, un segundo script carga consultant_signs_canine.json y traduce automáticamente Proceso, especialidades y cada elemento de Diagnósticos diferenciales usando GoogleTranslator.

7

Protección ante errores y guardado incremental

Si una traducción falla, el script reintenta tras unos segundos. Si vuelve a fallar, conserva el texto original para no romper el proceso. Además, guarda una copia temporal cada 10 items como medida de seguridad.

Idea clave: el nombre de Proceso se toma del TXT original durante el scraping, mientras que la traducción automática se aplica después sobre el JSON ya construido. Así se separan claramente la extracción de datos y la traducción del contenido.
Así se generó primero el JSON base y así se hizo después la traducción automática del JSON.
Importante: la traducción automática acelera muchísimo el proceso, pero conviene revisar posteriormente términos médicos complejos o muy específicos, ya que algunos diagnósticos pueden requerir corrección manual.

Python 1 · Scraper funcional Cornell

Este script genera el JSON base en inglés desde sings_link.txt, simulando correctamente el POST que hace aparecer los diagnósticos diferenciales en la web.

Entrada sings_link.txt Salida consultant_signs_canine.json Método POST
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import re
import time
from dataclasses import dataclass
from typing import Dict, List, Tuple

import requests
from bs4 import BeautifulSoup

BASE_URL = "https://consultant.vet.cornell.edu/"
URL_TMPL = BASE_URL + "?Fun=F_Sign&spc=Canine&dxkw=&sxkw=&signs=1-{code}"

INPUT_TXT = "sings_link.txt"
OUTPUT_JSON = "consultant_signs_canine.json"

SYSTEM_MAP: Dict[str, str] = {
    "A": "Acoustic",
    "C": "Cardiovascular",
    "D": "Digestive",
    "E": "Ophthalmology",
    "G": "General",
    "M": "Musculoskeletal",
    "N": "Nervous",
    "P": "Pain/Discomfort",
    "R": "Respiratory",
    "S": "Skin/Integumentary",
    "T": "Reproductive",
    "U": "Urinary",
}

SLEEP_SECONDS = 0.6
TIMEOUT = 25


@dataclass
class SignRow:
    code: str
    proceso: str


def read_signs_txt(path: str) -> List[SignRow]:
    rows: List[SignRow] = []
    with open(path, "r", encoding="utf-8", errors="replace") as f:
        for raw in f:
            line = raw.strip()
            if not line:
                continue
            parts = re.split(r"\t+", line, maxsplit=1)
            if len(parts) < 2:
                parts = re.split(r"\s{2,}", line, maxsplit=1)
            if len(parts) < 2:
                continue
            code = parts[0].strip()
            proceso = parts[1].strip()
            if code and proceso:
                rows.append(SignRow(code=code, proceso=proceso))
    return rows


def infer_system(code: str) -> str:
    return SYSTEM_MAP.get(code[:1].upper(), "All") if code else "All"


def extract_diagnoses(html: str) -> List[str]:
    soup = BeautifulSoup(html, "html.parser")
    diags: List[str] = []
    seen = set()

    for a in soup.select("a.news"):
        href = a.get("href", "") or ""
        if "Fun=Cause_" not in href:
            continue
        text = a.get_text(" ", strip=True)
        if text and text not in seen:
            seen.add(text)
            diags.append(text)

    return diags


def fetch_page_after_submit(session: requests.Session, code: str) -> str:
    """
    IMPORTANT: The diagnoses list appears after submitting the form (POST),
    not just by opening the URL (GET).
    """
    url = URL_TMPL.format(code=code)

    data = {
        "SPC": "Canine",
        "kw": "",
        "SubBut": "Search for Diagnoses",
        f"SX_{code}": f"SX_{code}",
    }

    r = session.post(url, data=data, timeout=TIMEOUT)
    r.raise_for_status()
    r.encoding = r.apparent_encoding or "utf-8"
    return r.text


def main() -> None:
    signs = read_signs_txt(INPUT_TXT)
    if not signs:
        raise SystemExit(f"No valid rows found in {INPUT_TXT}")

    session = requests.Session()
    session.headers.update({
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) CornellConsultantScraper/1.1",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "Connection": "keep-alive",
        "Referer": BASE_URL,
    })

    out: List[dict] = []
    errors: List[Tuple[str, str]] = []

    for i, row in enumerate(signs, start=1):
        try:
            html = fetch_page_after_submit(session, row.code)
            diags = extract_diagnoses(html)

            out.append({
                "Proceso": row.proceso,
                "especialidades": infer_system(row.code),
                "Diagnósticos diferenciales": diags,
                "meta": {
                    "sign_code": row.code,
                    "url": URL_TMPL.format(code=row.code),
                    "method": "POST(SubBut=Search for Diagnoses)"
                }
            })
        except Exception as e:
            errors.append((row.code, str(e)))

        time.sleep(SLEEP_SECONDS)
        if i % 25 == 0:
            print(f"Progress: {i}/{len(signs)}")

    payload = {
        "source": "consultant.vet.cornell.edu",
        "species": "Canine",
        "generated_from": INPUT_TXT,
        "count": len(out),
        "items": out,
        "errors": [{"code": c, "error": msg} for c, msg in errors],
    }

    with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
        json.dump(payload, f, ensure_ascii=False, indent=2)

    print(f"Saved: {OUTPUT_JSON}  (items={len(out)}, errors={len(errors)})")


if __name__ == "__main__":
    main()

Python 2 · Traducción automática del JSON

Este segundo script carga el JSON base en inglés y traduce automáticamente los campos principales al español, generando la versión consultant_signs_canine_ES.json.

Entrada consultant_signs_canine.json Salida consultant_signs_canine_ES.json Librería deep_translator
import json
import time
from deep_translator import GoogleTranslator
from deep_translator.exceptions import TranslationNotFound

# Configurar traductor
traductor = GoogleTranslator(source='en', target='es')

# Cargar datos
filename = 'consultant_signs_canine.json'

try:
    with open(filename, 'r', encoding='utf-8') as f:
        data = json.load(f)
except FileNotFoundError:
    print(f"Error: No se encuentra {filename}")
    exit()

items = data['items']
total = len(items)

def traducir_texto(texto):
    if not texto or texto.strip() == "":
        return texto
    try:
        # Intentar traducir
        return traductor.translate(texto)
    except Exception:
        try:
            # Si falla, esperar 5 segundos y reintentar una vez
            print(f"  ...reintentando traducción para: {texto[:30]}...")
            time.sleep(5)
            return traductor.translate(texto)
        except Exception:
            # Si vuelve a fallar, devolver el original para no romper el script
            print(f"  ⚠️ No se pudo traducir, manteniendo original: {texto[:30]}")
            return texto

print(f"Iniciando traducción de {total} procesos médicos...")

for i, item in enumerate(items):
    # Traducir Proceso
    item['Proceso'] = traducir_texto(item['Proceso'])

    # Traducir Especialidad
    item['especialidades'] = traducir_texto(item['especialidades'])

    # Traducir lista de diagnósticos
    nuevos_dx = []
    for dx in item['Diagnósticos diferenciales']:
        nuevos_dx.append(traducir_texto(dx))
        # Pausa mínima entre frases para evitar bloqueos
        time.sleep(0.1)

    item['Diagnósticos diferenciales'] = nuevos_dx

    # Mostrar progreso
    print(f"✅ Progreso: {i+1}/{total} ({((i+1)/total)*100:.1f}%) - Último: {item['Proceso']}")

    # Guardar copia de seguridad cada 10 items por si acaso
    if i % 10 == 0:
        with open('consultant_signs_canine_ES_temp.json', 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

# Guardar resultado final
with open('consultant_signs_canine_ES.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

print("\n¡FINALIZADO! Archivo guardado como: consultant_signs_canine_ES.json")