系统架构概述
本项目实现了一个完整的微博热榜抓取与展示,主要流程如下:
- 定时任务抓取微博热榜信息
- 存储到MySQL数据库
- Node.js服务从MySQL读取数据
- 前端页面通过接口获取数据并展示
技术实现细节
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}`)
})
部署到服务器
部署中。。。