Implementación de un crawler de noticias en PHP con integración a WordPress y notificación a Baidu

Este crawler se desarrolla para automatizar la obtención de noticias populares de fuentes como Baidu, filtrar contenido relevante de Sohu, e incorporarlo en una base de datos de WordPress, además de enviar URLs a Baidu para indexación.

Componentes principales

El sistema se compone de módulos PHP para el raspado web, manejo de base de datos, y envío de datos a APIs externas.

Módulo de raspado web

La clase WebExtractor utiliza cURL para recuperar páginas web, configurando encabezados y manejando codificaciones de caracteres para evitar errores de visualización.


<?php class WebExtractor {
    private $conexion;
    private $tiempo_espera = 5;
    private $contenido;
    private $url_origen;

    public function __construct($enlace) {
        $this-?>url_origen = $enlace;
        $this->conexion = curl_init();
        curl_setopt($this->conexion, CURLOPT_ENCODING, "");
        curl_setopt($this->conexion, CURLOPT_URL, $enlace);
        curl_setopt($this->conexion, CURLOPT_HEADER, 0);
        curl_setopt($this->conexion, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($this->conexion, CURLOPT_CONNECTTIMEOUT, $this->tiempo_espera);
        curl_setopt($this->conexion, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
        $this->contenido = curl_exec($this->conexion);
        if ($this->contenido) {
            if (preg_match("/<meta.>contenido, $codificacion)) {
                $cod = strtolower($codificacion[1]);
                if ($cod !== 'utf-8' && $cod !== '') {
                    $this->contenido = iconv($cod, "utf-8//IGNORE", $this->contenido);
                }
            }
        } else {
            return false;
        }
    }
}
?>
</meta.>

Módulo de base de datos

La clase DatabaseManager gestiona la conexión a MySQL, ejecución de consultas y obtención de resultados, utilizando mysqli en lugar de funciones obsoletas como mysql_*.


<?php header('Content-Type: text/html; charset=UTF-8');
class DatabaseManager {
    private $conexion;

    public function __construct($servidor, $usuario, $contrasena, $base_datos) {
        $this-?>conexion = new mysqli($servidor, $usuario, $contrasena, $base_datos);
        if ($this->conexion->connect_error) {
            die('Error de conexión: ' . $this->conexion->connect_error);
        }
        $this->conexion->set_charset("utf8");
    }

    public function ejecutarConsulta($sql) {
        return $this->conexion->query($sql);
    }

    public function obtenerResultados($resultado) {
        $lista = array();
        while ($fila = $resultado->fetch_assoc()) {
            $lista[] = $fila;
        }
        return $lista;
    }

    public function filasAfectadas() {
        return $this->conexion->affected_rows;
    }

    public function ultimoId() {
        return $this->conexion->insert_id;
    }

    public function cerrarConexion() {
        $this->conexion->close();
    }
}
?>

Módulo de envío a Baidu

La clase BaiduSubmitter permite enviar URLs a la API de Baidu para acelerar la indexación mediante una solicitud POST con los enlaces.


<?php class BaiduSubmitter {
    private $respuesta;

    public function __construct($urls, $api_endpoint) {
        $sesion = curl_init();
        $opciones = array(
            CURLOPT_URL =?> $api_endpoint,
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POSTFIELDS => implode("\n", $urls),
            CURLOPT_HTTPHEADER => array('Content-Type: text/plain'),
        );
        curl_setopt_array($sesion, $opciones);
        $this->respuesta = curl_exec($sesion);
        curl_close($sesion);
    }

    public function obtenerRespuesta() {
        return $this->respuesta;
    }
}
?>

Lógica principal de extracción e inserción

La clase NewsProcessor extiende WebExtractor para procesar páginas de noticias de Sohu, extraer títulos, contenido y fuentes, limpiar etiquetas HTML, e insertar los datos en la base de datos de WordPress, evitando duplicados.


<?php class NewsProcessor extends WebExtractor {
    private $titulo = array();
    private $contenido_filtrado = array();
    private $categoria;

    public function procesarNoticiaSohu($categoria) {
        if (preg_match('/top\-pager\-current/', $this-?>contenido)) {
            echo "Contenido omitido por paginación";
        } else {
            // Extracción del cuerpo del artículo
            if (preg_match('/<div>]*itemprop="articleBody"[^>]*>(.*?) seo/si', $this->contenido, $datos)) {
                $this->contenido_filtrado = $datos;
            } elseif (preg_match('/<div>]*id="contentText"[^>]*>(.*?) seo/si', $this->contenido, $datos)) {
                $this->contenido_filtrado = $datos;
            }
            // Extracción del título
            if (preg_match('/<h1>(.*?)<\/h1>/si', $this->contenido, $tit)) {
                $this->titulo = $tit;
            }
            // Limpieza y filtrado
            $this->contenido_filtrado[0] = $this->limpiarHTML($this->contenido_filtrado[0], "<img></img><div><span><p>");
            $this->titulo[0] = $this->limpiarHTML($this->titulo[0], "");
            // Obtener fuente
            if (preg_match('/<span itemprop="name">(.*?)<\/span>/si', $this->contenido, $fuente)) {
                $fuente_limpia = $this->limpiarHTML($fuente[0], "");
            }
            // Validación e inserción
            if (!empty($this->contenido_filtrado[0]) && !empty($this->titulo[0]) && isset($fuente_limpia)) {
                $fecha_actual = date('Y-m-d H:i:s');
                $this->insertarEnBaseDatos($this->titulo[0], $fecha_actual, $this->contenido_filtrado[0], $fuente_limpia, $this->url_origen, $categoria);
            }
        }
    }

    private function insertarEnBaseDatos($titulo, $fecha, $contenido, $fuente, $url_identificador, $categoria) {
        global $db_manager;
        // Verificar si ya existe
        $consulta_verificacion = "SELECT COUNT(*) as contador FROM wp_posts WHERE post_content_filtered='{$url_identificador}'";
        $resultado = $db_manager->ejecutarConsulta($consulta_verificacion);
        $fila = $db_manager->obtenerResultados($resultado);
        if ($fila[0]['contador'] == 0) {
            // Insertar nuevo post
            $sql_insert = "INSERT INTO wp_posts (post_author, post_date, post_date_gmt, post_content, post_title, post_status, post_name, post_modified, post_modified_gmt, post_content_filtered, post_type, laiyuan) VALUES ('1', '{$fecha}', '{$fecha}', '{$contenido}', '{$titulo}', 'publish', 'slug', '{$fecha}', '{$fecha}', '{$url_identificador}', 'post', '{$fuente}')";
            $db_manager->ejecutarConsulta($sql_insert);
            $filas_afectadas = $db_manager->filasAfectadas();
            if ($filas_afectadas == 1) {
                $id_nuevo = $db_manager->ultimoId();
                // Enviar a Baidu
                $urls_para_envio = array("http://ejemplo.com/archivos/{$id_nuevo}.html");
                $api_baidu = 'http://data.zz.baidu.com/urls?site=ejemplo.com&token=tu_token';
                $submitter = new BaiduSubmitter($urls_para_envio, $api_baidu);
                echo $submitter->obtenerRespuesta();
                // Asignar categoría
                $sql_categoria = "INSERT INTO wp_term_relationships (object_id, term_taxonomy_id) VALUES ('{$id_nuevo}', '{$categoria}')";
                $db_manager->ejecutarConsulta($sql_categoria);
                echo "Noticia '{$titulo}' insertada con ID {$id_nuevo}";
            } else {
                echo "Error al insertar noticia '{$titulo}'";
            }
        } else {
            echo "Noticia duplicada ignorada";
        }
    }

    private function limpiarHTML($datos, $excepciones) {
        return strip_tags($datos, $excepciones);
    }

    public function __destruct() {
        curl_close($this->conexion);
    }
}
?>

<h3>Flujo de ejecución para listados de noticias</h3>
<p>Las clases ListScraper y SohuScraper manejan la obtención de listas de noticias desde fuentes como Baidu, aplicando expresiones regulares para extraer enlaces y luego procesar cada noticia individualmente.</p>

<?php class ListScraper extends WebExtractor {
    public function procesarListado($expresion_regular, $categoria) {
        $this-?>contenido = $this->limpiarHTML($this->contenido, '<td>');
        $this->contenido = preg_replace("/search/si", "", $this->contenido);
        if (preg_match_all($expresion_regular, $this->contenido, $coincidencias, PREG_SET_ORDER)) {
            $enlaces = array_unique(array_column($coincidencias, 0));
            foreach ($enlaces as $indice => $enlace) {
                if ($enlace) {
                    $enlace_limpio = trim(strip_tags($enlace));
                    $enlace_codificado = rawurlencode($enlace_limpio);
                    $url_sohu = "http://news.sogou.com/news?query=site%3Asohu.com+" . $enlace_codificado;
                    $scraper_sohu = new SohuScraper($url_sohu);
                    $scraper_sohu->extraerNoticia('/http:\/\/([\.a-z]+)\.sohu\.com\/20(\d+)\/n(\d+)\.shtml/', $categoria);
                }
            }
        }
    }

    private function limpiarHTML($datos, $excepciones) {
        return strip_tags($datos, $excepciones);
    }

    public function __destruct() {
        curl_close($this->conexion);
    }
}

class SohuScraper extends WebExtractor {
    public function extraerNoticia($patron_enlace, $categoria) {
        if (preg_match('/<h3 class="vrTitle">(.*?)<\/h3>/si', $this->contenido, $encabezado)) {
            $contenido_limpio = $this->limpiarHTML($encabezado[0], '<a><h3>');
            if (preg_match($patron_enlace, $contenido_limpio, $enlace_noticia)) {
                $procesador = new NewsProcessor($enlace_noticia[0]);
                $procesador->procesarNoticiaSohu($categoria);
            }
        }
    }

    private function limpiarHTML($datos, $excepciones) {
        return strip_tags($datos, $excepciones);
    }

    public function __destruct() {
        curl_close($this->conexion);
    }
}

// Ejemplo de configuración y ejecución
$db_manager = new DatabaseManager('localhost', 'usuario', 'contrasena', 'wordpress_db');
$categorias = array(
    array('id' => 3021, 'url' => 'http://top.baidu.com/buzz?b=344&c=513', 'nombre' => 'Entretenimiento'),
    array('id' => 2585, 'url' => 'http://top.baidu.com/buzz?b=341&c=513', 'nombre' => 'Tendencias'),
);
foreach ($categorias as $cat) {
    $scraper_listado = new ListScraper($cat['url']);
    $scraper_listado->procesarListado('/<td>]*class="keyword">(.*?)<\/td>/si', $cat['id']);
}
$db_manager->cerrarConexion();
?>

<p>Este enfoque permite una integración automatizada de contenido dinámico en sitios WordPress, optimizando la recopilación de noticias y su visibilidad en motores de búsqueda.</p></td></h3></a></h3></td></span></p></span></div></h1></div></div>

Etiquetas: PHP wordpress Web Scraping MySQL Baidu API

Publicado el 6-12 19:09