Файлы с разделителями и файлы фиксированной ширины
Плоские текстовые файлы, содержащие таблицы данных, обычно организованы одним из двух способов: как файлы с разделителями или как файлы фиксированной ширины . Файлы с разделителями используют один или несколько символов последовательно для разделения столбцов табличных данных вдоль каждой строки (а разрывы строк почти всегда используются для разделения строк). Распространенным форматом файлов с разделителями является формат 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> Listlines = 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 (Listrow : 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> Mapmap = 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> ListemptyCols = 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> ListemptyIndices = 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”