Fortran:使用 'read' 读取数据时检查数据格式

Fortran : check data format while reading data with 'read'

我有特定的数据格式,比如 'n'(任意)行和“4”列。如果 'n' 为“10”,则示例数据将如下所示。

 1.01e+00 -2.01e-02 -3.01e-01    4.01e+02
 1.02e+00 -2.02e-02 -3.02e-01    4.02e+02
 1.03e+00 -2.03e-02 -3.03e-01    4.03e+02
 1.04e+00 -2.04e-02 -3.04e-01    4.04e+02
 1.05e+00 -2.05e-02 -3.05e-01    4.05e+02
 1.06e+00 -2.06e-02 -3.06e-01    4.06e+02
 1.07e+00 -2.07e-02 -3.07e-01    4.07e+02
 1.08e+00 -2.08e-02 -3.08e-01    4.07e+02
 1.09e+00 -2.09e-02 -3.09e-01    4.09e+02
 1.10e+00 -2.10e-02 -3.10e-01    4.10e+02

构建此输入的限制是

  1. 数据应该有“4”列。
  2. 数据以白色分隔 spaces.

我想实现一个功能来检查输入文件的每一行是否有“4”列,并根据 post 中的“M.S.B”答案构建我自己的Reading data file in Fortran with known number of lines but unknown number of entries in each line.

program readtest

  use :: iso_fortran_env

  implicit none

  character(len=512)     :: buffer

  integer                :: i, i_line, n, io, pos, pos_tmp, n_space
  integer,parameter      :: max_len = 512
  character(len=max_len) :: filename

  filename = 'data_wrong.dat'

  open(42, file=trim(filename), status='old', action='read')

  print *, '+++++++++++++++++++++++++++++++++++'
  print *, '+ Count lines                     +'
  print *, '+++++++++++++++++++++++++++++++++++'
  n       = 0
  i_line  = 0
  do
    pos     = 1
    pos_tmp = 1

    i_line = i_line+1
    read(42, '(a)', iostat=io) buffer

(*1)! Count blank spaces.
    n_space = 0
    do
      pos = index(buffer(pos+1:), " ") + pos
      if (pos /= 0) then
        if (pos > pos_tmp+1) then
          n_space = n_space+1
          pos_tmp = pos
        else
          pos_tmp = pos
        end if
      endif
      if (pos == max_len) then
        exit
      end if
    end do
    pos_tmp = pos

    if (io /= 0) then
      exit
    end if

    print *, '> line : ', i_line, ' n_space : ', n_space

    n = n+1
  end do

  print *, ' >> number of line = ', n

end program                                        

如果我 运行 上述程序的输入文件有如下错误行,

1.01e+00 -2.01e-02 -3.01e-01  4.01e+02
1.02e+00 -2.02e-02 -3.02e-01  4.02e+02
1.03e+00 -2.03e-02 -3.03e-01  4.03e+02
1.04e+00 -2.04e-02 -3.04e-01  4.04e+02
1.05e+00 -2.05e-02 -3.05e-01  4.05e+02
1.06e+00 -2.06e-02 -3.06e-01  4.06e+02
1.07e+00 -2.07e-02 -3.07e-01  4.07e+02
1.0      2.0       3.0
1.08e+00 -2.08e-02 -3.08e-01  4.07e+02  1.00
1.09e+00 -2.09e-02 -3.09e-01  4.09e+02
1.10e+00 -2.10e-02 -3.10e-01  4.10e+02

输出是这样的,

 +++++++++++++++++++++++++++++++++++
 + Count lines                     +
 +++++++++++++++++++++++++++++++++++
 > line :            1  n_space :            4
 > line :            2  n_space :            4
 > line :            3  n_space :            4
 > line :            4  n_space :            4
 > line :            5  n_space :            4
 > line :            6  n_space :            4
 > line :            7  n_space :            4
 > line :            8  n_space :            3   (*2)
 > line :            9  n_space :            5   (*3)
 > line :           10  n_space :            4
 > line :           11  n_space :            4
  >> number of line =           11

而且你可以看到错误的行被正确检测到,正如我预期的那样(参见(*2)和(*3)),我可以编写 'if' 语句来生成一些错误消息。

