基于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}`)
})

部署到服务器

部署中。。。

相关推荐
一_个前端8 分钟前
Vite项目中SVG同步转换成Image对象
前端
20269 分钟前
12. npm version方法总结
前端·javascript·vue.js
用户876128290737410 分钟前
mapboxgl中对popup弹窗添加事件
前端·vue.js
帅夫帅夫11 分钟前
JavaScript继承探秘:从原型链到ES6 Class
前端·javascript
a别念m11 分钟前
HTML5 离线存储
前端·html·html5
goldenocean42 分钟前
React之旅-06 Ref
前端·react.js·前端框架
子林super44 分钟前
【非标】es屏蔽中心扩容协调节点
前端
前端拿破轮1 小时前
刷了这么久LeetCode了,挑战一道hard。。。
前端·javascript·面试
代码小学僧1 小时前
「双端 + 响应式」企业官网开发经验分享
前端·css·响应式设计
土豆骑士1 小时前
简单理解Typescript 装饰器
前端·typescript