【cloudflare-workder】0-1.使用worker生成一条Vless协议链接

一、前言

(一)浏览前注意事项

vless协议链接是干什么用的,懂的再浏览下面的内容。

(二)前置条件

需要满足一下两个条件

  • 需要注册cloudflare账户
  • 需要一个托管在cloudflare的域名
  • 需要V2客户端(windows端)

二、详细步骤

(一)获取worker的代码

// <!--GAMFC-->version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC<!--GAMFC-END-->.
// @ts-ignore
import { connect } from 'cloudflare:sockets';

// How to generate your own UUID:
// [Windows] Press "Win + R", input cmd and run:  Powershell -NoExit -Command "[guid]::NewGuid()"
let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4';

let proxyIP = '';


if (!isValidUUID(userID)) {
	throw new Error('uuid is not valid');
}

export default {
	/**
	 * @param {import("@cloudflare/workers-types").Request} request
	 * @param {{UUID: string, PROXYIP: string}} env
	 * @param {import("@cloudflare/workers-types").ExecutionContext} ctx
	 * @returns {Promise<Response>}
	 */
	async fetch(request, env, ctx) {
		try {
			userID = env.UUID || userID;
			proxyIP = env.PROXYIP || proxyIP;
			const upgradeHeader = request.headers.get('Upgrade');
			if (!upgradeHeader || upgradeHeader !== 'websocket') {
				const url = new URL(request.url);
				switch (url.pathname) {
					case '/':
						return new Response(JSON.stringify(request.cf), { status: 200 });
					case `/${userID}`: {
						const vlessConfig = getVLESSConfig(userID, request.headers.get('Host'));
						return new Response(`${vlessConfig}`, {
							status: 200,
							headers: {
								"Content-Type": "text/plain;charset=utf-8",
							}
						});
					}
					default:
						return new Response('Not found', { status: 404 });
				}
			} else {
				return await vlessOverWSHandler(request);
			}
		} catch (err) {
			/** @type {Error} */ let e = err;
			return new Response(e.toString());
		}
	},
};




/**
 * 
 * @param {import("@cloudflare/workers-types").Request} request
 */
async function vlessOverWSHandler(request) {

	/** @type {import("@cloudflare/workers-types").WebSocket[]} */
	// @ts-ignore
	const webSocketPair = new WebSocketPair();
	const [client, webSocket] = Object.values(webSocketPair);

	webSocket.accept();

	let address = '';
	let portWithRandomLog = '';
	const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {
		console.log(`[${address}:${portWithRandomLog}] ${info}`, event || '');
	};
	const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';

	const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);

	/** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/
	let remoteSocketWapper = {
		value: null,
	};
	let udpStreamWrite = null;
	let isDns = false;

	// ws --> remote
	readableWebSocketStream.pipeTo(new WritableStream({
		async write(chunk, controller) {
			if (isDns && udpStreamWrite) {
				return udpStreamWrite(chunk);
			}
			if (remoteSocketWapper.value) {
				const writer = remoteSocketWapper.value.writable.getWriter()
				await writer.write(chunk);
				writer.releaseLock();
				return;
			}

			const {
				hasError,
				message,
				portRemote = 443,
				addressRemote = '',
				rawDataIndex,
				vlessVersion = new Uint8Array([0, 0]),
				isUDP,
			} = processVlessHeader(chunk, userID);
			address = addressRemote;
			portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp '
				} `;
			if (hasError) {
				// controller.error(message);
				throw new Error(message); // cf seems has bug, controller.error will not end stream
				// webSocket.close(1000, message);
				return;
			}
			// if UDP but port not DNS port, close it
			if (isUDP) {
				if (portRemote === 53) {
					isDns = true;
				} else {
					// controller.error('UDP proxy only enable for DNS which is port 53');
					throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream
					return;
				}
			}
			// ["version", "闄勫姞淇℃伅闀垮害 N"]
			const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]);
			const rawClientData = chunk.slice(rawDataIndex);

			// TODO: support udp here when cf runtime has udp support
			if (isDns) {
				const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log);
				udpStreamWrite = write;
				udpStreamWrite(rawClientData);
				return;
			}
			handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log);
		},
		close() {
			log(`readableWebSocketStream is close`);
		},
		abort(reason) {
			log(`readableWebSocketStream is abort`, JSON.stringify(reason));
		},
	})).catch((err) => {
		log('readableWebSocketStream pipeTo error', err);
	});

	return new Response(null, {
		status: 101,
		// @ts-ignore
		webSocket: client,
	});
}

/**
 * Handles outbound TCP connections.
 *
 * @param {any} remoteSocket 
 * @param {string} addressRemote The remote address to connect to.
 * @param {number} portRemote The remote port to connect to.
 * @param {Uint8Array} rawClientData The raw client data to write.
 * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.
 * @param {Uint8Array} vlessResponseHeader The VLESS response header.
 * @param {function} log The logging function.
 * @returns {Promise<void>} The remote socket.
 */