但我认为我的代码 'extremely' 很难看,因为我必须在代码中执行类似 (*1) 的操作才能将连续的白色 space 计为一个 space。我认为会有更优雅的方法来确保每行只包含“4”列,比如说,

read(*,'4(X, A)') line

(没用)

如果 'buffer' 的长度超过 'max_len'(在本例中设置为“512”),我的程序也会失败。事实上,“512”对于大多数实际用途来说应该足够了,我也希望我的检查子程序以这种方式健壮。

所以,我想至少在这些方面改进我的子程序

  1. 希望它更优雅(而不是 (*1))
  2. 更笼统(尤其是 'max_len')

有没有人有构建这种输入检查子程序的经验??

如有任何意见,我们将不胜感激。

感谢您阅读问题。

如果不了解确切的数据格式,我认为实现您想要的效果会相当困难(或者至少,我不知道该怎么做)。

在最一般的情况下,我认为你的space计数思路是最稳健和正确的。 可以对其进行调整以避免您描述的最大字符串长度问题。

在下面的代码中,我将数据作为未格式化的流访问文件进行处理。 基本上你阅读每个字符并记下 new_linesspaces。 正如您所做的那样,您使用 spaces 来计算列数(跳过双 spaces)和 new_line 字符来计算行数。 但是,这里我们不是将整行作为字符串读取并通过它来查找 spaces;我们逐个读取一个字符,避免了固定字符串长度的问题,而且我们也以一个循环结束。希望对你有帮助。

编辑:现在在行尾开始处理白色 spaces 和空行

program readtest

  use :: iso_fortran_env

  implicit none

  character              :: old_char, new_char
  integer                :: line, io, cols
  logical                :: beg_line
  integer,parameter      :: max_len = 512
  character(len=max_len) :: filename

  filename = 'data_wrong.txt'

  ! Output format to be used later
  100 format (a, 3x, i0, a, 3x , i0)

  open(42, file=trim(filename), status='old', action='read', &
      form="unformatted", access="stream")

  ! set utils
  old_char = " "
  line = 0
  beg_line = .true.
  cols = 0

  ! Start scannig char by char
  do
     read(42, iostat = io) new_char

     ! Exit if EOF
     if (io < 0) then
         exit
     end if

     ! Deal with empty lines
     if  (beg_line .and. new_char==new_line(new_char)) then
         line = line + 1
         write(*, 100, advance="no") "Line number:", line, &
             "; Columns: Number", cols
         write(*,'(6x, a5)') "EMPTYLINE"

     ! Deal with beginning of line for white spaces
     elseif  (beg_line) then
         beg_line = .false.

     ! this indicates new columns
     elseif (new_char==" " .and. old_char/=" ") then
         cols = cols + 1

     ! End of line: time to print
     elseif (new_char==new_line(new_char)) then
         if (old_char/=" ") then
             cols = cols+1
         endif
         line = line + 1

         ! Printing out results
         write(*, 100, advance="no") "Line number:", line, &
             "; Columns: Number", cols
         if (cols == 4) then
             write(*,'(6x, a5)') "OK"
         else
             write(*,'(6x, a5)') "ERROR"
         end if

         ! Restart with a new line (reset counters)
         cols = 0
         beg_line = .true.
     end if
     old_char = new_char
  end do
end program

这是这个程序的输出:

Line number:   1; Columns number:   4         OK
Line number:   2; Columns number:   4         OK
Line number:   3; Columns number:   4         OK
Line number:   4; Columns number:   4         OK
Line number:   5; Columns number:   4         OK
Line number:   6; Columns number:   4         OK
Line number:   7; Columns number:   4         OK
Line number:   8; Columns number:   3      ERROR
Line number:   9; Columns number:   5      ERROR
Line number:   10; Columns number:   4         OK
Line number:   11; Columns number:   4         OK

如果你知道你的数据格式,你可以在 4 维向量中读取你的行,并使用 iostat 变量在每一行打印出一个错误,其中 iostat 是一个大于的整数0.

您可以使用子字符串操作来获取您想要的内容,而不是计算空格。一个简单的例子如下:

program foo

  implicit none

  character(len=512) str    ! Assume str is sufficiently long buffer
  integer fd, cnt, m, n

  open(newunit=fd, file='test.dat', status='old')

  do
     cnt = 0
     read(fd,'(A)',end=10) str
     str = adjustl(str)      ! Eliminate possible leading whitespace
     do 
        n = index(str, ' ')  ! Find first space
        if (n /= 0) then
           write(*, '(A)', advance='no') str(1:n)
           str = adjustl(str(n+1:))
        end if
        if (len_trim(str) == 0) exit    ! Trailing whitespace
        cnt = cnt + 1
     end do
     if (cnt /= 3) then
        write(*,'(A)') '   Error'
     else
        write(*,*)
     end if
  end do

10 close(fd) 

end program foo

这应该读取任何合理长度的行(直到你的编译器默认的行限制,现在通常是 2GB)。您可以将其更改为流 I/O 以没有限制,但大多数 Fortran 编译器无法从标准输入读取流 I/O,本示例从中读取。因此,如果该行看起来像一个数字列表,它应该读取它们,告诉你它读取了多少,并让你知道它读取任何值作为数字(字符串,大于 REAL 大小的字符串)是否有错误价值, ....)。这里的所有部分都在 Fortran Wiki 上进行了解释,但为了简短起见,这是一个将各个部分组合在一起的精简版本。最奇怪的行为是,如果你输入这样的东西,里面有一个斜线

10 20,,30,40e4    50 / this is a list of numbers

它会将斜杠后的所有内容都视为注释,并且不会生成非零状态 return 而 return 有五个值。对于代码的更详细解释,我认为 Wiki 上的注释部分解释了它是如何工作的。在搜索中,查找“getvals”和“readline”。

所以使用这个程序你可以读取一行,如果 return 状态为零并且读取的值的数量是四个你应该很好,除了一些尘土飞扬的角落,这些线条肯定不会看起来就像一个数字列表。

module M_getvals
private
public getvals, readline
implicit none
contains
subroutine getvals(line,values,icount,ierr)
character(len=*),intent(in)     :: line
real                            :: values(:)
integer,intent(out)             :: icount, ierr
character(len=:),allocatable    :: buffer
character(len=len(line))        :: words(size(values))
integer                         :: ios, i
   ierr=0
   words=' '                            
   buffer=trim(line)//"/"               
   read(buffer,*,iostat=ios) words      
   icount=0
   do i=1,size(values)                 
      if(words(i).eq.'') cycle
      read(words(i),*,iostat=ios)values(icount+1)
      if(ios.eq.0)then
         icount=icount+1
      else
         ierr=ios
         write(*,*)'*getvals* WARNING:['//trim(words(i))//'] is not a number'
      endif
   enddo
end subroutine getvals
subroutine readline(line,ier)
character(len=:),allocatable,intent(out) :: line
integer,intent(out)                      :: ier
integer,parameter                        :: buflen=1024
character(len=buflen)                    :: buffer
integer                                  :: last, isize
   line=''
   ier=0
   INFINITE: do
      read(*,iostat=ier,fmt='(a)',advance='no',size=isize) buffer
      if(isize.gt.0)line=line//buffer(:isize)
      if(is_iostat_eor(ier))then
         last=len(line)
         if(last.ne.0)then
            if(line(last:last).eq.'\')then
               line=line(:last-1)
               cycle INFINITE
            endif
         endif
         ier=0
         exit INFINITE
     elseif(ier.ne.0)then
        exit INFINITE
     endif
   enddo INFINITE
   line=trim(line)
end subroutine readline
end module M_getvals
program tryit
use M_getvals, only: getvals, readline
implicit none
character(len=:),allocatable :: line
real,allocatable             :: values(:)
integer                      :: icount, ier, ierr
   INFINITE: do
      call readline(line,ier)
      if(allocated(values))deallocate(values)
      allocate(values(len(line)/2+1))
      if(ier.ne.0)exit INFINITE
      call getvals(line,values,icount,ierr)
      write(*,'(*(g0,1x))')'VALUES=',values(:icount),'NUMBER OF VALUES=',icount,'STATUS=',ierr
   enddo INFINITE
end program tryit

老实说,它应该可以与你扔给它的任何一行都合理地工作。

PS: 如果您总是读取四个值,使用列表导向 I/O 并在 READ 上检查 iostat= 值并检查您是否命中 EOR 将非常简单(只需几行),但既然您说过要读取行任意长度我假设一行有四个值只是一个例子,你想要一些非常通用的东西。