基于Node.js的微博热榜抓取与展示开发记录

系统架构概述

本项目实现了一个完整的微博热榜抓取与展示,主要流程如下:

  1. 定时任务抓取微博热榜信息
  2. 存储到MySQL数据库
  3. Node.js服务从MySQL读取数据
  4. 前端页面通过接口获取数据并展示

技术实现细节

1. MySQL数据库操作

安装MySQL

bash

bash 复制代码
# CentOS
sudo yum install mysql-server
sudo systemctl start mysqld
sudo mysql_secure_installation

建表操作

sql

sql 复制代码
CREATE DATABASE hot_test;

USE hot_test;

CREATE TABLE hot_list (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL COMMENT '标题',
    link VARCHAR(255) NOT NULL COMMENT '链接',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    sort INT NOT NULL COMMENT '序号,表示数据的排列顺序',
    UNIQUE KEY uk_sort (sort) COMMENT '确保序号唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='热点列表表';

数据操作示例

sql

sql 复制代码
-- 添加数据
INSERT INTO hot_list (title, link, create_time, sort) VALUES ?

2. Node.js数据库连接与操作

javascript

javascript 复制代码
// db.js
const mysql = require('mysql2/promise');

class HotListManager {
    /**
     * 构造函数
     * @param {Object} config 数据库配置
     * @param {string} config.host 数据库主机
     * @param {string} config.user 数据库用户名
     * @param {string} config.password 数据库密码
     * @param {string} config.database 数据库名
     */
    constructor (config){
        this.config = config;
        this.pool = mysql.createPool({
            host: config.host,
            user: config.user,
            password: config.password,
            database: config.database,
            waitForConnections: true,
            connectionLimit: 10,
            queueLimit: 0
        })
    }
    // 批量添加数据到 hot_list表
    async batchInsert(items) {}
    // 查询 hot_list 表全部数据
    async getAllData() {}
    // 关闭数据库连接池
    async close() {}
}
module.exports = HotListManager

javascript

javascript 复制代码
// 增删查操作示例
const pool = require('./db');

/**
 * 批量添加数据到 hot_list表
 * @param {Array} items 要添加的数据数组
 * @returns {Promise<number>} 成功添加的记录数
 */
async batchInsert(items) {
    if (!Array.isArray(items)) {
        throw new Error('批量添加数据失败:items 必须是一个数组');
    }
    let connection;
    try {
       connection = await this.pool.getConnection();
        await connection.beginTransaction();
         await connection.query('TRUNCATE TABLE hot_list;');

        const sql = "INSERT INTO hot_list (title, link, create_time, sort) VALUES ?";
        const values = items.map(item => {
            if (!item.title || !item.link) {
                throw new Error('每个项目必须包含 title 和 link 属性');
            }
            return [
                item.title,
                item.link,
                item.create_time,
                item.sort || 0, // 提供默认排序值
            ];
        });

        const [result] = await connection.query(sql, [values]);
        await connection.commit();
        return result.affectedRows;
    } catch (err) {
        if (connection) await connection.rollback();
        throw new Error('批量添加数据失败' + err.message);
    } finally {
       if (connection) connection.release();
    }
}

/**
 * 查询 hot_list 表全部数据
 * @returns {Promise<Array>} 查询结果数组
 */
async getAllData() {
    const connection = await this.pool.getConnection();
    try {
        const [rows] = await connection.query("SELECT * FROM hot_list ORDER BY sort ASC");
        return rows;
    } catch (error) {
        throw new Error(`查询数据失败: ${error.message}`);
    } finally {
        connection.release();
    }
}

3. 微博热榜抓取方案

经过尝试,最终采用Puppeteer方案:

javascript

javascript 复制代码
const puppeteer = require('puppeteer');

async function fetchWeiboHotList() {
  const browser = await puppeteer.launch({
        headless: true,
        args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--disable-blink-features=AutomationControlled',
            '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        ]
    })

    try {
        const page = await browser.newPage();
        await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
        
        // 设置页面视口大小
        await page.setViewport({ width: 1366, height: 768 });

        // 打开微博热榜
        await page.goto('https://s.weibo.com/top/summary', {
            waitUntil: 'networkidle2',
            timeout: 30000
        });

        // 等待内容加载 - 增加更可靠的等待方式
        await page.waitForSelector('#pl_top_realtimehot', { timeout: 15000 });
        
        // 确保表格内容加载完成
        await page.waitForSelector('#pl_top_realtimehot table tbody tr', { timeout: 10000 });

        // 提取热榜数据 - 修复后的evaluate函数
        const hotList = await page.evaluate(() => {
            const items = Array.from(document.querySelectorAll('#pl_top_realtimehot table tbody tr'));
            return items.map(item => {
                const titleElement = item.querySelector('td.td-02 a');
                const title = titleElement ? titleElement.innerText.trim() : null;
                const link = titleElement ? titleElement.getAttribute('href') : null;
                const sortElement = item.querySelector('td.td-01');
                const sort = sortElement ? sortElement.innerText.trim() : null;
                
                return {
                    title,
                    link: link ? `https://s.weibo.com${link}` : null,
                    sort,
                    create_time: new Date().toISOString().slice(0, 19).replace('T', ' ')
                };
            }).filter(item => item.title && item.sort && item.sort !== '•' && item.sort !== '·'); // 过滤掉无效条目
        });
        
        // 将抓取到的数据存储到mysql中
        if (hotList.length > 0) {
            const length = await mydb.batchInsert(hotList);
            console.log('成功抓取微博热榜' + length + '条数据')
        } else {
            console.warn('未获取到有效的热榜数据');
        }
        
    } catch (err) {
        console.error('抓取微博热榜失败:', err);
    } finally {
        mydb.close();
        await browser.close();
    }
}

4. 定时任务实现

javascript

ini 复制代码
const schedule = require('node-schedule');

// 每小时执行一次
schedule.scheduleJob('0 * * * *', fetchWeiboHot);

5. Express服务端配置

javascript

javascript 复制代码
const express = require('express')
const cors = require('cors'); // 引入 cors 模块
const HotListManager = require('../db/index.js')
const mydb = new HotListManager({
    host: '127.0.0.1',
    user: 'root',
    password: '1234567890',
    database: 'hot_test'
})

const app = express()
app.use(cors()); // 使用默认配置允许所有跨域请求
const port = 3000
// API
app.get('/api/weibo', async (req, res) => {
  try {
    const hotList = await mydb.getAllData()
    if (hotList) {
      res.json({
        success: true,
        data: hotList
      })
    } else {
      res.status(404).json({
        success: false,
        message: '暂无数据'
      })
    }
  } catch (error) {
    console.error('Error fetching weibo data:', error)
    res.status(500).json({
      success: false,
      message: '服务器错误'
    })
  }
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

部署到服务器

部署中。。。

相关推荐
咬人喵喵1 天前
CSS Flexbox:拥有魔法的排版盒子
前端·css
LYFlied1 天前
TS-Loader 源码解析与自定义 Webpack Loader 开发指南
前端·webpack·node.js·编译·打包
yzp01121 天前
css收集
前端·css
暴富的Tdy1 天前
【Webpack 的核心应用场景】
前端·webpack·node.js
遇见很ok1 天前
Web Worker
前端·javascript·vue.js
风舞红枫1 天前
前端可配置权限规则案例
前端
zhougl9961 天前
前端模块化
前端
xiliuhu1 天前
Node.js 的事件循环机制
node.js
暴富暴富暴富啦啦啦1 天前
Map 缓存和拿取
前端·javascript·缓存
天问一1 天前
前端Vue使用js-audio-plugin实现录音功能
前端·javascript·vue.js