async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) {
	async function connectAndWrite(address, port) {
		/** @type {import("@cloudflare/workers-types").Socket} */
		const tcpSocket = connect({
			hostname: address,
			port: port,
		});
		remoteSocket.value = tcpSocket;
		log(`connected to ${address}:${port}`);
		const writer = tcpSocket.writable.getWriter();
		await writer.write(rawClientData); // first write, nomal is tls client hello
		writer.releaseLock();
		return tcpSocket;
	}

	// if the cf connect tcp socket have no incoming data, we retry to redirect ip
	async function retry() {
		const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote)
		// no matter retry success or not, close websocket
		tcpSocket.closed.catch(error => {
			console.log('retry tcpSocket closed error', error);
		}).finally(() => {
			safeCloseWebSocket(webSocket);
		})
		remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log);
	}

	const tcpSocket = await connectAndWrite(addressRemote, portRemote);

	// when remoteSocket is ready, pass to websocket
	// remote--> ws
	remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log);
}

/**
 * 
 * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer
 * @param {string} earlyDataHeader for ws 0rtt
 * @param {(info: string)=> void} log for ws 0rtt
 */
function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
	let readableStreamCancel = false;
	const stream = new ReadableStream({
		start(controller) {
			webSocketServer.addEventListener('message', (event) => {
				if (readableStreamCancel) {
					return;
				}
				const message = event.data;
				controller.enqueue(message);
			});

			// The event means that the client closed the client -> server stream.
			// However, the server -> client stream is still open until you call close() on the server side.
			// The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket.
			webSocketServer.addEventListener('close', () => {
				// client send close, need close server
				// if stream is cancel, skip controller.close
				safeCloseWebSocket(webSocketServer);
				if (readableStreamCancel) {
					return;
				}
				controller.close();
			}
			);
			webSocketServer.addEventListener('error', (err) => {
				log('webSocketServer has error');
				controller.error(err);
			}
			);
			// for ws 0rtt
			const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);
			if (error) {
				controller.error(error);
			} else if (earlyData) {
				controller.enqueue(earlyData);
			}
		},

		pull(controller) {
			// if ws can stop read if stream is full, we can implement backpressure
			// https://streams.spec.whatwg.org/#example-rs-push-backpressure
		},
		cancel(reason) {
			// 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here
			// 2. if readableStream is cancel, all controller.close/enqueue need skip,
			// 3. but from testing controller.error still work even if readableStream is cancel
			if (readableStreamCancel) {
				return;
			}
			log(`ReadableStream was canceled, due to ${reason}`)
			readableStreamCancel = true;
			safeCloseWebSocket(webSocketServer);
		}
	});

	return stream;

}

// https://xtls.github.io/development/protocols/vless.html
// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw

/**
 * 
 * @param { ArrayBuffer} vlessBuffer 
 * @param {string} userID 
 * @returns 
 */
