0

У меня есть следующий текстовый файл:

Input data:
 N         tn         t1         t2         tk         u1         u2
21        5.000     20.000     50.000     65.000    100.000     95.000

i Time Uvx Uvix 1 5.000 0.000 0.000 2 8.000 20.000 50.000 3 11.000 40.000 50.000 4 14.000 60.000 50.000 5 17.000 80.000 50.000 6 20.000 100.000 50.000 7 23.000 99.500 50.000 8 26.000 99.000 50.000 9 29.000 98.500 50.000

Длительность переднего фронта:

Uvx Uvix 9.000 0.000

Задача заключается в том, чтобы считать с этого файла все числовые значения, минуя при этом строки с названиями переменных и прочим текстом, а также пустые строки. Так вот как пропустить эти самые строки и начать считывать данные с произвольной строки? Язык - Си.

αλεχολυτ
  • 28,987
  • 13
  • 60
  • 119
ivnku
  • 177
  • А что мешает просто пропустить первые n строк? – VladD Dec 20 '14 at 22:09
  • @VladD, может, я сейчас задам глупый вопрос, но как пропустить эти n строк? – ivnku Dec 21 '14 at 08:40
  • @Иван Кущёв, если размер каждой из них в байтах заранее неизвестен, то только последовательным чтением. – avp Dec 21 '14 at 09:25
  • @avp, просто несколько раз подряд использовать функцию fscanf()? Просто она вроде как начинает читать с первой строки. И, допустим, размер строк нам известен, тогда как решается эта задача? – ivnku Dec 21 '14 at 09:30
  • На каком шаге возникли трудности? Ясно ли, как прочитать строку из ввода, предполагая, что длина самой большой строки фиксирована (e.g., fgets())? Ясно ли, как пропустить не-числа в заданной строке (e.g., strcspn())? Ясно ли, как можно прочитать несколько чисел, разделённых пробелами (e.g., sscanf())? – jfs Dec 21 '14 at 13:42

2 Answers2

6

Чтобы избежать загрузки в память строк, которые всё равно будут проигнорированы и чтобы поддерживать произвольно большие входные строки, можно читать файл побайтно, используя fgetc().

Чтобы пропустить строки до тех пор пока не встретится строка, которая начинается с цифры (строка с численными данными), игнорируя возможные пробелы, можно использовать автомат с двумя состояниями:

  1. читаем до тех пор пока не прочитан символ новой строки (\n)
  2. переходим в состояние ожидания цифры (expect_digit=1): все пробелы и табы игнорируются, если прочитана цифра, то возвращаем её назад в поток и выходим из функции, в противном случае (не цифра) возвращаемся в начальное состояние 1.

#include <stdio.h>

// read until a line that starts with a digit (ignoring leading hor.space)
int skip_until_line_startswith_digit(FILE* file)
{
  int expect_digit = 1; // expect a digit as the next character
  for (int c; (c = fgetc(file)) != EOF; ) {
    switch (c) {
    case '\n': // newline
      expect_digit = 1;
      break;
    case ' ': // horizontal whitespace
    case '\t':
      // ignore
      break;
    case '0': // digit
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      if (expect_digit) {
        if (ungetc(c, file) == EOF)
          return -2; // error
        return 0; // found digit
      }
      // unexpected digit: ignore
      break;
    default: // everything else
      expect_digit = 0;
    }
  }
  return feof(file) ? -1 : -2; // not found or an error
}

isdigit() может зависеть от локали на Винде, поэтому она здесь не используется.

Пример использования

FILE* file = stdin;
if (skip_until_line_startswith_digit(file) < 0)
  exit(EXIT_FAILURE);

// read 1st numeric block
size_t N;
double tn, t1, t2, tk, u1, u2;
errno = 0;
if (fscanf(file, "%zu %lf  %lf  %lf  %lf  %lf  %lf",
                  &N, &tn, &t1, &t2, &tk, &u1, &u2) != 7) {
  if (errno) perror("N tn t1 t2 tk u1 u2");
  exit(EXIT_FAILURE);
}
printf("%zu %f %f %f %f %f %f\n", N, tn, t1, t2, tk, u1, u2);

