Рубрики
Без рубрики

Java: Вывод ширины столбцов текстового файла фиксированной ширины

Файлы с разделителями и файлы фиксированной ширины Плоские текстовые файлы, содержащие вкладку… Помеченный java, синтаксический анализ, функционал, потоки.

Файлы с разделителями и файлы фиксированной ширины

Плоские текстовые файлы, содержащие таблицы данных, обычно организованы одним из двух способов: как файлы с разделителями или как файлы фиксированной ширины . Файлы с разделителями используют один или несколько символов последовательно для разделения столбцов табличных данных вдоль каждой строки (а разрывы строк почти всегда используются для разделения строк). Распространенным форматом файлов с разделителями является формат CSV (значения, разделенные запятыми):

287540,Smith,Jones,Accountant,"$55,000"
204878,Ross,Betsy,Senior Accountant,"$66,000"
208417,Arthur,Wilbur,CEO,"$123,000"

…разделитель иногда может отображаться внутри значения в строке, и когда это происходит, значение обычно заключено в двойные кавычки. Кавычки также могут отображаться в значениях, и когда они появляются, они экранируются путем удвоения (“”). RFC-4180 определяет стандартный формат CSV.

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

287540 Smith  Jones  Accountant         $55,000
204878 Ross   Betsy  Senior Accountant  $66,000
208417 Arthur Wilbur CEO               $123,000

У каждого из этих подходов есть свои преимущества и недостатки. Файл с разделителями может быть проще для анализа, если в значения не встроены escape-символы и разделители. Файл с разделителями также занимает меньше места, чем файл фиксированной ширины, так как он не тратит байты на заполнение файла пробелами. Синтаксический анализ CSV-файлов может быть достаточно простым, если у вас есть хорошее регулярное выражение , но анализ файла фиксированной ширины может быть сложным. Либо пользователь должен заранее знать ширину столбцов и передать ее методу синтаксического анализа, либо метод должен определить ширину столбцов. Второй, немного более автоматизированный, я бы предпочел, так что давайте попробуем это сделать!

Считывание текстового файла в список <Строка>

Самое первое, что мы хотим сделать, это поместить наш файл фиксированной ширины в Список<Строка> . Для этого мы просто получаем java.io . Считыватель для файла в качестве буферизованного считывателя , а затем используйте BufferedReader ‘s readline() метод снова и снова, пока он не вернет null (он возвращает Строка если он успешно прочитал строку):

jshell> String fileName = "src/main/resources/example_sql_windows.txt"
fileName ==> "src/main/resources/example_sql_windows.txt"

jshell> BufferedReader reader = new BufferedReader(new FileReader(fileName))
reader ==> java.io.BufferedReader@2353b3e6

jshell> List lines = new ArrayList<>()
lines ==> []

jshell> String line = null // for use in the loop below
line ==> null

jshell> while ((line = reader.readLine()) != null) lines.add(line)

jshell> int nLines = lines.size() // save this for later
nLines ==> 22

Вот и все! Полегче! Обратите внимание, что нам пришлось создать экземпляр ArrayList , потому что Список является только интерфейсом и не может быть создан напрямую. Мы также можем использовать оператор diamond <> для экономии набора текста. Кроме этого, я надеюсь, что остальная часть приведенного выше кода более или менее проста. Теперь мы можем получить доступ к строкам нашего файла по их индексам в нашем списке строки .

Подсчитайте Количество символов, Не содержащих пробелов, В столбце символов

Затем мы хотим подсчитать количество символов, не содержащих пробелов, в столбце символов (в отличие от столбцов данных). “Столбец символов” – это столбец файла шириной в один символ, в то время как столбец данных состоит из одного или нескольких смежных столбцов символов. Столбец символов с очень небольшим количеством символов, не содержащих пробелов, скорее всего, будет столбцом-разделителем (разделяющим столбцы данных). Я объясню код шаг за шагом здесь, для ясности.

Во-первых, мы хотим взять каждую строку нашего файла и определить, является ли символ пробелом или нет. В принципе, мы хотим преобразовать наш Список<Строка> в Список<Список<Логическое>> , где каждый элемент внутреннего Списка является истинным если символ в этой позиции в этой строке не является символом пробела. Для этого мы сначала разбиваем Строку на символ[] массив, используя Строку.toCharArray() . (Для начала я буду использовать первую строку строк ( lines.get(0) ) в качестве заполнителя для последующего использования, когда мы будем использовать цикл.)

