【问题标题】:Reading tabular data with fixed width and missing values读取具有固定宽度和缺失值的表格数据
【发布时间】:2017-01-07 03:43:59
【问题描述】:

我正在尝试从 Go 中的磁盘读取一个表,其中包含混合整数和浮点数,其中每个字段的宽度是固定的(每个字段占据固定数量的位置,如果太短,则前面有空格)并且其中一些值可能会丢失(并且应该默认为零)。

文件在这里:https://celestrak.com/SpaceData/sw20100101.txt

用于读取它的 Fortran 格式写在标题中:

FORMAT(I4,I3,I3,I5,I3,8I3,I4,8I4,I4,F4.1,I2,I4,F6.1,I2,5F6.1)

这些行看起来像这样(最后几行,有空格):

2014 12 29 2475  2 20 30 23 33 37 47 33 47 270   7  15   9  18  22  39  18  39  21 1.1 5  64 127.1 0 150.4 156.0 131.4 153.3 160.9
2014 12 30 2475  3 30 40 37 20 30 27 27 23 233  15  27  22   7  15  12  12   9  15 0.8 4  66 126.0 0 150.3 156.1 130.3 152.7 161.0
2014 12 31 2475  4 13 23 13 17 20 33 13 17 150   5   9   5   6   7  18   5   6   8 0.4 2  65 129.2 0 150.5 156.3 133.6 152.4 161.3
2015 01 01 2475  5 20 10 10 10 10 20 20 30 130   7   4   4   4   4   7   7  15   6       101 138.0 0 150.7 156.6 142.7 152.1 161.7
2015 01 02 2475  6 30 10 20 20 30 20 30 40 200  15   4   7   7  15   7  15  27  12       113 146.0 0 150.9 157.0 151.0 152.2 162.1
2015 01 03 2475  7 50 30 30 30 30 20 20 10 220  48  15  15  15  15   7   7   4  15       122 149.0 0 151.0 157.2 154.1 152.4 162.4

我一直在尝试一个巧妙的格式字符串来与 Sscanf 一起使用(例如“%4d%3d%3d%5d...”),但它不适用于空格,或者如果数字没有右对齐它的插槽。

我正在寻找一种像在 Fortran 中那样阅读它的方法,其中:

  • 可以使用混合字段类型(整数、浮点数、字符串)。
  • 每列都有固定的字符大小,必要时用空格填充空格,但不同的列可能有不同的大小。
  • 数值前面可以有零。
  • 可能缺少值,在这种情况下,它会给出零值。
  • 值可以位于槽中的任何位置,不一定是右对齐(不是示例,但可能)

是否有一种聪明的方法来阅读这样的内容,或者我应该手动拆分、修剪、检查和转换每个字段?

【问题讨论】:

  • 我认为拆分/修剪方法是您最好的选择。当您读入它时,看起来字节的顺序是标准化的,因此您可以为每一行运行一个循环,该循环只为每一行抓取字节 n 到 n+x 并相应地转换它们。例如:date, _ := time.Parse("2006 01 02",string(bytes[0:9])) 并继续使用 val := strconv.Atoi(string.TrimSpace(string(bytes[n:n+x]))) 等。

标签: go tabular


【解决方案1】:
package main

import "fmt"
import "reflect"
import "strconv"
import "strings"

type scanner struct {
    len   int
    parts []int
}

func (ss *scanner) Scan(s string, args ...interface{}) (n int, err error) {
    if i := len(s); i != ss.len {
        return 0, fmt.Errorf("exepected string of size %d, actual %d", ss.len, i)
    }
    if len(args) != len(ss.parts) {
        return 0, fmt.Errorf("expected %d args, actual %d", len(ss.parts), len(args))
    }
    n = 0
    start := 0
    for ; n < len(args); n++ {
        a := args[n]
        l := ss.parts[n]
        if err = scanOne(s[start:start+l], a); err != nil {
            return
        }
        start += l
    }
    return n, nil
}

func newScan(parts ...int) *scanner {
    len := 0
    for _, v := range parts {
        len += v
    }
    return &scanner{len, parts}
}

func scanOne(s string, arg interface{}) (err error) {
    s = strings.TrimSpace(s)
    switch v := arg.(type) {
    case *int:
        if s == "" {
            *v = int(0)
        } else {
            *v, err = strconv.Atoi(s)
        }
    case *int32:
        if s == "" {
            *v = int32(0)
        } else {
            var val int64
            val, err = strconv.ParseInt(s, 10, 32)
            *v = int32(val)
        }
    case *int64:
        if s == "" {
            *v = int64(0)
        } else {
            *v, err = strconv.ParseInt(s, 10, 64)
        }
    case *float32:
        if s == "" {
            *v = float32(0)
        } else {
            var val float64
            val, err = strconv.ParseFloat(s, 32)
            *v = float32(val)
        }
    case *float64:
        if s == "" {
            *v = float64(0)
        } else {
            *v, err = strconv.ParseFloat(s, 64)
        }
    default:
        val := reflect.ValueOf(v)
        err = fmt.Errorf("can't scan type: " + val.Type().String())
    }
    return
}

func main() {
    s := newScan(2, 4, 2)
    var a int
    var b float32
    var c int32

    s.Scan("12 2.2 1", &a, &b, &c)
    fmt.Printf("%d %f %d\n", a, b, c)

    s.Scan("1      2", &a, &b, &c)
    fmt.Printf("%d %f %d\n", a, b, c)

    s.Scan("        ", &a, &b, &c)
    fmt.Printf("%d %f %d\n", a, b, c)
}

输出:

12 2.200000 1
1 0.000000 1
0 0.000000 0

请注意,Scan 函数返回 n - 已解析参数的数量和错误。如果缺少值,该函数会将其设置为 0。实现主要取自 fmt.Scanf。

【讨论】:

  • 谢谢@kopiczko,这是一个非常好的解决方案!它可以很容易地演变为使用 Fortran 样式的格式来初始化扫描仪。你介意我把它做成一个包放在我的 Github 上吗?
  • 如果你愿意,你可以这样做。
【解决方案2】:

您可以使用分隔符设置为空格的 csv 编码。像这样的

import (
"encoding/csv"
"os"
)
file, _:=os.Open("/SpaceData/sw20100101.txt")
csvreader:=csv.NewReader(file)
csvreader.Comma=' '
csvreader.FieldsPerRecord=33
csvreader.TrimLeadingSpace=true
parsedout, _ := csvreader.Read()

这里是工作示例https://play.golang.org/p/Tsp72D4vsR

【讨论】:

  • 当记录之间可能出现多个空格时,我不确定这是否有效。您可能希望删除之前的连续空格。
  • 这样做的问题是值没有转换成int/float,如果有缺失值或者它们之间没有空格的值,就会失败。
猜你喜欢
  • 2021-06-16
  • 1970-01-01
  • 2013-09-14
  • 2014-04-17
  • 2012-09-26
  • 1970-01-01
  • 2019-07-31
  • 1970-01-01
  • 2017-01-23
相关资源
最近更新 更多