function processVlessHeader(
	vlessBuffer,
	userID
) {
	if (vlessBuffer.byteLength < 24) {
		return {
			hasError: true,
			message: 'invalid data',
		};
	}
	const version = new Uint8Array(vlessBuffer.slice(0, 1));
	let isValidUser = false;
	let isUDP = false;
	if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) {
		isValidUser = true;
	}
	if (!isValidUser) {
		return {
			hasError: true,
			message: 'invalid user',
		};
	}

	const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0];
	//skip opt for now

	const command = new Uint8Array(
		vlessBuffer.slice(18 + optLength, 18 + optLength + 1)
	)[0];

	// 0x01 TCP
	// 0x02 UDP
	// 0x03 MUX
	if (command === 1) {
	} else if (command === 2) {
		isUDP = true;
	} else {
		return {
			hasError: true,
			message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,
		};
	}
	const portIndex = 18 + optLength + 1;
	const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2);
	// port is big-Endian in raw data etc 80 == 0x005d
	const portRemote = new DataView(portBuffer).getUint16(0);

	let addressIndex = portIndex + 2;
	const addressBuffer = new Uint8Array(
		vlessBuffer.slice(addressIndex, addressIndex + 1)
	);

	// 1--> ipv4  addressLength =4
	// 2--> domain name addressLength=addressBuffer[1]
	// 3--> ipv6  addressLength =16
	const addressType = addressBuffer[0];
	let addressLength = 0;
	let addressValueIndex = addressIndex + 1;
	let addressValue = '';
	switch (addressType) {
		case 1:
			addressLength = 4;
			addressValue = new Uint8Array(
				vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
			).join('.');
			break;
		case 2:
			addressLength = new Uint8Array(
				vlessBuffer.slice(addressValueIndex, addressValueIndex + 1)
			)[0];
			addressValueIndex += 1;
			addressValue = new TextDecoder().decode(
				vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
			);
			break;
		case 3:
			addressLength = 16;
			const dataView = new DataView(
				vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
			);
			// 2001:0db8:85a3:0000:0000:8a2e:0370:7334
			const ipv6 = [];
			for (let i = 0; i < 8; i++) {
				ipv6.push(dataView.getUint16(i * 2).toString(16));
			}
			addressValue = ipv6.join(':');
			// seems no need add [] for ipv6
			break;
		default:
			return {
				hasError: true,
				message: `invild  addressType is ${addressType}`,
			};
	}
	if (!addressValue) {
		return {
			hasError: true,
			message: `addressValue is empty, addressType is ${addressType}`,
		};
	}

	return {
		hasError: false,
		addressRemote: addressValue,
		addressType,
		portRemote,
		rawDataIndex: addressValueIndex + addressLength,
		vlessVersion: version,
		isUDP,
	};
}


/**
 * 
 * @param {import("@cloudflare/workers-types").Socket} remoteSocket 
 * @param {import("@cloudflare/workers-types").WebSocket} webSocket 
 * @param {ArrayBuffer} vlessResponseHeader 
 * @param {(() => Promise<void>) | null} retry
 * @param {*} log 
 */
async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) {
	// remote--> ws
	let remoteChunkCount = 0;
	let chunks = [];
	/** @type {ArrayBuffer | null} */
	let vlessHeader = vlessResponseHeader;
	let hasIncomingData = false; // check if remoteSocket has incoming data
	await remoteSocket.readable
		.pipeTo(
			new WritableStream({
				start() {
				},
				/**
				 * 
				 * @param {Uint8Array} chunk 
				 * @param {*} controller 
				 */
				async write(chunk, controller) {
					hasIncomingData = true;
					// remoteChunkCount++;
					if (webSocket.readyState !== WS_READY_STATE_OPEN) {
						controller.error(
							'webSocket.readyState is not open, maybe close'
						);
					}
					if (vlessHeader) {
						webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());
						vlessHeader = null;
					} else {
						// seems no need rate limit this, CF seems fix this??..
						// if (remoteChunkCount > 20000) {
						// 	// cf one package is 4096 byte(4kb),  4096 * 20000 = 80M
						// 	await delay(1);
						// }
						webSocket.send(chunk);
					}
				},
				close() {
					log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);
					// safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway.
				},
				abort(reason) {
					console.error(`remoteConnection!.readable abort`, reason);
				},
			})
		)
		.catch((error) => {
			console.error(
				`remoteSocketToWS has exception `,
				error.stack || error
			);
			safeCloseWebSocket(webSocket);
		});

	// seems is cf connect socket have error,
	// 1. Socket.closed will have error
	// 2. Socket.readable will be close without any data coming
	if (hasIncomingData === false && retry) {
		log(`retry`)
		retry();
	}
}

/**
 * 
 * @param {string} base64Str 
 * @returns 
 */
