import axios from 'axios'; import * as cheerio from 'cheerio'; import { PrismaClient } from '../../generated/prisma/client.js'; import { PrismaMariaDb } from '@prisma/adapter-mariadb'; import { createPool } from 'mariadb'; // 创建数据库连接池 const pool = createPool({ host: '192.168.111.111', port: 3306, user: 'root', password: 'fullstack', database: 'iboard', connectionLimit: 5 }); // 构造 PrismaClient,提供 adapter const prisma = new PrismaClient({ adapter: new PrismaMariaDb(pool) }); // 浙江数据开放网站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(); // 关闭数据库连接池 await pool.end(); } } // 强制全量更新函数 async function scrapeSowInventoryFull() { console.log('警告: 即将执行全量更新,这将更新所有数据!'); console.log('如果只是想获取新数据,请使用 scrapeSowInventory() 函数。\n'); await scrapeSowInventory(false); } // 导出函数 export { scrapeSowInventory, scrapeSowInventoryFull }; // 如果直接运行此文件 if (import.meta.url === `file://${process.argv[1]}`) { // 默认执行增量更新 // 如果需要强制全量更新,使用: scrapeSowInventoryFull() scrapeSowInventory().catch(error => { console.error('执行失败:', error); process.exit(1); }); }