287 lines
8.9 KiB
JavaScript
287 lines
8.9 KiB
JavaScript
const axios = require('axios');
|
||
const cheerio = require('cheerio');
|
||
const { PrismaClient } = require('../../generated/prisma');
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
// 浙江数据开放网站URL(示例,实际需要根据真实网站修改)
|
||
const ZHEJIANG_OPEN_DATA_URL = 'https://data.zj.gov.cn/';
|
||
|
||
// 爬虫名称,用于缓存表中的标识
|
||
const SCRAPER_NAME = 'sow-inventory';
|
||
|
||
// 最小更新间隔(毫秒),默认30分钟
|
||
const MIN_UPDATE_INTERVAL = 30 * 60 * 1000;
|
||
|
||
// 从数据库读取缓存
|
||
async function readCache() {
|
||
try {
|
||
const cache = await prisma.scraperCache.findUnique({
|
||
where: { scraperName: SCRAPER_NAME }
|
||
});
|
||
return cache || { scraperName: SCRAPER_NAME, lastDataHash: null, lastUpdateAt: null };
|
||
} catch (error) {
|
||
console.error('读取数据库缓存失败:', error.message);
|
||
return { scraperName: SCRAPER_NAME, lastDataHash: null, lastUpdateAt: null };
|
||
}
|
||
}
|
||
|
||
// 写入数据库缓存
|
||
async function writeCache(lastDataHash) {
|
||
try {
|
||
await prisma.scraperCache.upsert({
|
||
where: { scraperName: SCRAPER_NAME },
|
||
update: {
|
||
lastDataHash,
|
||
lastUpdateAt: new Date()
|
||
},
|
||
create: {
|
||
scraperName: SCRAPER_NAME,
|
||
lastDataHash,
|
||
lastUpdateAt: new Date()
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('写入数据库缓存失败:', error.message);
|
||
}
|
||
}
|
||
|
||
// 计算数据hash,用于检测数据是否变化
|
||
function computeDataHash(data) {
|
||
const str = JSON.stringify(data);
|
||
let hash = 0;
|
||
for (let i = 0; i < str.length; i++) {
|
||
const char = str.charCodeAt(i);
|
||
hash = ((hash << 5) - hash) + char;
|
||
hash = hash & hash;
|
||
}
|
||
return hash.toString();
|
||
}
|
||
|
||
// 获取数据库中最新的月份
|
||
async function getLatestMonth() {
|
||
try {
|
||
const latest = await prisma.econ_SowInventory.findFirst({
|
||
orderBy: {
|
||
month: 'desc'
|
||
}
|
||
});
|
||
return latest ? latest.month : null;
|
||
} catch (error) {
|
||
console.error('查询最新月份失败:', error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 判断月份是否需要更新(只有比数据库中更新的月份才需要处理)
|
||
function shouldUpdateMonth(month, latestDbMonth) {
|
||
if (!latestDbMonth) return true;
|
||
return month > latestDbMonth;
|
||
}
|
||
|
||
// 检查是否需要发起HTTP请求(基于更新间隔)
|
||
function shouldSkipRequest(cache) {
|
||
if (!cache.lastUpdateAt) return false;
|
||
|
||
const now = new Date();
|
||
const lastUpdate = new Date(cache.lastUpdateAt);
|
||
const interval = now - lastUpdate;
|
||
|
||
return interval < MIN_UPDATE_INTERVAL;
|
||
}
|
||
|
||
// 能繁母猪数量数据爬取函数(增量更新版本)
|
||
async function scrapeSowInventory(incremental = true) {
|
||
try {
|
||
console.log('='.repeat(50));
|
||
console.log('开始爬取浙江省能繁母猪数量数据...');
|
||
console.log(`增量更新模式: ${incremental ? '开启' : '关闭'}`);
|
||
console.log('='.repeat(50));
|
||
|
||
// 0. 检查是否需要发起请求
|
||
if (incremental) {
|
||
const cache = await readCache();
|
||
|
||
if (shouldSkipRequest(cache)) {
|
||
const lastUpdate = cache.lastUpdateAt ? new Date(cache.lastUpdateAt) : null;
|
||
const hoursDiff = lastUpdate ? ((new Date() - lastUpdate) / (1000 * 60 * 60)).toFixed(2) : '未知';
|
||
console.log('\n' + '='.repeat(50));
|
||
console.log('跳过请求:距离上次更新不足30分钟');
|
||
console.log(`上次更新时间: ${cache.lastUpdateAt}`);
|
||
console.log(`距离现在: ${hoursDiff} 小时`);
|
||
console.log('='.repeat(50));
|
||
return;
|
||
}
|
||
|
||
// 获取数据库中最新的月份
|
||
const latestDbMonth = await getLatestMonth();
|
||
if (latestDbMonth) {
|
||
console.log(`数据库中最新月份: ${latestDbMonth}`);
|
||
} else {
|
||
console.log('数据库为空,将获取所有数据');
|
||
}
|
||
}
|
||
|
||
// 1. 发送HTTP请求获取页面内容
|
||
console.log('\n正在请求数据源...');
|
||
const response = await axios.get(ZHEJIANG_OPEN_DATA_URL, {
|
||
headers: {
|
||
'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'
|
||
}
|
||
});
|
||
|
||
// 2. 解析HTML内容
|
||
const $ = cheerio.load(response.data);
|
||
|
||
// 3. 提取数据(这里需要根据实际网站结构修改选择器)
|
||
const allData = [];
|
||
|
||
$('table').each((tableIndex, table) => {
|
||
$(table).find('tr').each((rowIndex, row) => {
|
||
if (rowIndex > 0) { // 跳过表头
|
||
const cells = $(row).find('td');
|
||
if (cells.length >= 2) {
|
||
const month = $(cells[0]).text().trim();
|
||
const inventory = parseInt($(cells[1]).text().trim());
|
||
|
||
if (month && !isNaN(inventory)) {
|
||
allData.push({
|
||
month,
|
||
inventory
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
console.log(`页面总数据量: ${allData.length} 条`);
|
||
|
||
// 4. 读取数据库缓存
|
||
const cache = await readCache();
|
||
|
||
// 5. 计算当前数据hash,检测数据是否变化
|
||
const currentDataHash = computeDataHash(allData);
|
||
|
||
// 6. 如果是增量更新且数据未变化,跳过更新
|
||
if (incremental && cache.lastDataHash === currentDataHash) {
|
||
console.log('\n' + '='.repeat(50));
|
||
console.log('数据未发生变化,跳过数据库更新!');
|
||
console.log(`上次更新时间: ${cache.lastUpdateAt || '未知'}`);
|
||
console.log('='.repeat(50));
|
||
return;
|
||
}
|
||
|
||
// 7. 如果是增量更新,获取数据库中最新的月份用于过滤
|
||
let latestDbMonth = null;
|
||
if (incremental) {
|
||
latestDbMonth = await getLatestMonth();
|
||
}
|
||
|
||
// 8. 过滤需要更新的数据
|
||
let dataToUpdate = allData;
|
||
let skippedCount = 0;
|
||
|
||
if (incremental && latestDbMonth) {
|
||
dataToUpdate = allData.filter(item => shouldUpdateMonth(item.month, latestDbMonth));
|
||
skippedCount = allData.length - dataToUpdate.length;
|
||
console.log(`\n增量更新: 跳过 ${skippedCount} 条已存在的数据`);
|
||
console.log(`需要处理: ${dataToUpdate.length} 条新数据`);
|
||
}
|
||
|
||
// 9. 如果没有需要更新的数据
|
||
if (dataToUpdate.length === 0) {
|
||
console.log('\n没有需要更新的数据!');
|
||
return;
|
||
}
|
||
|
||
// 10. 按月份排序(从旧到新)
|
||
dataToUpdate.sort((a, b) => a.month.localeCompare(b.month));
|
||
|
||
console.log('\n开始处理数据...');
|
||
console.log('-'.repeat(50));
|
||
|
||
let newCount = 0;
|
||
let updateCount = 0;
|
||
let errorCount = 0;
|
||
|
||
// 11. 存储数据到数据库
|
||
for (const item of dataToUpdate) {
|
||
try {
|
||
// 检查数据是否已存在
|
||
const existing = await prisma.econ_SowInventory.findUnique({
|
||
where: { month: item.month }
|
||
});
|
||
|
||
if (existing) {
|
||
// 检查数据是否真的需要更新(值变化了才更新)
|
||
if (existing.inventory !== item.inventory) {
|
||
await prisma.econ_SowInventory.update({
|
||
where: { month: item.month },
|
||
data: { inventory: item.inventory }
|
||
});
|
||
updateCount++;
|
||
console.log(`更新数据: ${item.month} - ${item.inventory} (原值: ${existing.inventory})`);
|
||
} else {
|
||
console.log(`数据未变化,跳过: ${item.month}`);
|
||
}
|
||
} else {
|
||
await prisma.econ_SowInventory.create({
|
||
data: item
|
||
});
|
||
newCount++;
|
||
console.log(`新增数据: ${item.month} - ${item.inventory}`);
|
||
}
|
||
} catch (error) {
|
||
errorCount++;
|
||
console.error(`处理数据 ${item.month} 时出错:`, error.message);
|
||
}
|
||
}
|
||
|
||
// 12. 更新数据库缓存
|
||
await writeCache(currentDataHash);
|
||
|
||
// 13. 输出统计信息
|
||
const now = new Date().toISOString();
|
||
console.log('\n' + '='.repeat(50));
|
||
console.log('数据爬取和存储完成!');
|
||
console.log('统计信息:');
|
||
console.log(` - 新增数据: ${newCount} 条`);
|
||
console.log(` - 更新数据: ${updateCount} 条`);
|
||
console.log(` - 跳过数据: ${skippedCount} 条`);
|
||
console.log(` - 错误数量: ${errorCount} 条`);
|
||
console.log(` - 本次更新时间: ${now}`);
|
||
console.log('='.repeat(50));
|
||
|
||
} catch (error) {
|
||
console.error('爬取数据时出错:', error.message);
|
||
throw error;
|
||
} finally {
|
||
// 关闭Prisma连接
|
||
await prisma.$disconnect();
|
||
}
|
||
}
|
||
|
||
// 强制全量更新函数
|
||
async function scrapeSowInventoryFull() {
|
||
console.log('警告: 即将执行全量更新,这将更新所有数据!');
|
||
console.log('如果只是想获取新数据,请使用 scrapeSowInventory() 函数。\n');
|
||
await scrapeSowInventory(false);
|
||
}
|
||
|
||
// 导出函数
|
||
module.exports = {
|
||
scrapeSowInventory,
|
||
scrapeSowInventoryFull
|
||
};
|
||
|
||
// 如果直接运行此文件
|
||
if (require.main === module) {
|
||
// 默认执行增量更新
|
||
// 如果需要强制全量更新,使用: scrapeSowInventoryFull()
|
||
scrapeSowInventory().catch(error => {
|
||
console.error('执行失败:', error);
|
||
process.exit(1);
|
||
});
|
||
}
|