function base64ToArrayBuffer(base64Str) {
	if (!base64Str) {
		return { error: null };
	}
	try {
		// go use modified Base64 for URL rfc4648 which js atob not support
		base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
		const decode = atob(base64Str);
		const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
		return { earlyData: arryBuffer.buffer, error: null };
	} catch (error) {
		return { error };
	}
}

/**
 * This is not real UUID validation
 * @param {string} uuid 
 */
function isValidUUID(uuid) {
	const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
	return uuidRegex.test(uuid);
}

const WS_READY_STATE_OPEN = 1;
const WS_READY_STATE_CLOSING = 2;
/**
 * Normally, WebSocket will not has exceptions when close.
 * @param {import("@cloudflare/workers-types").WebSocket} socket
 */
function safeCloseWebSocket(socket) {
	try {
		if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
			socket.close();
		}
	} catch (error) {
		console.error('safeCloseWebSocket error', error);
	}
}

const byteToHex = [];
for (let i = 0; i < 256; ++i) {
	byteToHex.push((i + 256).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
	return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
}
function stringify(arr, offset = 0) {
	const uuid = unsafeStringify(arr, offset);
	if (!isValidUUID(uuid)) {
		throw TypeError("Stringified UUID is invalid");
	}
	return uuid;
}


/**
 * 
 * @param {import("@cloudflare/workers-types").WebSocket} webSocket 
 * @param {ArrayBuffer} vlessResponseHeader 
 * @param {(string)=> void} log 
 */
async function handleUDPOutBound(webSocket, vlessResponseHeader, log) {

	let isVlessHeaderSent = false;
	const transformStream = new TransformStream({
		start(controller) {

		},
		transform(chunk, controller) {
			// udp message 2 byte is the the length of udp data
			// TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message
			for (let index = 0; index < chunk.byteLength;) {
				const lengthBuffer = chunk.slice(index, index + 2);
				const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);
				const udpData = new Uint8Array(
					chunk.slice(index + 2, index + 2 + udpPakcetLength)
				);
				index = index + 2 + udpPakcetLength;
				controller.enqueue(udpData);
			}
		},
		flush(controller) {
		}
	});

	// only handle dns udp for now
	transformStream.readable.pipeTo(new WritableStream({
		async write(chunk) {
			const resp = await fetch('https://1.1.1.1/dns-query',
				{
					method: 'POST',
					headers: {
						'content-type': 'application/dns-message',
					},
					body: chunk,
				})
			const dnsQueryResult = await resp.arrayBuffer();
			const udpSize = dnsQueryResult.byteLength;
			// console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));
			const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);
			if (webSocket.readyState === WS_READY_STATE_OPEN) {
				log(`doh success and dns message length is ${udpSize}`);
				if (isVlessHeaderSent) {
					webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());
				} else {
					webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());
					isVlessHeaderSent = true;
				}
			}
		}
	})).catch((error) => {
		log('dns udp has error' + error)
	});

	const writer = transformStream.writable.getWriter();

	return {
		/**
		 * 
		 * @param {Uint8Array} chunk 
		 */
		write(chunk) {
			writer.write(chunk);
		}
	};
}

/**
 * 
 * @param {string} userID 
 * @param {string | null} hostName
 * @returns {string}
 */
