序章:资料预处理(python3.6 可用fortran unformatted sequencial data读取模块)

首先我只是一个接触Python约半年的菜鸟,开这一个专栏的目的主要是记录自己所学,以及实践的一些有用的东西,顺便分享一些自己写的公用代码段以方便具有同样想法的朋友。


既然是序章我就多写一些吧,我本人对人工智能在气象方面的应用特别感兴趣,有一样想法的你欢迎email到(wenqs@grmc.gov.cn

为什么是Python?

这个问题其实被问道了很多次了,相对其他行业气象尤其是天气业务类里能真充分发挥Python强大处理能力的地方其实比较有限,并且本来气象学就是一个偏重理解的学科,所以许多大佬们对这种新型的面向对象语言和其他编程语言的区别也不太在意。
但这里既然是我的场地,我要说:python的好处我这菜鸡可能体会得不深,不过有三点需要明确:

  1. 想要使用最火热的人工智能算法你绕不过python,应为很多很多的例子就是直接用python书写的。
  2. 我并不是专业编程科班出身,接触的编程语言并不多,但是在我接触到的里面,python的语法结构是最清晰,并且本人认为是最接近人类思维逻辑及自然语言的。这导致很多代码段其实就跟作文有一点点相似了。
  3. Python有非常强大的社区支持,无论你程序编写时,还是查找错误时,有用的帮助信息无处不在,同时网上许多的可用模块都统样高效,也保持着较为一致的语法特点。

为什么需要资料预处理呢?

——一句话,巧妇难为无米之炊啊!
这一点其实比较尴尬,请注意我这里说的“预处理”还没有到许多机器学习教程提到的“Data clean proccess”,仅仅只是将数据读入python。。。。
由于一些历史原因,国内天气预报业务用的数值模式预报产品一般采用两种格式:

  1. 标准的NetCDF格式格点资料。(这种资料网上到处是读取模块,这里就不赘述了)
  2. Fortran的“无格式二进制顺序存取”文件(fortran unformatted sequencial data)。这种文件在不同的操作系统中还细分为big-endian与little-endian版本。而且在存放高位数组集合时,将他们统一的看成很多个二维数组的叠加,然后存放每一个二维数组时会在数组的一头一尾添加特定的占位符,然后再在更高维度重读这种操作,所以直接用python二进制文件读取模块会因为错位问题根本读取不到想要的信息。endian问题和占位符问题也是网上很多文件读取教程根本无法正常读取气象模式预报数据的原因。(本章只针对这种格式

很不幸,我工作的生产环境采用了第二种,这种格式由于太过时,网上python对这种格式的支持并不好,一般的教程顶多叫你用numpy.fromfile()等等的方法定制特定数据类型再尝试读取,但是讲得都不够深入。另一种做饭是通过一些强大的数据格式转换软件如:CDO等等,将数据转化为NetCDF再进行读取,可是这样做即不效率又需要双倍的存储空间(我也曾经尝试过这种做法,实在是不好用)。
于是就诞生了自己书写可用的读取模块的冲动
这里首先说明,这个模块的设计思路来自一篇网页,但是作者停止了更新,于是我按照这个思路成功的重写了适合于grapes模式输出结果的读取模块CTLReader,完整的测试数据及和代码在github中,欢迎大家一起开发完善。

为了能够让大家看得懂代码,我在代码中进行了详细的中文注释,不需要的可以删除。

下面通过截图来说明几个有意思的代码段

图片.png

这一块为大家都会用的import

图片.png

这这一块我们进行了big/little endian的转换,一次性搞定以后就不需要类似>f4等等的类型说明符了。
图片.png

代码中有很多类似这样的正则表达式定义,网上有很多详细的说明,使用它们可以很方便的处理提取文本中的有用信息,具体到这个表达式的意思是:

匹配这样一段字符串“它以任意字母或数字开头重复一次或多次,后面接着一个或多个空格,再后面接着一个或多个数字,再后面接着一个或多个空格,再后面接着一个或多个数字,再后面接着一个或多个空格,最后面是任意字符串的组合”
具体到我们的test.ctl文件它能匹配到:


图片.png

红色圈圈表示正则式里的()中的内容

利用正则表达式和python中类型的定义我们愉快的完成了变量的分类

接下来,这一段里:

图片.png

看起来比较复杂,其实这里体现了python强大的数组管理功能,近乎完美的将这种叠成放置且每一个二维数组头尾各有一段多余占位符的数据处理了。首先[i:i+size]指定了类似一个“记录长度的范围”形成一个具有reshape方法的“子集”。然后,该方法的-1参数表示将这个子集的所有数据按照原本的大小进行遍历,然后在利用计算出的二维平面大小去迭代这段数据,相当于不用指定层数python自动把一个高维数组(这里是三维或思维)叠成了一叠由二维数组构造的“千层饼”。[:,int(place_hold/2):-int(place_hold/2)]剔除掉了“每一平面层”不需要的一头一尾,这样得到的子集再按照应该有的变量维度进行reshape。(真是非常方便呢!)
图片.png

上面图片的这一块按照本人的工作环境进行了特意的布置,一般的思路是既然ctl文件里有时间信息就应该直接获取之,但是在数值模式的产品中往往存在多个时间节点的数据,这些数据又是单独以不同文件的形式存放的,这样如果需要读取特定文件要不就是遍历所有的预报时次,要不就是生成新的ctl文件,这样都不效率,所有这里我直接将从文件名获取的时间信息写入变量属性中,这一段可以根据自己的需要相应修改。
以下放上模块的主要代码,不想去github下载的同学直接复制就可以用了:

import pandas as pd    # <---python 的通用类似数据库的数据存储模块,可以轻松的实现各种分析或与其他模块的对接。
import numpy as np     # <---这个模块就厉害了!可以说是python所有数组或矩阵计算的基础模块
                       #,擅长处理各种各样的数据类型,还能以object形式组建数组。
import dateparser      # <---网上查找到的处理时间信息的模块,据作者说基本可以将世界各国语言写成的人类能读懂的时间信息转化成
                       # python中的datetime类型对象,实现进一步处理。
import datetime        # <---时间类对象的本体模块
import re              # <---正则表达式模块,用来快速,精准,高效的处理有规律的文本信息。
import os              # <---跨系统平台的系统命令模块,可以使得python脚本具有跨平台运行的能力。
#以上是本脚本主体部分需要的功能模块。
from mpl_toolkits.basemap import Basemap 
import matplotlib.pyplot as plt
#以上是出图以测试数据需要用到的模块。
NUMBER = '[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?'         #识别一定长度或科学计数法的范例,因经常用到就单独写了
class CTLReader(object):
    def __init__(self,filepath,filename,place_hold=2):
        self.dimensions = {}
        self.variables = {}
        self.ctlpath = filepath
        self.filename = filename
#将ctl文件信息读入一个巨大的字符串中便于之后处理        
        with open(self.ctlpath,'r') as f:
            self.ctl = f.read()
        self._read_data()                #读取二进制文件数据
        self._read_dimensions()          #获取ctl中的维度信息
        self._read_vars(place_hold)      #将二进制文件数据规整为变量明命名的数组
    
    def _read_data(self):        
        self.undef = eval(re.search('undef (%s)' % NUMBER , self.ctl).group(1))    #获取CTL文件中的缺省值信息
        big_endian = bool(re.search('options.*big_endian',self.ctl,flags=re.I))    #探测数据是否是big_endian
        data = np.fromfile(self.filename,'f4')    #以4bytes的浮点形式(单精度)读取二进制文件
        if big_endian:
            data = data.byteswap()    #统一将big_endian数据进行位调换
        self.data = np.ma.masked_values(data,self.undef)    #建立带有默认缺省值的numpy数组并添加到类的自身属性中
        
    def _read_dimensions(self):
        if 'xdef' in self.ctl:    #探测是否存在xdef关键字
            p = re.compile("%s\s+(\d+)\s+linear\s+(%s)\s+(%s)" % ('xdef',NUMBER,NUMBER))    #创建正则维度信息范式
            m = p.search(self.ctl)
            self.variables['longitude'] = np.linspace(float(m.group(2)),
                                                      float(m.group(2))+float(m.group(3))*(int(m.group(1))-1),
                                                      int(m.group(1)))
            self.dimensions['longitude'] = int(m.group(1))
            
        if 'ydef' in self.ctl:    #探测是否存在ydef关键字
            p = re.compile("%s\s+(\d+)\s+linear\s+(%s)\s+(%s)" % ('ydef',NUMBER,NUMBER))    #创建正则维度信息范式
            m = p.search(self.ctl)
            self.variables['latitude'] = np.linspace(float(m.group(2)),
                                                     float(m.group(2))+float(m.group(3))*(int(m.group(1))-1),
                                                     int(m.group(1)))
            self.dimensions['latitude'] = int(m.group(1))
            
        if 'zdef' in self.ctl:    #探测是否存在zdef关键字
            self.variables['levels'] = Variable('levels',self._parse_dimension('zdef'))    #创建“层数”信息变量
            self.dimensions['levels'] = len(self.variables['levels'])
            
        if 'grapes' in self.ctl:  #探测是否存在grapes关键字
            self.variables['time'] = Variable('time',self._parse_dimension('time'))        #创建“时间”信息变量
            #目前只需要处理“单片”时次的数据
            self.dimensions['time'] = 1
            
    def _read_vars(self,place_hold):
        read = False    #是否识别为目标变量的开关

        for line in self.ctl.split('\n'):
            if line.startswith('endvars'):    #探测目标变量组结束符号
                read = False
            if read:
                p = re.compile('(\w+)\s+(\d+)\s+(\d+)\s+(.*)')    #目标变量行的正则范式
                m = p.match(line)
                name = m.group(1)
                var = self.variables[name] = Variable(name)       #生成特定的变量类并在本段方法中以"var"的别名进行描述
                levels = int(m.group(2))
                SPACE = self.dimensions['latitude']*self.dimensions['longitude']
                if levels > 0:
                    var.dimensions = ('time','levels','latitude','longitude')    #当变量为四维数组时变量的维度信息
                    size = self.dimensions['time']*self.dimensions['levels']*(SPACE+place_hold)
                else:
                    var.dimensions = ('time','latitude','longitude')    #当变量为三维数组时变量的维度信息
                    size = self.dimensions['time']*(SPACE+place_hold)
                
                var.shape = tuple(self.dimensions[dim] for dim in var.dimensions)    #根据不同的维度信息创建维度宽度提示元组
                var.data = self.data[i:i+size].reshape(-1,SPACE+place_hold)[:,
                                                                            int(place_hold/2):
                                                                            -int(place_hold/2)].reshape(var.shape)
                #以上操作较复杂,主要就是重构数据,去掉头尾的占位符,再次按照维度重构数据
                i += size    #相当与跳过一定长度的二进制数据字段
                
                units = int(m.group(3))    #单位信息,由于目前阶段处理数据不复杂,暂时不需要添加
                if units != 0:             #变量的量级转化开关(这种功能交给pandas等模拟自动做吧^_^)
                    raise NotImplementedError('for now only 0 units will be implemented!')
                
                var.attributes = {
                    'long_name': m.group(4).strip(),
                    'units': 'not needed right now'
                }
                #以上是变量的描述信息,及单位的存放属性
            if line.startswith('var'):    #探测目标变量组开始符号
                i = 0
                read = True
                
    def _parse_dimension(self,dim):    #用于检索CTL信息中维度相关信息的方法
        
        p = re.compile("%s\s+(\d+)\s+levels([\s\S]+)tdef"  % (dim))    #获取层数的具体信息的正则范式
        m = p.search(self.ctl)
        if m:
            return np.fromstring(m.group(2),sep='\n')    #以换行符分离目标信息,并生成numpy数组
        
        #time info read from file name
        if dim == 'time':    #对时间信息的定制处理
            filetime = os.path.basename(self.filename)
            p = re.compile('mars3km(\d{8})(\d+)')
            m = p.search(filetime)
            date = m.group(1)
            initime = dateparser.parse("20%s %s %s-%s:00:00" % (date[:2],date[2:4],date[4:6],date[6:8]))
            endtime = initime + datetime.timedelta(hours=int(m.group(2)))
            p = re.compile('\s+\d+\s+linear\s+[:\w]+\s+(\d+)(\w{2})')
            m = p.search(self.ctl)
            if m:
                if m.group(2) == 'mn':
                    increment = datetime.timedelta(minutes=int(m.group(1)))
                else:
                    increment = datetime.timedelta(hours=int(m.group(1)))
            return np.array([initime,endtime,increment])
        
    
class Variable(object):    #变量类定义
    def __init__(self,name,data=None):    #创世纪
        self.name = name                  #python说:“要有名字“!于是有了变量
        self.data = data                  #python说:”要有数据“!于是有了变量      
    def __getitem__(self,index):          #python说:”要有方法“!于是有了变量
        return self.data[index]    
    def __getattr__(self,key):
        return self.attributes[key]
    def __len__(self):
        return len(self.data)

最后,我这么做只是希望能方便的将模式数据读取到Python 中方便接下来的人工智能应用,如果下面还有合适分享的公用代码我还是会分享到简书和github上的。

最后的最后,祝福大家狗年吉祥如意!工作这半年确实学习到了不少好东西,希望狗年能尽快将方法应用到实际生产生活中。
顺便帮同学打个广告,我码字这么轻松就能写这么多主要是多亏了有“航天枸杞”保驾护航~~~_

图片.png

图片.png

忘记了最重要的数据读取测试结果了>.<

补充如下:

图片.png

可以看到读取数据画出来的反射率结果完全一致,说明读取数据是成功的~~oh,year~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容

  • Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对...
    顺毛阅读 4,171评论 4 16
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,598评论 25 707
  • 本次更新:20180319一、Mysql:1.S锁(共享锁)、X锁(排它锁):select语句默认加S锁;2.聚簇...
    白马王朗阅读 330评论 0 1
  • 清明小长假第一站,去号称最美乡村的王哥庄港西村,赏花,游乐,吃吃喝喝。 看看我镜头下的半老徐娘们,是不是各具风韵 ...
    苍山暮雪阅读 588评论 1 0
  • 刚才第一篇写了3.6千字,才仅仅写了一部分,看来全部整理下来之后,这将是一部万字以上的文章呢,所以趁着有兴致,就多...
    北境之风阅读 320评论 0 1