jshell> lines.get(0).toCharArray()
$86 ==> char[771] { 'e', 'x', 'e', 'c', ...

На этом этапе мы могли бы преобразовать этот символ[] в Поток<Символ> , окружив вышесказанное CharBuffer.wrap() , а затем вызвав символы() в результирующем буфере символов , используя mapToObj() и так далее, но есть гораздо более эффективный способ добиться того же самого – хороший, старый для цикла:

jshell> List> charsNonWS = new ArrayList<>() // String line => List line
charsNonWS ==> []

jshell> for (int ll = 0; ll < nLines; ++ll) { // loop over lines read from file
   ...>   charsNonWS.add(new ArrayList()); // add new empty array to List
   ...>   List temp = charsNonWS.get(ll); // save reference to use below
   ...>   for (char ch : lines.get(ll).toCharArray()) // loop over chars in this line
   ...>     temp.add(!Character.isWhitespace(ch)); // true if char is non-whitespace
   ...> }

jshell> charsNonWS
charsNonWS ==> [[true, true, true, true, true, ...

Теперь мы хотим подсчитать количество непробельных символов в столбце , а не в строке. Поэтому нам нужно в некотором смысле “повернуть” наши данные. Для этого давайте сначала найдем максимальное количество символьных столбцов в строке, а затем создадим массив такой длины. Здесь я использую Поток , чтобы сохранить ввод другого большого для цикла:

jshell> int nCharCols = charsNonWS.stream().mapToInt(e -> e.size()).max().orElse(0)
nCharCols ==> 771

charsNonWS.stream() преобразует charsNonWS из Списка<Список<Логический>> в Поток<Список<Логический>> . Другими словами, каждый элемент Потока представляет собой одну строку файла, где символы были преобразованы в ложные /|истинные значения в зависимости от того, являются ли они пробелами или нет, соответственно. Затем мы сопоставляем каждый Список <Логический> с одним Целое значение с mapToInt() . Это значение представляет собой длину строки в количестве символов, которую мы находим, сопоставляя каждый Список<Логический> с его размером с помощью mapToInt(e -> e.size()) . Наконец, мы находим максимальное значение потока (который теперь является потоком <Целое число> ) с max() . max() возвращает Необязательный , поэтому нам нужно извлечь это значение с помощью get() или чего-то подобного. Я выбрал OrElse(0) , который вернет 0 как максимальная длина строки (в символах), если что-то пошло не так в потоке .

Таким образом, максимальное количество символов, которое содержит любая строка в нашем файле, равно 771 . Теперь давайте создадим int[] и подсчитаем количество символов без пробелов в каждом из них 771 столбцы:

jshell> int[] counts = new int[nCharCols]
counts ==> int[771] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... , 0, 0, 0, 0, 0, 0, 0, 0 }

int s инициализируются для 0 , поэтому нам не нужно очищать массив, прежде чем мы начнем с ним работать. Вместо этого давайте перейдем прямо к нашему подсчету небелых символов в столбце.

jshell> for (List row : charsNonWS) // loop over each "row" ("line" / inner List)
   ...>   for (int cc = 0; cc < row.size(); ++cc) // loop over each "column" (char) in that "row" (line)
   ...>     if (row.get(cc)) ++counts[cc]; // if the char is non-whitespace (true), increment column

jshell> counts
counts ==> int[771] { 4, 4, 4, 2, 4, 4, 4, 4, 2, ...

Таким образом, подсчитывает теперь содержит количество символов, не содержащих пробелов, в каждом столбце символов текстового файла.

Выводить “Пустые” Столбцы

Далее мы хотим получить общее представление о количестве символов, не содержащих пробелов, в столбце. Другими словами, есть ли в каких-либо столбцах символы, не содержащие пробелов? Или есть минимальное количество? По сути, мы хотим сделать гистограмму из подсчетов . Самый простой способ сделать это, вероятно, с помощью другого потока :

jshell> Map map = Arrays.stream(counts). // convert int[] to Stream of primitive ints
   ...>   mapToObj(i -> (Integer)i). // convert primitive ints to Integers
   ...>   collect(Collectors.groupingBy( // group the Integers according to...
   ...>     Function.identity(), // their identity (value)
   ...>     Collectors.counting() // and then count the number in each group
   ...>   ))
map ==> {16=10, 0=9, 1=549, 17=113, 18=31, 2=39, 19=7, 3=2, 4=11}

Так что есть 9 линии с 0 символы, не содержащие пробелов, 549 линии с 1 символ, не содержащий пробелов, и так далее. Представляется вероятным, что те 9 “пустые” символьные столбцы разделяют столбцы данных. Давайте программно извлекем минимальное количество символов, не содержащих пробелов, в заданном столбце символов с карты выше, используя это для определения “пустых” столбцов:

jshell> int emptyColDef = Collections.min(map.keySet())
emptyColDef ==> 0

Это может показаться немного чрезмерным для этого приложения, но в целом неплохо автоматизировать подобные вещи. Это делает ваш код более надежным и многоразовым для будущих приложений. Приведенный выше код просто сравнивает ключи map (количество символов, не содержащих пробелов, в столбце символов) и находит минимальный.

Найдите Разделяющие Столбцы

Теперь мы можем найти столбцы символов, которые определяют (разделяют) экстенты столбцов данных. Обычно это столбцы с наименьшим количеством символов, не содержащих пробелов (когда пробелы используются для заполнения столбцов данных фиксированной ширины). Нам нужны индексы этих символьных столбцов, поэтому давайте получим Поток из подсчетов и сравним эти значения с нашим emptyColDef :

jshell> List emptyCols = Arrays.stream(counts). // convert int[] to Stream of primitive ints
   ...>   mapToObj(n -> n == emptyCol). // convert primitive ints to Booleans
   ...>   collect(Collectors.toList()) // collect in a List
emptyCols ==> [false, false, false, ...

Пустыми (разделяющими) столбцами являются столбцы со значениями true в пустых столбцах . Чтобы найти индексы, мы просто перебираем пустые столбцы :

jshell> List emptyIndices = new ArrayList<>()
emptyIndices ==> []

jshell> for (int cc = 0; cc < nCharCols; ++cc)
   ...>   if (emptyCols.get(cc)) emptyIndices.add(cc)

jshell> emptyIndices
emptyIndices ==> [38, 89, 120, 151, 352, 553, 592, 631, 670]

Приведенный выше цикл для просто проверяет, соответствует ли значение в индексе cc в пустые/| это правда . Если это так, он добавляет этот индекс в пустые индексы , которые теперь содержат индексы столбцов символов, которые разделяют столбцы данных в нашем файле фиксированной ширины! Последнее, что нужно сделать, это добавить 0 к началу Списка , потому что мы будем использовать смежные значения в качестве столбцов символов "начало" и "конец" для каждого столбца данных, а первый столбец данных начинается с 0 й персонаж:

jshell> int nDataCols = emptyIndices.size()
nDataCols ==> 9

jshell> emptyIndices.add(0, 0) // add a value 0 at the 0th position in the List

jshell> emptyIndices
emptyIndices ==> [0, 38, 89, 120, 151, 352, 553, 592, 631, 670]

Разбор

Наконец, мы можем использовать пустые индексы для анализа файла. Мы можем разделить каждую строку на заданные индексы символов, а затем выполнить String.trim() , чтобы удалить начальные и/или конечные пробелы. Обратите внимание, что некоторые строки могут быть короче “стандартной” длины строки (содержащие метаданные или что-то подобное), поэтому нам нужно выполнить проверку границ, прежде чем мы разделим Строку на подстроки:

jshell> List> tokens = new ArrayList<>(nLines) // pre-allocate space
tokens ==> []

jshell> for (int ll = 0; ll < nLines; ++ll) { // loop over all lines in file
   ...>   tokens.add(new ArrayList()); // add new List parsed tokens for line
   ...>   List tokensList = tokens.get(ll); // get reference to List to use below
   ...>   String line = lines.get(ll); // get line as String
   ...>   int len = line.length(); // get length of line in characters
   ...>   for (int ii = 1; ii <= nDataCols; ++ii) { // loop over data columns
   ...>     if (len < emptyIndices.get(ii)) break; // check if line is long enough to have next token
   ...>     tokensList.add(line.substring(emptyIndices.get(ii-1), emptyIndices.get(ii)).trim()); // get token
   ...>   }
   ...> }

jshell> tokens
tokens ==> [[execBegan, SampleID, ExperimentID, ...

jshell> tokens.get(7) // for example
$142 ==> [2018-11-04 11:07:16.8570000, 0016M978, test, test, SP -> Gilson, Execution Completed, 2018-11-04 11:07:15.0000000, 2018-11-04 11:09:37.5330000, 2018-11-04 11:07:11.7870000]

Красивая! Теперь у нас есть Список<Список<Строка>> , содержащий (во внешнем Списке ) строки файла, разбитые на (во внутреннем Списке s) Строка токены с обрезанными начальными и конечными пробелами. Мы определили ширину столбцов текстового файла фиксированной ширины и проанализировали его содержимое! В качестве следующего шага мы могли бы попытаться определить тип данных, содержащихся в каждом токене, возможно, используя что-то вроде моего Типизированный , который определяет тип данных, хранящихся в Java Строка s.

Я надеюсь, что это пошаговое руководство было полезным и/или интересным! Если у вас есть какие-либо комментарии или вопросы, пожалуйста, дайте мне знать в комментариях ниже. Я скомпилировал приведенный выше код в класс и также опубликовал его в Gist . Счастливого кодирования!

Оригинал: “https://dev.to/awwsmm/java-infer-column-widths-of-a-fixed-width-text-file-2hh0”