function getVLESSConfig(userID, hostName) {
	const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`
	return `
################################################################
v2ray
---------------------------------------------------------------
${vlessMain}
---------------------------------------------------------------
################################################################
clash-meta
---------------------------------------------------------------
- type: vless
  name: ${hostName}
  server: ${hostName}
  port: 443
  uuid: ${userID}
  network: ws
  tls: true
  udp: false
  sni: ${hostName}
  client-fingerprint: chrome
  ws-opts:
    path: "/?ed=2048"
    headers:
      host: ${hostName}
---------------------------------------------------------------
################################################################
`;
}

(二)按步骤配置cloudflare的worker(熟悉worker用法的可以不用看,将代码复制进去就行)

1.在cloudflare的“Workers 和 Pages”菜单中“创建应用程序”;

2.创建worker;

3.名称可以保持默认,也可以修改为任意名称(英文);

4.点击“部署”按钮;

5.点击“配置worker”;

6.点击“编辑代码”;

7.将上面获取的代码替换掉worker中的原来的代码;

8.打开V2ray客户端—服务器—添加[VLESS]服务器;

9.点击“用户ID(id)”后面的“生成”按钮,生成一个类似于“86797703-523c-4325-8fd8-ea3fc228038f”的字符串;

10.复制该字符串,替换掉worker中的第7行中的userID:

let userID = '86797703-523c-4325-8fd8-ea3fc228038f';

11.将第9行的“let proxyIP = ”修改为下面cdn加速中的任意一个,例如:

let proxyIP = 'cdn.anycast.eu.org';

cdn加速

cdn-all.xn--b6gac.eu.org
cdn.xn--b6gac.eu.org 
cdn-b100.xn--b6gac.eu.org 
edgetunnel.anycast.eu.org 
cdn.anycast.eu.org (亚洲地区)

12.点击右上角的“保存并部署”;

13.退出编辑模式,回到worker的主页,点击预览中的“Custom Domains”后面的“查看”;

14.点击“添加自定义域”,假如你在该cloudflare账号托管的一个域名是“abc.com”,则你可以添加一个二级自定义域名例如“link.abc.com”;

15.当你添加的自定义域的证书下面显示绿色的“有效”的时候,你就可以通过“link.abc.com”这个域名访问上面的json网页了!

16.在浏览器输入“link.abc.com\<第10步的userID>”,你就可以获得VLESS的链接了!

17.复制该VLESS链接,就可以添加VLESS服务器到V2客户端

然后你就得到一个免费的vless链接啦。

注意:在访问一些页面的时候可能会提示证书问题,这些就要换下第11步中cdn加速的地址了

最后编辑于2023/11/15

<think>好的,我现在需要帮助用户解决他们关于使用Python处理气象数据的问题。用户希望读取NCEP1的500hPa月平均高度场数据(nc格式)和中国160站的气温数据(txt格式),计算1951-20101月份的EU遥相关指数及其标准化值,生成时间序列图,并分析该指数与高度场及气温的空间相关系数分布,最后绘制地图图形。 首先,我需要确认用户提到的任务的具体步骤。EU遥相关指数的计算通常需要定义关键区域的高度场异常,然后通过标准化处理得到指数。用户可能需要先了解EU指数的定义,比如参考文献中的方法。根据我的知识,EU指数可能涉及欧洲和北大西洋地区的几个关键点的高度场差异,但具体的计算方式可能需要查阅相关文献或者标准方法。用户提供的引用中并没有直接提到EU指数的计算方法,所以可能需要依赖外部资料或者假设一个通用的计算方法,比如区域平均的标准化异常差值。 接下来,处理数据部分。用户需要读取nc格式的高度场数据和txt格式的气温数据。对于nc文件,Python中常用的库是xarray,结合netCDF4库。而txt格式的气温数据可能需要使用pandas来读取,尤其是处理表格数据。用户提供的引用[4]中提到了使用xarray打开NetCDF文件,这可以作为参考。因此,代码中需要使用xarray.open_dataset来读取高度场数据,pandas读取txt气温数据。 然后,时间范围是1951-2010年的1月份数据。需要确保数据的时间维度正确,提取每年1月的数据。这里需要注意时间坐标的处理,可能需要对时间变量进行解析,比如使用xarray的日期功能筛选出每年的1月。 计算EU指数可能需要以下步骤:首先提取关键区域的高度场数据,计算区域平均值,然后对每个区域的时间序列进行标准化,最后EU指数可能是两个区域标准化值的差值或者某种组合。例如,假设EU指数是区域A的平均高度场异常减去区域B的平均值。标准化通常是指对每个区域的时间序列减去均值,除以标准差。用户可能需要先计算每个区域的平均高度场异常,再进行标准化,最后得到EU指数。 生成时间序列图表,需要使用matplotlib或seaborn来绘制折线图,并标注标准化后的EU指数随时间的变化。 空间相关系数的计算涉及将EU指数的时间序列与每个格点的高度场异常进行相关系数计算,同样地,与中国160站气温的相关系数也需要逐个站点计算。这里可以使用xarray和pandas的相关函数,比如xarray的corr方法,或者numpy的相关系数函数。计算后的相关系数需要保存为网格场,以便绘制空间分布图。 绘制地图图形需要用到cartopy库来创建地图投影,添加海岸线、经纬度标签等。对于中国160站的气温相关系数,可能需要将站点的经纬度信息与相关系数值对应,用散点图或站点标记来显示。 接下来,我需要考虑用户可能遇到的问题。例如,数据的时间对齐,确保高度场和气温数据的时间维度一致,都是1月份的数据。此外,处理不同分辨率的数据,如高度场是网格数据,而气温是站点数据,如何正确计算相关系数。还有,计算相关系数时的显著性检验是否需要,但用户没有提到,可能暂时不需要。 代码的步骤大致如下: 1. 导入必要的库:xarray, pandas, numpy, matplotlib, cartopy等。 2. 读取高度场nc文件,提取1月数据,时间范围1951-2010。 3. 读取中国160站气温txt文件,同样提取1月数据,可能需要对txt文件的格式进行解析,确保正确读取站点号、经纬度、各年数据。 4. 计算EU指数: a. 定义关键区域的经纬度范围,例如区域1(55°N-65°N, 20°E-40°E)和区域2(40°N-50°N, 80°E-100°E),假设这是EU指数的定义区域。 b. 计算每个区域内的高度场区域平均,得到两个时间序列。 c. 对每个时间序列进行标准化(减去均值,除以标准差)。 d. EU指数为两个标准化序列的差值。 5. 标准化EU指数本身,可能需要再次标准化,或者用户指的标准化是区域平均后的处理。这里需要明确步骤,可能需要根据文献确认。 6. 绘制EU指数的时间序列图。 7. 计算空间相关系数: a. 对高度场,每个格点的月高度场异常与EU指数进行相关计算。 b. 对气温站点,每个站点的气温异常与EU指数进行相关计算。 8. 绘制相关系数的空间分布图: a. 高度场使用填色图,叠加地图特征。 b. 气温站点使用散点图,颜色表示相关系数值。 可能遇到的问题包括: - 数据的时间处理:确保所有数据都是1月份,且年份范围正确。 - 区域平均的计算:使用xarray的sel和mean方法选择区域并计算平均。 - 标准化处理:使用时间序列的均值和标准差进行标准化。 - 相关系数的计算:使用xarray的corr函数或numpy.corrcoef。 - 地图投影的选择:通常使用PlateCarree投影,并正确设置经纬度范围。 需要验证每个步骤的正确性,比如提取1月数据是否正确,区域平均是否计算准确,相关系数的计算是否无误。此外,用户提供的引用[4]中提到了处理hgt.mon.mean.nc数据的方法,可以作为读取和筛选时间段的参考。 关于中国160站气温数据的格式,用户提到是txt格式,可能需要解析每个站的各年1月气温数据。例如,每行代表一个站点,包含站号、经纬度、以及各年1月的气温值。需要用pandas读取并转换为DataFrame,然后提取1951-2010年的数据。 在编写代码时,需要注意异常值的处理,比如缺失数据,可能需要填充或排除。另外,计算相关系数时,要确保两个变量的数据长度一致,且没有NaN值。 最后,整合所有步骤,写出完整的Python代码,并进行测试,确保每个部分运行正常,结果合理。</think>以下是为您设计的气象数据处理Python解决方案,包含完整的代码框架和关键步骤说明: ```python # -*- coding: utf-8 -*- import xarray as xr import pandas as pd import numpy as np import matplotlib.pyplot as plt import cartopy.crs as ccrs import cartopy.feature as cfeature from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter # 读取NCEP 500hPa高度场数据 def read_hgt_data(): ds = xr.open_dataset('hgt.mon.mean.nc') # 提取1月数据并裁剪时间范围 hgt_jan = ds.hgt.sel(time=ds['time.month']==1).sel(time=slice('1951-01', '2010-12')) return hgt_jan # 读取中国160站气温数据 def read_temp_data(): cols = ['station'] + [f'{y}' for y in range(1951, 2011)] df = pd.read_csv('China_160_stations.txt', delim_whitespace=True, header=None, names=cols, usecols=range(62)) # 解析站点信息(假设前3列为站号、经度、纬度) stations = df.iloc[:, :3].copy() stations.columns = ['station', 'lon', 'lat'] temp_data = df.iloc[:, 3:].T temp_data.index = pd.date_range('1951-01', periods=60, freq='AS-JAN') return stations, temp_data # 计算EU遥相关指数 def calculate_eu_index(hgt): # 定义EU关键区域(示例区域需根据实际定义调整) regionA = hgt.sel(lat=slice(55,65), lon=slice(20,40)).mean(dim=['lat','lon']) regionB = hgt.sel(lat=slice(40,50), lon=slice(80,100)).mean(dim=['lat','lon']) # 标准化处理 eu_index = (regionA - regionB) eu_norm = (eu_index - eu_index.mean()) / eu_index.std() return eu_norm # 绘制EU指数时间序列 def plot_eu_index(eu): plt.figure(figsize=(12,4)) eu.plot(color='royalblue', lw=2) plt.title('EU Teleconnection Index (1951-2010)', fontsize=14) plt.xlabel('Year') plt.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('EU_Index.png', dpi=300) # 计算空间相关系数 def spatial_correlation(hgt, temp, eu): # 高度场相关 hgt_corr = xr.corr(hgt, eu, dim='time') # 气温相关(假设temp已与eu时间对齐) temp_corr = temp.corrwith(eu.to_series()) return hgt_corr, temp_corr # 绘制空间相关分布图 def plot_correlation(hgt_corr, temp_corr, stations): # 高度场相关图 plt.figure(figsize=(10,6)) ax = plt.axes(projection=ccrs.PlateCarree()) hgt_corr.plot.contourf(ax=ax, levels=np.linspace(-1,1,21), cmap='RdBu_r', add_colorbar=True) ax.add_feature(cfeature.LAND.with_scale('50m')) ax.add_feature(cfeature.COASTLINE.with_scale('50m')) ax.set_xticks(np.arange(0,361,60)) ax.set_yticks(np.arange(-90,91,30)) ax.xaxis.set_major_formatter(LongitudeFormatter()) ax.yaxis.set_major_formatter(LatitudeFormatter()) plt.title('Correlation with 500hPa Height', fontsize=12) plt.savefig('Hgt_Correlation.png', dpi=300) # 气温相关图 plt.figure(figsize=(10,6)) ax = plt.axes(projection=ccrs.PlateCarree()) ax.add_feature(cfeature.COASTLINE.with_scale('50m')) sc = ax.scatter(stations.lon, stations.lat, c=temp_corr, cmap='RdBu_r', s=40, edgecolor='k', vmin=-1, vmax=1) plt.colorbar(sc, orientation='horizontal', pad=0.03) plt.title('Temperature Correlation with EU Index', fontsize=12) plt.savefig('Temp_Correlation.png', dpi=300) # 主程序流程 if __name__ == '__main__': hgt_data = read_hgt_data() stations, temp_data = read_temp_data() eu_index = calculate_eu_index(hgt_data) plot_eu_index(eu_index) hgt_corr, temp_corr = spatial_correlation(hgt_data, temp_data, eu_index) plot_correlation(hgt_corr, temp_corr, stations) ``` ### 关键步骤说明 1. **数据读取**: - 高度场使用xarray处理NetCDF格式,筛选1月数据时采用`time.month`选择器[^4] - 站点数据采用pandas解析文本格式,需注意数据排列方式(假设前3列为站点元数据) 2. **EU指数计算**: - 基于定义的关键区域(需根据实际研究调整经纬度范围) - 标准化处理采用`(X - μ)/σ`公式,确保指数无量纲化 3. **相关分析**: - 高度场相关使用xarray内置的corr函数进行网格点计算 - 气温相关使用pandas的corrwith方法实现站点级计算 4. **可视化**: - 采用Cartopy绘制地理空间分布图 - 使用发散色阶(RdBu_r)突出正负相关关系[^2] ### 数据预处理建议 1. 高度场数据需进行纬度加权处理: ```python weights = np.cos(np.deg2rad(hgt_data.lat)) hgt_weighted = hgt_data * weights ``` 2. 站点数据缺失值处理: ```python temp_data = temp_data.replace(-999.9, np.nan).interpolate(axis=0) ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值