/** * 批量下载模型文件并解析数据脚本 * 如果需要生成风场数,需: * justDownloadModel 与 justTrDataMode 改为 false * drgree 改为 gfs_1p00 后重启脚本 * 待数据解析完成后 basePath 内的 wind 文件夹内就是需要的数据,及时备份好 * 如需生成温度图等数据,需: * 先将 drgree 改为 gfs_0p25, justDownloadModel 改为 true 后运行脚本等待下载完成 * 下载完成后,再将 justTrDataMode 改为 true 后运行脚本 * 数据解析完成后 basePath 内会生成相关的图源数据,及时备份好 */ const express = require("express"); //Node.js的一个Web框架,可以用来快速构建Web应用程序 const moment = require("moment"); //JavaScript日期处理库,可以用来格式化和解析日期。 const http = require('http'); //Node.js的一个内置模块,可以用来创建HTTP服务器和客户端。 const request = require('request'); //Node.js的HTTP客户端库,可以用来发送HTTP请求和处理响应。 const fs = require('fs'); //Node.js的一个内置模块,可以用来读写文件和目录。 const Q = require('q'); //JavaScript的Promise库,可以用来处理异步操作。 const cors = require('cors'); //Node.js的中间件,可以用来处理跨域请求。 const { exec, execSync } = require('child_process'); const dayjs = require('dayjs'); const path = require('path'); const { createCloudImage } = require("./schema/createCloudImage_old.js"); // 解析出的数据所存储的根目录 // const basePath = path.join(__dirname, `./exportData`) const basePath = path.join("F:", `./exportData`) // 获取的模型文件名称 const fileName = "f000" // 获取模型文件时间步进 const dateStep = 6; // 获取模型文件的精度 // const drgree = "gfs_0p25"; const drgree = "gfs_1p00"; // 仅下载模式 const justDownloadModel = false; // 仅转换数据模式 const justTrDataMode = false; // 模型下载根地址 const baseDir = `http://nomads.ncep.noaa.gov/cgi-bin/filter_${drgree}.pl`; const windDirLayer = []; const modelDirLayer = []; function run(targetMoment) { if (justTrDataMode) { trModelData(); console.log("所有转换数据转换工作已完成"); } else { getGribData(targetMoment).then((response) => { if (response.stamp) { if (!justDownloadModel) { convertGribToJson(response.stamp, response.targetMoment); } else { run(moment(targetMoment).subtract(dateStep, 'hours')); } } }); } } async function trModelData() { const fileList = JSON.parse(fs.readFileSync(`${basePath}/model/layer.json`, "utf8")); let mixData = [{ lv: "500 mb", // 层级 vr: "TCDC", // 变量 type: "cloud", // 渲染图片类型 dir: "cloud", // 导出文件夹名称 rgb: { r: 255, g: 255, b: 255 }, // 渲染颜色 typeName: "云层图" // layer.json文件内描述字段 }, { lv: "50 mb", vr: "RWMR", type: "rain", dir: "rain", rgb: { r: 64, g: 158, b: 255 }, typeName: "降雨图" }, { lv: "2 m above ground", vr: "TMP", type: "tmp", dir: "tmp", rgb: { r: 0, g: 0, b: 0 }, typeName: "温度图" }]; // [{ // // 数据观测时间戳,可用此字段排序数据,值越大日期越靠后 // "sort": 1757721600000, // // 数据观测时间格式化 // "date": "2025-09-13 08:00", // // 此条数据对应的文件地址 // "path": "/exportData/cloud/20250913/08.png", // // 当前取值的模型精度 // "drgree": "gfs_0p25", // // 当前映射的中文描述 // "description": "2025-09-13 08:00云层图片" // }] for (let j = 0; j < mixData.length; j++) { const { lv, vr, type, dir, rgb, typeName } = mixData[j]; let dirLayer = []; for (let i = 0; i < fileList.length; i++) { console.log(`[[${type}:${lv}:${vr}]] 正在提取第 ${i + 1}/${fileList.length} 个模型`); const date = dayjs(fileList[i].date).format("YYYYMMDD"); const hour = dayjs(fileList[i].date).format("HH"); // 云层图 try { const txtPath = `${mkDir(basePath + "/" + dir + "/" + date + "/")}${hour}.txt`; const command = `"${path.join(__dirname, "./wgrib2/wgrib2.exe")}" "${basePath + fileList[i].path.replace(/\/exportData/, "")}" -match "${vr}:${lv}" -text ${txtPath}`; execSync(command); await createCloudImage(txtPath, `${basePath}/${dir}/${date}/${hour}.png`, rgb, type); dirLayer.push({ sort: fileList[i].sort, date: fileList[i].date, path: `/exportData/${type}/${date}/${hour}.png`, drgree, description: `${fileList[i].date}${typeName}` }); execSync(`rm -rf ${txtPath}`); } catch (e) { console.log(`模型 "${fileList[i].path}" 数据类型 ${type} 生成失败...或许是因为所选层级与变量导出txt无内容`) } // 降雨图 // const command = `"${path.join(__dirname, "../../wgrib2/wgrib2.exe")}" "${fileList.list[0]}" -match "RWMR:50 mb" -text ${tempTxtFilePath}`; // stdout = execSync(command); // await createCloudImage(tempTxtFilePath, path.join(__dirname, `../../tempDir/rain.png`), { r: 64, g: 158, b: 255 }, "rain"); // // 降雨图 // const command = `"${path.join(__dirname, "../../wgrib2/wgrib2.exe")}" "${fileList.list[0]}" -match "TMP:2 m above ground" -text ${tempTxtFilePath}`; // stdout = execSync(command); // await createCloudImage(tempTxtFilePath, path.join(__dirname, `../../tempDir/tmp.png`), { r: 0, g: 0, b: 0 }, "tmp"); } fs.writeFileSync(`${mkDir(basePath + "/" + type)}/layer.json`, JSON.stringify(dirLayer), 'utf8'); } } function getGribData(targetMoment) { var deferred = Q.defer(); function runQuery(targetMoment) { // only go 2 weeks deep if (moment.utc().diff(targetMoment, 'days') > 30) { // 写入文件 fs.writeFileSync(`${mkDir(basePath + "/model")}/layer.json`, JSON.stringify(modelDirLayer), 'utf8'); if (!justDownloadModel) { fs.writeFileSync(`${mkDir(basePath + "/wind")}/layer.json`, JSON.stringify(windDirLayer), 'utf8'); } console.log('命中极限,收获完成或数据存在较大缺口。'); return; } var stamp = moment(targetMoment).format('YYYYMMDD') + roundHours(moment(targetMoment).hour(), dateStep); var years = moment(targetMoment).format('YYYYMMDD') var hour = roundHours(moment(targetMoment).hour(), dateStep); let requestQs = null; if (justDownloadModel) { requestQs = { file: `gfs.t${roundHours(moment(targetMoment).hour(), dateStep)}z.pgrb2.${drgree.split("_")[1]}.${fileName}`,//指定需要获取的气象预报数据文件的文件名。在这里,文件名是由多个参数组成的字符串,包括GFS模型的起报时间、预报时效、数据格式等信息。 all_lev: "on", all_var: "on", leftlon: 0,//从0度经线开始 获取数据 rightlon: 360,//到360度经线结束 获取数据 toplat: 90, //到90度北纬结束 获取数据 bottomlat: -90, //到90度南纬结束 获取数据 dir: '/gfs.' + years + '/' + hour + '/atmos' //dir=%2Fgfs.20231127 %2F(转义:/) stamp:时间戳 } } else { requestQs = { file: `gfs.t${roundHours(moment(targetMoment).hour(), dateStep)}z.pgrb2.${drgree.split("_")[1]}.${fileName}`,//指定需要获取的气象预报数据文件的文件名。在这里,文件名是由多个参数组成的字符串,包括GFS模型的起报时间、预报时效、数据格式等信息。 lev_10_m_above_ground: 'on', //指定是否获取地面以上10米高度层的气象变量数据。这里设置为'on',表示需要获取该高度层的数据。 lev_surface: 'on', //指定是否获取地面高度层的气象变量数据。这里设置为'on',表示需要获取该高度层的数据 var_TMP: 'on', //指定是否获取温度(Temperature)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据 var_UGRD: 'on', //指定是否获取东西向风速(Eastward Wind)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据 var_VGRD: 'on', //指定是否获取南北向风速(Northward Wind)气象变量的数据。这里设置为'on',表示需要获取该气象变量的数据 leftlon: 0,//从0度经线开始 获取数据 rightlon: 360,//到360度经线结束 获取数据 toplat: 90, //到90度北纬结束 获取数据 bottomlat: -90, //到90度南纬结束 获取数据 dir: '/gfs.' + years + '/' + hour + '/atmos' //dir=%2Fgfs.20231127 %2F(转义:/) stamp:时间戳 } } request.get({ url: baseDir, qs: requestQs }).on('error', function (err) { // console.log(err); runQuery(moment(targetMoment).subtract(dateStep, 'hours')); }).on('response', function (response) { console.log("🚀 ~ name:response", response.request.url.href) console.log('响应状态:' + response.statusCode + ' 时间节点: ' + stamp); if (response.statusCode != 200) { runQuery(moment(targetMoment).subtract(dateStep, 'hours')); } else { const windDir = mkDir(`${basePath}/wind/${stamp.substring(0, 8)}`); const modelDir = mkDir(`${basePath}/model/${stamp.substring(0, 8)}`); if (justDownloadModel) { var file = fs.createWriteStream(`${modelDir}/${stamp.substring(stamp.length - 2)}.${fileName}`); modelDirLayer.push({ sort: dayjs(stamp).add(8, 'hour').valueOf(), date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"), path: `/exportData/model/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.${fileName}`, drgree, description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}气象模型文件` }) response.pipe(file); file.on('finish', function () { file.close(); deferred.resolve({ stamp: stamp, targetMoment: targetMoment }); }); } else { // don't rewrite stamps if (!checkPath(`${windDir}/${stamp.substring(stamp.length - 2)}.json`, false)) { console.log('piping ' + stamp); // mk sure we've got somewhere to put output // checkPath('grib-data', true); // pipe the file, resolve the valid time stamp var file = fs.createWriteStream(`${modelDir}/${stamp.substring(stamp.length - 2)}.${fileName}`); modelDirLayer.push({ sort: dayjs(stamp).add(8, 'hour').valueOf(), date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"), path: `/exportData/model/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.${fileName}`, drgree, description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}气象模型文件` }) response.pipe(file); file.on('finish', function () { file.close(); deferred.resolve({ stamp: stamp, targetMoment: targetMoment }); }); } else { console.log('already have ' + stamp + ', not looking further'); deferred.resolve({ stamp: false, targetMoment: false }); } } } }); } runQuery(targetMoment); return deferred.promise; } function convertGribToJson(stamp, targetMoment) { // mk sure we've got somewhere to put output // checkPath('json-data', true); const windDir = mkDir(`${basePath}/wind/${stamp.substring(0, 8)}`); const modelDir = mkDir(`${basePath}/model/${stamp.substring(0, 8)}`); const result = exec(`java\\bin\\grib2json --data --output ${windDir}\\${stamp.substring(stamp.length - 2)}.json --names --compact ${modelDir}\\${stamp.substring(stamp.length - 2)}.${fileName}`, { maxBuffer: 200 * 500 * 1024 }, function (error, stdout, stderr) { if (stdout.error) { console.log('exec error: ' + error); } else { console.log("converted.."); // don't keep raw grib data // exec('rm grib-data/*'); windDirLayer.push({ sort: dayjs(stamp).add(8, 'hour').valueOf(), date: dayjs(stamp).add(8, 'hour').format("YYYY-MM-DD HH:mm"), path: `/exportData/wind/${stamp.substring(0, 8)}/${stamp.substring(stamp.length - 2)}.json`, drgree, description: `${dayjs(`${stamp}`).add(8, 'hour').format("YYYY-MM-DD HH:mm")}风场图数据Json` }) // if we don't have older stamp, try and harvest one var prevMoment = moment(targetMoment).subtract(dateStep, 'hours'); var prevStamp = prevMoment.format('YYYYMMDD') + roundHours(prevMoment.hour(), dateStep); if (!checkPath(`${basePath + "/wind/" + prevStamp.substring(0, 8)}/${prevStamp.substring(prevStamp.length - 2)}.json`, false)) { console.log("attempting to harvest older data " + stamp); run(prevMoment); } else { console.log('got older, no need to harvest further'); } } } ); } function checkPath(path, mkdir) { try { fs.statSync(path); return true; } catch (e) { if (mkdir) { fs.mkdirSync(path); } return false; } } function roundHours(hours, interval) { if (interval > 0) { var result = (Math.floor(hours / interval) * interval); return result < 10 ? '0' + result.toString() : result; } } // utc 时间转中国时间 function convertUtcDateForChinaDate(date) { return dayjs(date).add(8, 'hour').format("YYYYMMDDHHmm"); } function mkDir(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } return dirPath; } run(moment.utc());