在 r 中解码声纳二进制数据
Decode sonar binary data in r
作为一名水文学家而不是程序员,我一直在解决简单的任务——从 LOWRENCE Fish Finder 中提取数据。它以二进制格式写入航迹、水深、温度等SL2. Which is a 144 bytes length with a 10 bytes header. There is a bunch of decoding implementations written in different languages (e.g. Java or Ruby)。
基于wiki, Ruby code and stack overflow I was trying to extract at least depth from this example file. Which in .csv can be found here.
我知道,depth
是从 60 开始的 4 字节浮点数。但是,this answer returns zero
:
建议的代码
# Open binary file
toread <- file("Chart 09_07_2018 [0].sl2", "rb")
# all data
alldata <- readBin(toread, raw(), n = 144, size = 1, endian = "little")
# read WaterDepth
readBin(alldata[59:65], double(), size = 4)
> [1] 0
close(toread)
如果我要使用 seek
函数,则相同 returns:
con <- file("Chart 09_07_2018 [2].sl2", "rb")
seek(con, 60L)
readBin(con, double(), endian="little")
close(con)
文件中的数据存在 — 我能够通过 Sonar Viewer
(see .csv example) 提取它。
制作 javascript/node.js 版本的小伙子修正了现场转录中的一些错误,值得一两枚奖章。
以下内容对于 grok 来说应该是相当简单的,但如果其中有任何需要 'splainin.您需要处理 longitude/latitude 编码(等)。
请注意,您可以跳过阅读函数源代码而直接执行:
devtools::install_git("https://gitlab.com/hrbrmstr/arabia")
library(arabia) # b/c I like puns way too much
read_sl2("your-sl2-file.sl2")
您可以在此处或此处查看源代码,不过:
read_sl2 <- function(path, verbose=TRUE) {
f <- file(path.expand(path), "rb")
dat <- readBin(f, "raw", n = file.size(path.expand(path)), endian="little")
close(f)
# read in the header
header <- readBin(dat, what = "raw", n = 10)
format <- readBin(header[1:2], "int", size=2, endian="little", signed=FALSE)
if (!(format %in% 1:3)) stop("Invalid 'format' in header; Likely not an slg/sl2/sl3 file")
ok_formats <- c("slg", "sl2", "sl3")
if (verbose) message("Format: ", ok_formats[format])
version <- readBin(header[3:4], "int", size=2, endian="little", signed=FALSE)
blockSize <- readBin(header[5:6], "int", size=2, endian="little", signed=FALSE)
if (blockSize == 1970) {
if (verbose) message("Block size: downscan")
} else if (blockSize == 3200) {
if (verbose) message("Block size: sidescan")
} else {
stop("Block size is not 'downscan' or 'sidescan'; Likely not an slg/sl2/sl3 file")
}
alwaysZero <- readBin(header[7:8], "int", size=2, endian="little", signed=FALSE)
# yep, we're going to build a list the hard/slow way
sl2_lst <- vector("list")
idx <- 1
pos <- 8 # keeping track of our place in the stream
while (pos < length(dat)) {
# if verbose mode echo a "." every 100 records
if (verbose && ((idx %% 100) == 0)) cat(".")
blockSize <- readBin(dat[(pos+29):(pos+30)], "int", size=2, endian="little", signed=FALSE)
prevBlockSize <- readBin(dat[(pos+31):(pos+32)], "int", size=2, endian="little", signed=FALSE)
packetSize <- readBin(dat[(pos+35):(pos+36)], "int", size=2, endian="little", signed=FALSE)
frameIndex <- readBin(dat[(pos+37):(pos+40)], "int", size=4, endian="little")
dplyr::data_frame(
channel = readBin(dat[(pos+33):(pos+34)], "int", size=2,endian="little", signed=FALSE),
upperLimit = readBin(dat[(pos+41):(pos+44)], "double", size=4, endian="little"),
lowerLimit = readBin(dat[(pos+45):(pos+48)], "double", size=4, endian="little"),
frequency = readBin(dat[(pos+51)], "int", size=1, endian="little", signed=FALSE),
waterDepth = readBin(dat[(pos+65):(pos+68)], "double", size=4, endian="little"),
keelDepth = readBin(dat[(pos+69):(pos+72)], "double", size=4, endian="little"),
speedGps = readBin(dat[(pos+101):(pos+104)], "double", size=4, endian="little"),
temperature = readBin(dat[(pos+105):(pos+108)], "double", size=4, endian="little"),
lng_enc = readBin(dat[(pos+109):(pos+112)], "integer", size=4, endian="little"),
lat_enc = readBin(dat[(pos+113):(pos+116)], "integer", size=4, endian="little"),
speedWater = readBin(dat[(pos+117):(pos+120)], "double", size=4, endian="little"),
track = readBin(dat[(pos+121):(pos+124)], "double", size=4, endian="little"),
altitude = readBin(dat[(pos+125):(pos+128)], "double", size=4, endian="little"),
heading = readBin(dat[(pos+129):(pos+132)], "double", size=4, endian="little"),
timeOffset = readBin(dat[(pos+141):(pos+144)], "integer", size=4, endian="little"),
flags = list(
dat[(pos+133):(pos+134)] %>%
rawToBits() %>%
as.logical() %>%
set_names(
c(
"headingValid", "altitudeValid", sprintf("unk%d", 1:7),
"gpsSpeedValid", "waterTempValid", "unk8", "positionValid",
"unk9", "waterSpeedValid", "trackValid"
)
) %>%
.[c(1:2, 10:11, 13, 15:16)] %>%
as.list() %>%
purrr::flatten_df()
)
) -> sl2_lst[[idx]]
idx <- idx + 1
pos <- pos + (packetSize+145-1)
}
if (verbose) cat("\n")
dplyr::bind_rows(sl2_lst) %>%
dplyr::mutate(
channel = dplyr::case_when(
channel == 0 ~ "Primary",
channel == 1 ~ "Secondary",
channel == 2 ~ "DSI (Downscan)",
channel == 3 ~ "Left (Sidescan)",
channel == 4 ~ "Right (Sidescan)",
channel == 5 ~ "Composite",
TRUE ~ "Other/invalid"
)
) %>%
dplyr::mutate(
frequency = dplyr::case_when(
frequency == 0 ~ "200 KHz",
frequency == 1 ~ "50 KHz",
frequency == 2 ~ "83 KHz",
frequency == 4 ~ "800 KHz",
frequency == 5 ~ "38 KHz",
frequency == 6 ~ "28 KHz",
frequency == 7 ~ "130-210 KHz",
frequency == 8 ~ "90-150 KHz",
frequency == 9 ~ "40-60 KHz",
frequency == 10~ "25-45 KHz",
TRUE ~ "Other/invalid"
)
) %>%
tidyr::unnest(flags)
}
试一试:
xdf <- read_sl2("~/Downloads/Chart 09_07_2018 [2].sl2")
## Format: sl2
## Block size: downscan
## .............
xdf
## # A tibble: 1,308 x 22
## channel upperLimit lowerLimit frequency waterDepth keelDepth speedGps temperature lng_enc lat_enc
## <chr> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <int> <int>
## 1 Secondary 0 13.3 200 KHz 2.62 0.328 0.5 15.8 4433307 7003054
## 2 DSI (Downsc… 0 13.4 200 KHz 2.62 0.328 0.5 15.8 4433307 7003054
## 3 Primary 0 13.3 200 KHz 2.62 0.328 0.5 15.9 4433307 7003054
## 4 Secondary 0 13.3 200 KHz 2.62 0.328 0.5 15.9 4433307 7003054
## 5 DSI (Downsc… 0 13.4 200 KHz 2.59 0.328 0 15.8 4433307 7003054
## 6 Secondary 0 13.3 200 KHz 2.59 0.328 0 15.8 4433307 7003054
## 7 Secondary 0 13.3 200 KHz 2.52 0.328 0 15.9 4433307 7003054
## 8 DSI (Downsc… 0 13.4 200 KHz 2.52 0.328 0 15.9 4433307 7003054
## 9 Primary 0 13.3 200 KHz 2.52 0.328 0 15.8 4433307 7003054
## 10 DSI (Downsc… 0 13.4 200 KHz 2.52 0.328 0 15.8 4433307 7003054
## # ... with 1,298 more rows, and 12 more variables: speedWater <dbl>, track <dbl>, altitude <dbl>,
## # heading <dbl>, timeOffset <int>, headingValid <lgl>, altitudeValid <lgl>, gpsSpeedValid <lgl>,
## # waterTempValid <lgl>, positionValid <lgl>, waterSpeedValid <lgl>, trackValid <lgl>
再看一眼:
glimpse(xdf)
## Observations: 1,308
## Variables: 22
## $ channel <chr> "Secondary", "DSI (Downscan)", "Primary", "Secondary", "DSI (Downscan)", "Sec...
## $ upperLimit <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ lowerLimit <dbl> 13.3, 13.4, 13.3, 13.3, 13.4, 13.3, 13.3, 13.4, 13.3, 13.4, 13.3, 13.4, 13.3,...
## $ frequency <chr> "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", ...
## $ waterDepth <dbl> 2.620, 2.620, 2.620, 2.620, 2.586, 2.586, 2.516, 2.516, 2.516, 2.516, 2.516, ...
## $ keelDepth <dbl> 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.32808...
## $ speedGps <dbl> 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0....
## $ temperature <dbl> 15.84112, 15.84112, 15.86293, 15.86293, 15.79128, 15.79128, 15.86293, 15.8629...
## $ lng_enc <int> 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 44333...
## $ lat_enc <int> 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 70030...
## $ speedWater <dbl> 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0....
## $ track <dbl> 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.97418...
## $ altitude <dbl> 324.7375, 324.7375, 324.7375, 324.7375, 324.8687, 324.8687, 324.8687, 324.868...
## $ heading <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ timeOffset <int> 1317703, 1317706, 1318036, 1318905, 1318946, 1318982, 1319130, 1319140, 13192...
## $ headingValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ altitudeValid <lgl> TRUE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FAL...
## $ gpsSpeedValid <lgl> TRUE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FAL...
## $ waterTempValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ positionValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ waterSpeedValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ trackValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
您可能想与您创建的 CSV 文件进行比较,因为我只是目测了一下。请注意,当它们在标志中标记为 "invalid" 时,您似乎使用了 "carried-forward" 值的导出器。我只是按原样保留它们。
如果您不是 "tidyverse" 那种人,"un-tidyverse"(留给提问者的练习)应该很容易。
作为一名水文学家而不是程序员,我一直在解决简单的任务——从 LOWRENCE Fish Finder 中提取数据。它以二进制格式写入航迹、水深、温度等SL2. Which is a 144 bytes length with a 10 bytes header. There is a bunch of decoding implementations written in different languages (e.g. Java or Ruby)。
基于wiki, Ruby code and stack overflow I was trying to extract at least depth from this example file. Which in .csv can be found here.
我知道,depth
是从 60 开始的 4 字节浮点数。但是,this answer returns zero
:
# Open binary file
toread <- file("Chart 09_07_2018 [0].sl2", "rb")
# all data
alldata <- readBin(toread, raw(), n = 144, size = 1, endian = "little")
# read WaterDepth
readBin(alldata[59:65], double(), size = 4)
> [1] 0
close(toread)
如果我要使用 seek
函数,则相同 returns:
con <- file("Chart 09_07_2018 [2].sl2", "rb")
seek(con, 60L)
readBin(con, double(), endian="little")
close(con)
文件中的数据存在 — 我能够通过 Sonar Viewer
(see .csv example) 提取它。
制作 javascript/node.js 版本的小伙子修正了现场转录中的一些错误,值得一两枚奖章。
以下内容对于 grok 来说应该是相当简单的,但如果其中有任何需要 'splainin.您需要处理 longitude/latitude 编码(等)。
请注意,您可以跳过阅读函数源代码而直接执行:
devtools::install_git("https://gitlab.com/hrbrmstr/arabia")
library(arabia) # b/c I like puns way too much
read_sl2("your-sl2-file.sl2")
您可以在此处或此处查看源代码,不过:
read_sl2 <- function(path, verbose=TRUE) {
f <- file(path.expand(path), "rb")
dat <- readBin(f, "raw", n = file.size(path.expand(path)), endian="little")
close(f)
# read in the header
header <- readBin(dat, what = "raw", n = 10)
format <- readBin(header[1:2], "int", size=2, endian="little", signed=FALSE)
if (!(format %in% 1:3)) stop("Invalid 'format' in header; Likely not an slg/sl2/sl3 file")
ok_formats <- c("slg", "sl2", "sl3")
if (verbose) message("Format: ", ok_formats[format])
version <- readBin(header[3:4], "int", size=2, endian="little", signed=FALSE)
blockSize <- readBin(header[5:6], "int", size=2, endian="little", signed=FALSE)
if (blockSize == 1970) {
if (verbose) message("Block size: downscan")
} else if (blockSize == 3200) {
if (verbose) message("Block size: sidescan")
} else {
stop("Block size is not 'downscan' or 'sidescan'; Likely not an slg/sl2/sl3 file")
}
alwaysZero <- readBin(header[7:8], "int", size=2, endian="little", signed=FALSE)
# yep, we're going to build a list the hard/slow way
sl2_lst <- vector("list")
idx <- 1
pos <- 8 # keeping track of our place in the stream
while (pos < length(dat)) {
# if verbose mode echo a "." every 100 records
if (verbose && ((idx %% 100) == 0)) cat(".")
blockSize <- readBin(dat[(pos+29):(pos+30)], "int", size=2, endian="little", signed=FALSE)
prevBlockSize <- readBin(dat[(pos+31):(pos+32)], "int", size=2, endian="little", signed=FALSE)
packetSize <- readBin(dat[(pos+35):(pos+36)], "int", size=2, endian="little", signed=FALSE)
frameIndex <- readBin(dat[(pos+37):(pos+40)], "int", size=4, endian="little")
dplyr::data_frame(
channel = readBin(dat[(pos+33):(pos+34)], "int", size=2,endian="little", signed=FALSE),
upperLimit = readBin(dat[(pos+41):(pos+44)], "double", size=4, endian="little"),
lowerLimit = readBin(dat[(pos+45):(pos+48)], "double", size=4, endian="little"),
frequency = readBin(dat[(pos+51)], "int", size=1, endian="little", signed=FALSE),
waterDepth = readBin(dat[(pos+65):(pos+68)], "double", size=4, endian="little"),
keelDepth = readBin(dat[(pos+69):(pos+72)], "double", size=4, endian="little"),
speedGps = readBin(dat[(pos+101):(pos+104)], "double", size=4, endian="little"),
temperature = readBin(dat[(pos+105):(pos+108)], "double", size=4, endian="little"),
lng_enc = readBin(dat[(pos+109):(pos+112)], "integer", size=4, endian="little"),
lat_enc = readBin(dat[(pos+113):(pos+116)], "integer", size=4, endian="little"),
speedWater = readBin(dat[(pos+117):(pos+120)], "double", size=4, endian="little"),
track = readBin(dat[(pos+121):(pos+124)], "double", size=4, endian="little"),
altitude = readBin(dat[(pos+125):(pos+128)], "double", size=4, endian="little"),
heading = readBin(dat[(pos+129):(pos+132)], "double", size=4, endian="little"),
timeOffset = readBin(dat[(pos+141):(pos+144)], "integer", size=4, endian="little"),
flags = list(
dat[(pos+133):(pos+134)] %>%
rawToBits() %>%
as.logical() %>%
set_names(
c(
"headingValid", "altitudeValid", sprintf("unk%d", 1:7),
"gpsSpeedValid", "waterTempValid", "unk8", "positionValid",
"unk9", "waterSpeedValid", "trackValid"
)
) %>%
.[c(1:2, 10:11, 13, 15:16)] %>%
as.list() %>%
purrr::flatten_df()
)
) -> sl2_lst[[idx]]
idx <- idx + 1
pos <- pos + (packetSize+145-1)
}
if (verbose) cat("\n")
dplyr::bind_rows(sl2_lst) %>%
dplyr::mutate(
channel = dplyr::case_when(
channel == 0 ~ "Primary",
channel == 1 ~ "Secondary",
channel == 2 ~ "DSI (Downscan)",
channel == 3 ~ "Left (Sidescan)",
channel == 4 ~ "Right (Sidescan)",
channel == 5 ~ "Composite",
TRUE ~ "Other/invalid"
)
) %>%
dplyr::mutate(
frequency = dplyr::case_when(
frequency == 0 ~ "200 KHz",
frequency == 1 ~ "50 KHz",
frequency == 2 ~ "83 KHz",
frequency == 4 ~ "800 KHz",
frequency == 5 ~ "38 KHz",
frequency == 6 ~ "28 KHz",
frequency == 7 ~ "130-210 KHz",
frequency == 8 ~ "90-150 KHz",
frequency == 9 ~ "40-60 KHz",
frequency == 10~ "25-45 KHz",
TRUE ~ "Other/invalid"
)
) %>%
tidyr::unnest(flags)
}
试一试:
xdf <- read_sl2("~/Downloads/Chart 09_07_2018 [2].sl2")
## Format: sl2
## Block size: downscan
## .............
xdf
## # A tibble: 1,308 x 22
## channel upperLimit lowerLimit frequency waterDepth keelDepth speedGps temperature lng_enc lat_enc
## <chr> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <int> <int>
## 1 Secondary 0 13.3 200 KHz 2.62 0.328 0.5 15.8 4433307 7003054
## 2 DSI (Downsc… 0 13.4 200 KHz 2.62 0.328 0.5 15.8 4433307 7003054
## 3 Primary 0 13.3 200 KHz 2.62 0.328 0.5 15.9 4433307 7003054
## 4 Secondary 0 13.3 200 KHz 2.62 0.328 0.5 15.9 4433307 7003054
## 5 DSI (Downsc… 0 13.4 200 KHz 2.59 0.328 0 15.8 4433307 7003054
## 6 Secondary 0 13.3 200 KHz 2.59 0.328 0 15.8 4433307 7003054
## 7 Secondary 0 13.3 200 KHz 2.52 0.328 0 15.9 4433307 7003054
## 8 DSI (Downsc… 0 13.4 200 KHz 2.52 0.328 0 15.9 4433307 7003054
## 9 Primary 0 13.3 200 KHz 2.52 0.328 0 15.8 4433307 7003054
## 10 DSI (Downsc… 0 13.4 200 KHz 2.52 0.328 0 15.8 4433307 7003054
## # ... with 1,298 more rows, and 12 more variables: speedWater <dbl>, track <dbl>, altitude <dbl>,
## # heading <dbl>, timeOffset <int>, headingValid <lgl>, altitudeValid <lgl>, gpsSpeedValid <lgl>,
## # waterTempValid <lgl>, positionValid <lgl>, waterSpeedValid <lgl>, trackValid <lgl>
再看一眼:
glimpse(xdf)
## Observations: 1,308
## Variables: 22
## $ channel <chr> "Secondary", "DSI (Downscan)", "Primary", "Secondary", "DSI (Downscan)", "Sec...
## $ upperLimit <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ lowerLimit <dbl> 13.3, 13.4, 13.3, 13.3, 13.4, 13.3, 13.3, 13.4, 13.3, 13.4, 13.3, 13.4, 13.3,...
## $ frequency <chr> "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", "200 KHz", ...
## $ waterDepth <dbl> 2.620, 2.620, 2.620, 2.620, 2.586, 2.586, 2.516, 2.516, 2.516, 2.516, 2.516, ...
## $ keelDepth <dbl> 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.328084, 0.32808...
## $ speedGps <dbl> 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0....
## $ temperature <dbl> 15.84112, 15.84112, 15.86293, 15.86293, 15.79128, 15.79128, 15.86293, 15.8629...
## $ lng_enc <int> 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 4433307, 44333...
## $ lat_enc <int> 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 7003054, 70030...
## $ speedWater <dbl> 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0....
## $ track <dbl> 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.974188, 4.97418...
## $ altitude <dbl> 324.7375, 324.7375, 324.7375, 324.7375, 324.8687, 324.8687, 324.8687, 324.868...
## $ heading <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ timeOffset <int> 1317703, 1317706, 1318036, 1318905, 1318946, 1318982, 1319130, 1319140, 13192...
## $ headingValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ altitudeValid <lgl> TRUE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FAL...
## $ gpsSpeedValid <lgl> TRUE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FAL...
## $ waterTempValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ positionValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ waterSpeedValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
## $ trackValid <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, ...
您可能想与您创建的 CSV 文件进行比较,因为我只是目测了一下。请注意,当它们在标志中标记为 "invalid" 时,您似乎使用了 "carried-forward" 值的导出器。我只是按原样保留它们。
如果您不是 "tidyverse" 那种人,"un-tidyverse"(留给提问者的练习)应该很容易。