save
Docker Build and Push / build-image (push) Successful in 3m16s

This commit is contained in:
Cheney
2026-08-03 18:09:08 +08:00
parent 0f74602036
commit 79341bb8cc
25 changed files with 5593 additions and 1409 deletions
+62 -62
View File
@@ -1,62 +1,62 @@
# 数据爬取脚本
本目录包含用于从各种数据源爬取经济数据的脚本。
## 目录结构
- `sow-inventory-scraper.js` - 爬取能繁母猪数量数据
- `template-scraper.js` - 通用爬取脚本模板
## 依赖
- axios - 用于发送HTTP请求
- cheerio - 用于解析HTML内容
- csv-parser - 用于解析CSV格式数据
## 安装依赖
```bash
npm install
```
## 使用方法
1. **修改脚本配置**:
- 打开相应的爬取脚本
- 修改数据源URL和数据提取逻辑
2. **运行爬取脚本**:
```bash
node scripts/scraper/sow-inventory-scraper.js
```
## 注意事项
1. **数据来源合规性**:确保爬取的数据来源符合相关法律法规
2. **爬虫礼仪**:设置合理的请求间隔,避免对目标网站造成压力
3. **数据验证**:爬取后对数据进行验证,确保数据准确性
4. **错误处理**:脚本包含基本的错误处理,但可能需要根据实际情况进行调整
## 扩展指南
1. **添加新的爬取脚本**:
- 复制 `template-scraper.js` 模板
- 修改数据源URL和数据提取逻辑
- 更新数据库存储逻辑
2. **修改数据提取逻辑**:
- 使用Cheerio选择器提取HTML中的数据
- 对于CSV数据,使用csv-parser解析
- 对于JSON数据,直接处理响应数据
3. **数据存储**:
- 脚本会自动将数据存储到数据库
- 会检查数据是否已存在,存在则更新,不存在则创建
## 示例
爬取浙江省能繁母猪数量数据:
```bash
node scripts/scraper/sow-inventory-scraper.js
```
# 数据爬取脚本
本目录包含用于从各种数据源爬取经济数据的脚本。
## 目录结构
- `sow-inventory-scraper.js` - 爬取能繁母猪数量数据
- `template-scraper.js` - 通用爬取脚本模板
## 依赖
- axios - 用于发送HTTP请求
- cheerio - 用于解析HTML内容
- csv-parser - 用于解析CSV格式数据
## 安装依赖
```bash
npm install
```
## 使用方法
1. **修改脚本配置**:
- 打开相应的爬取脚本
- 修改数据源URL和数据提取逻辑
2. **运行爬取脚本**:
```bash
node scripts/scraper/sow-inventory-scraper.js
```
## 注意事项
1. **数据来源合规性**:确保爬取的数据来源符合相关法律法规
2. **爬虫礼仪**:设置合理的请求间隔,避免对目标网站造成压力
3. **数据验证**:爬取后对数据进行验证,确保数据准确性
4. **错误处理**:脚本包含基本的错误处理,但可能需要根据实际情况进行调整
## 扩展指南
1. **添加新的爬取脚本**:
- 复制 `template-scraper.js` 模板
- 修改数据源URL和数据提取逻辑
- 更新数据库存储逻辑
2. **修改数据提取逻辑**:
- 使用Cheerio选择器提取HTML中的数据
- 对于CSV数据,使用csv-parser解析
- 对于JSON数据,直接处理响应数据
3. **数据存储**:
- 脚本会自动将数据存储到数据库
- 会检查数据是否已存在,存在则更新,不存在则创建
## 示例
爬取浙江省能繁母猪数量数据:
```bash
node scripts/scraper/sow-inventory-scraper.js
```
+95 -352
View File
@@ -1,352 +1,95 @@
import axios from 'axios';
import * as cheerio from 'cheerio';
import { createPool } from 'mariadb';
import fs from 'fs';
import path from 'path';
function loadDatabaseConfig() {
const envPath = path.join(process.cwd(), '.env');
if (fs.existsSync(envPath)) {
console.log('读取 .env 文件...');
const envContent = fs.readFileSync(envPath, 'utf-8');
const lines = envContent.split('\n');
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('DATABASE_URL=')) {
const url = trimmedLine.substring('DATABASE_URL='.length).replace(/^"|"$/g, '');
console.log('从 .env 文件获取数据库配置成功!');
console.log(`DATABASE_URL: ${url}`);
// 解析URL
const parts = url.split('://')[1].split('@');
const auth = parts[0].split(':');
const hostAndDb = parts[1].split('/');
const hostAndPort = hostAndDb[0].split(':');
return {
user: auth[0],
password: auth[1],
host: hostAndPort[0],
port: parseInt(hostAndPort[1]),
database: hostAndDb[1]
};
}
}
}
console.log('未找到 .env 文件或 DATABASE_URL 配置,使用默认配置');
return {
host: '192.168.111.111',
port: 3306,
user: 'root',
password: 'fullstack',
database: 'iboard'
};
}
// 创建数据库连接池
const config = loadDatabaseConfig();
console.log('数据库配置:');
console.log(` 主机: ${config.host}`);
console.log(` 端口: ${config.port}`);
console.log(` 用户: ${config.user}`);
console.log(` 数据库: ${config.database}`);
let pool;
async function initPool() {
if (!pool) {
pool = createPool({
host: config.host,
port: config.port,
user: config.user,
password: config.password,
database: config.database,
connectionLimit: 5
});
try {
const conn = await pool.getConnection();
console.log('数据库连接成功!');
conn.release();
} catch (error) {
console.error('数据库连接失败:', error.message);
throw error;
}
}
return 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 {
await initPool();
const result = await pool.query('SELECT * FROM ScraperCache WHERE scraperName = ?', [SCRAPER_NAME]);
return result.length > 0 ? result[0] : { 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 initPool();
await pool.query(
'INSERT INTO ScraperCache (scraperName, lastDataHash, lastUpdateAt) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE lastDataHash = VALUES(lastDataHash), lastUpdateAt = VALUES(lastUpdateAt)',
[SCRAPER_NAME, lastDataHash, 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 {
await initPool();
const result = await pool.query('SELECT MAX(month) as latestMonth FROM Econ_SowInventory');
return result[0].latestMonth || 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 {
await initPool();
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 pool.query('SELECT * FROM Econ_SowInventory WHERE month = ?', [item.month]);
if (existing.length > 0) {
// 检查数据是否真的需要更新(值变化了才更新)
if (existing[0].inventory !== item.inventory) {
await pool.query(
'UPDATE Econ_SowInventory SET inventory = ? WHERE month = ?',
[item.inventory, item.month]
);
updateCount++;
console.log(`更新数据: ${item.month} - ${item.inventory} (原值: ${existing[0].inventory})`);
} else {
console.log(`数据未变化,跳过: ${item.month}`);
}
} else {
await pool.query(
'INSERT INTO Econ_SowInventory (month, inventory) VALUES (?, ?)',
[item.month, item.inventory]
);
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 {
// 关闭数据库连接池
if (pool) {
await pool.end();
console.log('数据库连接池已关闭');
}
}
}
// 强制全量更新函数
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);
});
}
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
const { PrismaClient } = require('../../generated/prisma');
const prisma = new PrismaClient();
// 浙江数据开放网站URL(示例,实际需要根据真实网站修改)
const ZHEJIANG_OPEN_DATA_URL = 'https://data.zj.gov.cn/';
// 能繁母猪数量数据爬取函数
async function scrapeSowInventory() {
try {
console.log('开始爬取浙江省能繁母猪数量数据...');
// 1. 发送HTTP请求获取页面内容
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 data = [];
$('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)) {
data.push({
month,
inventory
});
}
}
}
});
});
console.log(`爬取到 ${data.length} 条数据`);
// 4. 存储数据到数据库
for (const item of data) {
try {
// 检查数据是否已存在
const existing = await prisma.econ_SowInventory.findUnique({
where: { month: item.month }
});
if (existing) {
// 更新现有数据
await prisma.econ_SowInventory.update({
where: { month: item.month },
data: { inventory: item.inventory }
});
console.log(`更新数据: ${item.month} - ${item.inventory}`);
} else {
// 创建新数据
await prisma.econ_SowInventory.create({
data: item
});
console.log(`新增数据: ${item.month} - ${item.inventory}`);
}
} catch (error) {
console.error(`处理数据 ${item.month} 时出错:`, error.message);
}
}
console.log('数据爬取和存储完成!');
} catch (error) {
console.error('爬取数据时出错:', error.message);
} finally {
// 关闭Prisma连接
await prisma.$disconnect();
}
}
// 导出函数
module.exports = { scrapeSowInventory };
// 如果直接运行此文件
if (require.main === module) {
scrapeSowInventory();
}
+100 -100
View File
@@ -1,100 +1,100 @@
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
const { PrismaClient } = require('../../generated/prisma');
const prisma = new PrismaClient();
// 数据源URL(需要根据实际情况修改)
const DATA_SOURCE_URL = 'https://example.com/data';
// 通用数据爬取函数
async function scrapeData() {
try {
console.log('开始爬取数据...');
// 1. 发送HTTP请求获取页面内容
const response = await axios.get(DATA_SOURCE_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 data = [];
// 示例:从表格中提取数据
$('table').each((tableIndex, table) => {
$(table).find('tr').each((rowIndex, row) => {
if (rowIndex > 0) { // 跳过表头
const cells = $(row).find('td');
// 根据实际表格结构提取数据
// 示例:假设第一列是日期,第二列是数值
if (cells.length >= 2) {
const date = $(cells[0]).text().trim();
const value = parseFloat($(cells[1]).text().trim());
if (date && !isNaN(value)) {
data.push({
date,
value
});
}
}
}
});
});
console.log(`爬取到 ${data.length} 条数据`);
// 4. 存储数据到数据库(需要根据实际表结构修改)
for (const item of data) {
try {
// 这里需要根据实际表结构修改
// 示例:存储到某个表
/*
const existing = await prisma.someTable.findUnique({
where: { date: item.date }
});
if (existing) {
// 更新现有数据
await prisma.someTable.update({
where: { date: item.date },
data: { value: item.value }
});
console.log(`更新数据: ${item.date} - ${item.value}`);
} else {
// 创建新数据
await prisma.someTable.create({
data: item
});
console.log(`新增数据: ${item.date} - ${item.value}`);
}
*/
} catch (error) {
console.error(`处理数据时出错:`, error.message);
}
}
console.log('数据爬取和存储完成!');
} catch (error) {
console.error('爬取数据时出错:', error.message);
} finally {
// 关闭Prisma连接
await prisma.$disconnect();
}
}
// 导出函数
module.exports = { scrapeData };
// 如果直接运行此文件
if (require.main === module) {
scrapeData();
}
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
const { PrismaClient } = require('../../generated/prisma');
const prisma = new PrismaClient();
// 数据源URL(需要根据实际情况修改)
const DATA_SOURCE_URL = 'https://example.com/data';
// 通用数据爬取函数
async function scrapeData() {
try {
console.log('开始爬取数据...');
// 1. 发送HTTP请求获取页面内容
const response = await axios.get(DATA_SOURCE_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 data = [];
// 示例:从表格中提取数据
$('table').each((tableIndex, table) => {
$(table).find('tr').each((rowIndex, row) => {
if (rowIndex > 0) { // 跳过表头
const cells = $(row).find('td');
// 根据实际表格结构提取数据
// 示例:假设第一列是日期,第二列是数值
if (cells.length >= 2) {
const date = $(cells[0]).text().trim();
const value = parseFloat($(cells[1]).text().trim());
if (date && !isNaN(value)) {
data.push({
date,
value
});
}
}
}
});
});
console.log(`爬取到 ${data.length} 条数据`);
// 4. 存储数据到数据库(需要根据实际表结构修改)
for (const item of data) {
try {
// 这里需要根据实际表结构修改
// 示例:存储到某个表
/*
const existing = await prisma.someTable.findUnique({
where: { date: item.date }
});
if (existing) {
// 更新现有数据
await prisma.someTable.update({
where: { date: item.date },
data: { value: item.value }
});
console.log(`更新数据: ${item.date} - ${item.value}`);
} else {
// 创建新数据
await prisma.someTable.create({
data: item
});
console.log(`新增数据: ${item.date} - ${item.value}`);
}
*/
} catch (error) {
console.error(`处理数据时出错:`, error.message);
}
}
console.log('数据爬取和存储完成!');
} catch (error) {
console.error('爬取数据时出错:', error.message);
} finally {
// 关闭Prisma连接
await prisma.$disconnect();
}
}
// 导出函数
module.exports = { scrapeData };
// 如果直接运行此文件
if (require.main === module) {
scrapeData();
}