А по поводу зависимости isdigit от локали -- можно поподробнее (я думаю, это всем будет интересно)? В каких случаях это не будет работать для распознавания чисел (и почему только в винде)?

С стандарт говорит (например, см. сноску в 7.11.1.1 в n1570), что isdigit() не зависит от локали, но на Винде isdigit() может зависеть от локали, например, '\xb2' в cp1252 кодировке в Danish локале распознается как десятичная цифра, что не правильно.

Кстати, Unicode стандарт солидарен с С стандартом: верхние индексы такие как U+00B2 явно исключены see 4.6 Numerical Value (Unicode 6.2).

jfs
  • 52,361
  • @jfs, хорошая идея. И тогда уж лучше, если skip будет возвращать количество прочитанных \n. Это позволит сделать нормальную диагностику об ошибках (номер строки).

    Ну, а perror после fscanf почти всегда будет выводить -- Success.

    Может стоит предусмотреть, что в принципе бывают отрицательные числа (кстати, scanf нормально читает и числа, которые начинаются с +).

    --

    А по поводу зависимости isdigit от локали -- можно поподробнее (я думаю, это всем будет интересно)?

    В каких случаях это не будет работать для распознавания чисел (и почему только в винде)?

    – avp Dec 22 '14 at 11:25
  • @avp: добавил инфу о isdigit(). – jfs Dec 22 '14 at 17:48
  • Спасибо, интересно.

    А то я уж начал фантазировать об иероглифах 〇, 一, 二, 三 ... 九
    (шутка)

    – avp Dec 22 '14 at 20:21
  • @avp: даже если добавить поддержку Юникодных чисел (придётся читать больше байта), эти иероглифы всё равно не являются Юникодными десятичными цифрами :) – jfs Dec 22 '14 at 20:51
  • Конечно, не являются, также как и приведенные Вами из cp1252
    (раньше я о таких извратах и не подозревал).
    – avp Dec 22 '14 at 20:59
3

@Иван Кущёв, fscanf не пойдет, т.к. она читает не по строкам. Можно, например, так:

 int c, nl = 0, skip_lines = сколько строк пропустить;

while ((c = fgetc(f)) != EOF) if ((c == '\n') && (++nl == skip_lines)) break; if (feof(f)) fatal("Bad data");

Если размер строк, точнее смещение интересующей строки в файле известно (или его можно вычислить), то см. man 3 fseek.

Обновление

@VladD, после getline free не нужна (а если вызывать free, тогда указатель опять надо обнулить).

Можно так (одной строкой):

 while (n-- && getline(&s, &len, file) > 0);
 if (n != -1)
   fatal(...);
 printf("last skipped line: %s", s);

только сожрет немного больше ресурсов (а вообще-то, я отвечая, просто и забыл про getline).

--

@Иван Кущёв, м.б. на практике для Вашей задачи лучше просто пропустить строки до начала данных?

 while (getline(&s, &len, file) > 0 && *s != 'i');

а дальше читать построчно и использовать sscanf(s, "%d %d %d %d", ...) для выборки данных.

Обновление

Ага, пусть так. Идея-то остается. Все равно ведь 3 разных блока данных.

Просто 3 раза while (getline(...) && ...); потом чтение (один раз в цикле).


А если у автора в libc нет getline, то пусть напишет свою версию (заодно потренируется, тем более что во втором блоке данных ему может понадобиться динамический массив структур).

Nicolas Chabanovsky
  • 51,426
  • 87
  • 267
  • 507
avp
  • 46,098
  • 6
  • 48
  • 116
  • А почему не просто
    char* s = NULL;
    size_t len = 0;
    while (n--)
    {
        getline(&s, &len, file);
        free(s);
    }
    
    

    (надеюсь, getline есть в стандарте.)

    – VladD Dec 21 '14 at 10:38
  • @avp: Точно! Я неправильно понял документацию, сейчас перечитал и дошёл. – VladD Dec 21 '14 at 21:08
  • getline() в С нет. getline() есть в POSIX.

    @avp: строки с 21 и 9.000 тоже нужно читать, т.е. нельзя *s != 'i' условие использовать.

    – jfs Dec 21 '14 at 22:37