Actualización de artículos de NYTimes News (CN) | Generada y traducida por IA

Home PDF Audio

Haz clic en el botón de abajo para actualizar los artículos de NYTimes CN.

Esta página te permite desencadenar una actualización de los artículos de NYTimes CN. Hacer clic en el botón iniciará un flujo de trabajo que obtiene los artículos más recientes, los traduce y actualiza el contenido en este sitio. Ten en cuenta que puede llevar unos minutos para que los cambios aparezcan después de que se desencadene la actualización.

Código en Python:

import requests
from bs4 import BeautifulSoup
import os
import ssl
from dotenv import load_dotenv
import os
import sys

load_dotenv()

def call_mistral_api(prompt, model="mistral-small-2501"):
    """Llama a la API de Mistral para traducir texto."""
    api_key = os.environ.get("MISTRAL_API_KEY")
    if not api_key:
        print("Error: la variable de entorno MISTRAL_API_KEY no está configurada.")
        return None

    url = "https://api.mistral.ai/v1/chat/completions"
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": f"Bearer {api_key}"
    }

    data = {
        "model": model,
        "messages": [
            {
                "role": "user",
                "content": prompt
            }
        ]
    }

    try:
        print(f"Llamando a la API de Mistral con el modelo: {model}")
        print(f"Prompt enviado: {prompt[:1000]}...")  # Imprime los primeros 100 caracteres del prompt
        response = requests.post(url, headers=headers, json=data)
        response.raise_for_status()
        response_json = response.json()
        print(f"Respuesta de la API de Mistral: {response_json}")
        if response_json and response_json['choices']:
            content = response_json['choices'][0]['message']['content']
            print(f"Contenido de la API de Mistral: {content}")
            return content
        else:
            print(f"Error de la API de Mistral: formato de respuesta no válido: {response_json}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"Error de la API de Mistral: {e}")
        if e.response:
            print(f"Código de estado de la respuesta: {e.response.status_code}")
            print(f"Contenido de la respuesta: {e.response.text}")
        return None

def fetch_html_content(url):
    """Obtiene el contenido HTML de una URL dada."""
    try:
        # Crear un contexto SSL no verificado
        context = ssl._create_unverified_context()
        print(f"Obteniendo contenido HTML de: {url}")
        response = requests.get(url, verify=False)
        response.raise_for_status()  # Lanzar HTTPError para respuestas incorrectas (4xx o 5xx)
        print(f"Contenido HTML obtenido exitosamente de: {url}")
        return response.text
    except requests.exceptions.RequestException as e:
        print(f"No se pudo obtener la URL: {url} - {e}")
        return None

def extract_links(html):
    """Extrae enlaces de la página principal de cn.nytimes.com."""
    soup = BeautifulSoup(html, 'html.parser')
    links = []
    for a in soup.find_all('a', href=True):
        url = a['href']
        if url.startswith('https://cn.nytimes.com/'):
            links.append({
                'url': url,
                'text': a.text.strip()
            })
    print(f"Se extrajeron {len(links)} enlaces de la página principal.")
    return links

def translate_title(title):
    """Traduce el título del chino al inglés usando Mistral."""
    base_prompt = "Traduce el siguiente título a {target_language}. Proporciona solo el título traducido, sin notas ni explicaciones adicionales. No repitas ni menciones el texto de entrada.\n"
    prompt = base_prompt.format(target_language="English") + f"{title}"
    print(f"Traduciendo título: {title}")
    translated_title = call_mistral_api(prompt)
    if translated_title:
        translated_title = translated_title.strip()
        print(f"Título traducido: {translated_title}")
        return translated_title
    else:
        raise Exception(f"Fallo al traducir el título: {title}")

def summarize_article(html):
    """Resumen del contenido del artículo usando la API de Mistral en inglés."""
    soup = BeautifulSoup(html, 'html.parser')
    title_element = soup.select_one('.article-area .article-content .article-header header h1')
    title = title_element.text.strip() if title_element else ''
    print(f"Título extraído: {title}")

    # Extraer el texto principal del artículo
    article_area = soup.find('div', class_='article-area')
    if article_area:
        article_text = article_area.get_text(separator='\n', strip=True)
    else:
        article_text = None

    if not article_text:
        print("No se pudo extraer el texto del artículo.")
        return None, None

    # Crear un prompt para que Mistral resuma
    prompt = f"Resumen del siguiente artículo en inglés, enfocándose en los puntos principales y evitando frases introductorias como 'Resumen:' o 'Este artículo es sobre:'.\n\n{article_text[:30000]}\n\n"  # Limitar el texto del artículo a 30000 caracteres
    print(f"Creando resumen para el título: {title}")
    summary = call_mistral_api(prompt)

    if summary:
        # Limpiar el resumen eliminando "Resumen:" o frases similares
        summary = summary.replace("Summary:", "").strip()
        print(f"Resumen generado: {summary}")
        return title, summary
    else:
        print(f"Fallo al generar resumen para el título: {title}")
        return None, None

def generate_markdown_list(articles):
    """Genera una lista de Markdown a partir de una lista de resúmenes de artículos."""
    if not articles:
        return '* No se encontraron artículos.\n'

    markdown_list = ''
    for article in articles:
        title, summary = article
        translated_title = translate_title(title)
        markdown_list += f'## {translated_title}\n\n{summary}\n\n'
    print("Lista de Markdown generada.")
    return markdown_list

def update_markdown_file(filename, markdown_content):
    """Actualiza un archivo Markdown con el contenido dado."""
    try:
        # Leer el contenido existente del archivo
        print(f"Leyendo contenido existente de {filename}")
        with open(filename, 'r', encoding='utf-8') as f:
            existing_content = f.read()

        # Encontrar las posiciones de inicio y fin del contenido después de los metadatos iniciales
        start_index = existing_content.find('---', 3) + 4  # Encontrar el segundo '---' y moverse más allá de él
        end_index = len(existing_content)

        # Construir el contenido actualizado
        updated_content = existing_content[:start_index].strip() + '\n\n'  # Mantener los metadatos y agregar un salto de línea
        updated_content += markdown_content.strip() + '\n'  # Agregar la nueva lista de Markdown
        # updated_content += existing_content[end_index:].strip() # Anexar cualquier cosa después de la lista, si existe

        with open(filename, 'w', encoding='utf-8') as f:
            f.write(updated_content)
        print(f"{filename} actualizado exitosamente")
        markdown_changed = existing_content != updated_content
        print(f"Markdown cambiado: {markdown_changed}")
        return markdown_changed
    except Exception as e:
        print(f"Error actualizando {filename}: {e}")
        return False

def main():
    """Función principal para obtener, procesar y actualizar el archivo Markdown."""
    nytimes_url = 'https://m.cn.nytimes.com'
    print(f'Obteniendo enlaces de NYTimes desde: {nytimes_url}')

    html_content = fetch_html_content(nytimes_url)
    if not html_content:
        print("Fallo al obtener el contenido de la página principal.")
        return

    links = extract_links(html_content)
    print(f'Se encontraron {len(links)} enlaces en la página principal. Extrayendo enlaces...')

    all_articles = []
    for i, link in enumerate(links):
        url = link["url"]
        if not url.endswith('/dual/'):
            if not url.endswith('/'):
                url = url + '/dual/'
            else:
                url = url + 'dual/'

        print(f'Procesando enlace {i + 1} de {len(links)}: {url}')
        article_html = fetch_html_content(url)
        if article_html:
            title, summary = summarize_article(article_html)
            if title and summary:
                all_articles.append((title, summary))
        else:
            print(f'Fallo al obtener contenido desde {link["url"]}')

    markdown_list = generate_markdown_list(all_articles)

    filename = 'original/2025-03-14-nytimes-en.md'
    markdown_changed = update_markdown_file(filename, markdown_list)

    if markdown_changed:
        print("El archivo Markdown se actualizó con nuevos enlaces.")
        sys.exit(0)
    else:
        print("El archivo Markdown no se actualizó (sin cambios).")
        sys.exit(1)

if __name__ == "__main__":
    main()

Código del frontend:

const nytimesDiv = document.querySelector('.nytimes');

if (nytimesDiv) {
    const updateButton = document.createElement('button');
    updateButton.textContent = 'Actualizar artículos de NYTimes';
    nytimesDiv.appendChild(updateButton);

    updateButton.addEventListener('click', () => {
        fetch('https://api.github.com/repos/lzwjava/lzwjava.github.io/actions/workflows/nytimes.yml/dispatches', {
            method: 'POST',
            headers: {
                'Accept': 'application/vnd.github+json',
                'Authorization': 'Bearer token',
                'X-GitHub-Api-Version': '2022-11-28',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ ref: 'main' })
        })
        .then(response => {
            if (response.status === 204) {
                alert('Actualización desencadenada exitosamente! Por favor, espera unos minutos para ver el resultado.');
            } else {
                alert(`Fallo en la actualización. Código de estado: ${response.status}`);
                console.error('Fallo en la actualización:', response);
            }
        })
        .catch(error => {
            alert('Fallo en la actualización. Consulta la consola para errores.');
            console.error('Error al desencadenar la actualización:', error);
        });
    });
} else {
    console.error("Div de nytimes no encontrado!");
}

Back 2025.04.02 Donate