Эффективное извлечение данных из нескольких файлов в один файл CSV

Необходимо будет сохранить содержание файла так или иначе. Можно использовать переменную.

content=`cat`
x=`echo "$content" | grep -o '>' | wc -l`
y=`echo "$content" | grep -o '<' | wc -l`
if [ "$x" -ne "$y" ]; then
  echo "Mismatch!"
fi
echo $x
echo $y

Или временный файл (необходимый, если example.html содержит пустые байты).

tmp=`mktemp`
trap "rm $tmp" EXIT
x=`grep -o '>' "$tmp" | wc -l`
y=`grep -o '<' "$tmp" | wc -l`
if [ "$x" -ne "$y" ]; then
  echo "Mismatch!"
fi
echo $x
echo $y

Если чтение содержания файла от stdin не является требованием, можно передать имя файла как аргумент сценарию.

x=`grep -o '>' "$1" | wc -l`
y=`grep -o '<' "$1" | wc -l`
if [ "$x" -ne "$y" ]; then
  echo "Mismatch!"
fi
echo $x
echo $y

Назовите сценарий как так:

$ ./myscript.sh example.html
3
13.04.2017, 15:36
5 ответов

Что касается расширения глобуса, возможно превышающего предел - да и нет. Оболочка уже запущена, поэтому не остановится. Но если бы вы передали весь глобальный массив в качестве аргументов одной команде, тогда да, это вполне возможно. Переносимый и надежный способ справиться с этим включает в себя find ...

find . \! -name . -prune -name pattern -type f -exec cat {} + | ...

... который будет только cat обычных файлов в текущем каталоге с именем, которое соответствует шаблону , но также вызовет cat столько раз, сколько необходимо, чтобы избежать превышения ARG_MAX .

На самом деле, поскольку у вас есть GNU sed , мы можем почти сделать все с помощью sed в find ] скрипт.

cd /path/to/xmls
find . \! -name . -prune -name \*.xml -type f -exec  \
    sed -sne'1F;$x;/\n*\( \)*<\/*double>/!d' \
        -e  '$s//\1/gp;H' {} + | paste -d\\0 - -

Я придумал другой способ. Это будет очень быстро, но это абсолютно зависит от того, что в файле должно быть ровно 168 совпадений, а может быть только одно . точка в именах файлов.

(   export LC_ALL=C; set '' - -
    while [ "$#" -lt 168 ]; do set "$@$@"; done
    shift "$((${#}-168))"
    find . \! -name . -prune -name \*.xml -type f      \
              -exec  grep -F '<double>' /dev/null {} + |
    tr \<: '>>' | cut -d\> -f1,4 | paste -d\  "$@"     |
    sed 'h;s|./[^>]*>||g;x;s|\.x.*||;s|..||;G;s|\n| |'
)

В соответствии с просьбой, вот небольшая разбивка того, как работает эта команда:

  1. (...)

    • Во-первых, весь небольшой скрипт запускается в своей собственной подоболочке, потому что есть несколько глобальных свойств окружения. что мы будем изменять в ходе его выполнения, и таким образом, когда работа будет выполнена, все изменяемые нами свойства будут восстановлены до своих исходных значений - какими бы они ни были.
  2. экспорт LC_ALL = C; set '' - -
    • Установив текущую локаль на C , мы можем сэкономить нашим фильтрам много усилий.В локали UTF-8 любой символ может быть представлен одним или несколькими байтами за кусок, и любой найденный символ должен быть выбран из группы, состоящей из многих тысяч возможных. В локали C каждый символ представляет собой один байт, а их всего 128. Это значительно ускоряет поиск символов.
    • Оператор set изменяет позиционные параметры оболочки. Выполнение набора '' - - устанавливает $ 1 в \ 0 , а $ 2 и $ 3 в - .
  3. while ... set "$ @ $ @"; Выполнено; shift ...
    • По сути, весь смысл этого оператора состоит в том, чтобы получить массив из 168 тире. Позже мы будем использовать paste , чтобы заменить последовательные наборы из 167 символов новой строки пробелами, сохранив при этом 168-е. Самый простой способ сделать это - дать ему 168 ссылок на аргументы на - stdin и указать, чтобы он склеил их все вместе.
  4. find ... -exec grep -F '' / dev / null '...
    • Бит find уже обсуждался ранее, но с grep мы печатаем только те строки, которые могут быть сопоставлены с -F фиксированной строкой . Сделав первый аргумент grep / dev / null - файл, который никогда не может соответствовать нашей строке - мы гарантируем, что grep ] всегда ищет 2 или более аргумента файла для каждого вызова. При вызове с двумя или более именованными файлами поиска grep всегда будет печатать имя файла, например file_000.xml: в начале каждой выходной строки.
  5. tr \ <: '>>'
    • Здесь мы переводим каждое вхождение в вывод grep символов : или < в > .
    • На этом этапе образец сопоставленной строки будет иметь вид ./ file_000.xml>> double> 0,0000> / double> .
  6. cut -d \> -f1,4
    • cut удалит из своего вывода все входные данные, которые не могут быть найдены ни в 1-м, ни в 4-м полях, разделенных на > символов. .
    • На этом этапе образец сопоставленной строки будет иметь вид ./ file_000.xml> 0,0000 .
  7. paste -d \ "$ @"
    • Уже обсуждалось, но здесь мы вставляем строки ввода группами по 168.
    • На этом этапе 168 совпадающих строк встречаются вместе, например: ./file_000.xml>0.000 ... / file_000.xml> 0.167
  8. sed 'h; s | ./ [^>] *> || g; x; s | \ .xml. * ||; s | .. ||; G; s | \ n | | '
    • Теперь более быстрые и небольшие утилиты уже сделали большую часть работы. В многоядерной системе они, вероятно, даже делали это одновременно. И эти утилиты - особенно cut и paste намного быстрее, чем любые попытки эмуляции, которые мы могли бы сделать с помощью утилит более высокого уровня, таких как sed ] или, что еще хуже, awk . Но я зашел настолько далеко, насколько мог себе представить, что смогу зайти так далеко, и мне нужно вызвать sed .
    • Сначала я h старую копию каждой входной строки, затем я g локально удаляю все вхождения шаблона ./ [^>] *> в пространстве шаблонов - так что каждое вхождение имени файла. На данный момент пространство шаблонов sed выглядит так: 0,000 0,0001 ... 0,167
    • Затем I e x change h old and pattern пробелы и удалите все из \. xml. * на -так что все, начиная с первого имени файла в сохраненной копии строки дальше. Затем я удаляю первые два символа - или ./ также - и в этот момент пространство шаблонов выглядит как file_000 .
    • Так что все, что остается, - это склеить их вместе. I G и копия старого пространства h , добавленная к пространству шаблонов после символа \ n ewline char, затем I s /// ubставьте строку \ n вместо пробела.
    • Итак, наконец, пространство шаблонов выглядит как file_000 0,000 ... 0,167 . И это то, что sed записывает в вывод для каждого файла, который find передает в grep .
4
27.01.2020, 21:09

Посмотрите на содержимое/etc/nsswitch.conf. Возможно, система не настроена на использование DNS для разрешения имен узлов. nslookup и dig не удосуживаются посмотреть, настроена ли система на использование DNS для разрешения имен хостов. Они используют DNS независимо. (Хотя вы не указываете сервер, они будут использовать/etc/resolv.conf, чтобы найти DNS сервер для использования.)

Вы хотите видеть DNS в строке хостов, что-то вроде hosts: files dns

-121--145524-

Вы пишете дважды для каждого файла. Это, вероятно, самая дорогая вечеринка. Вместо этого вы хотите попытаться сохранить все это в памяти, вероятно, в массиве. Тогда напишите один раз в конце концов.

Загляните в ulimit , если вы начнете достигать пределов памяти. Если вы увеличиваете эту рабочую нагрузку до 10-100 раз, вы смотрите на 10-100 ГБ памяти. Вы можете пакетировать это в цикл, который делает так много тысяч за итерацию. Я не уверен, что это должен быть повторяемый процесс, но стать более сложным, если вам нужно, чтобы он был быстрее/надежнее. В противном случае, сшить партии вручную после этого.

Вы также порождаете несколько процессов на файл - каждый канал, который у вас есть. Можно выполнить весь анализ/munging (grep/sed/tr) с помощью одного процесса. После этого Zsh может обрабатывать другие переводы с помощью расширений (см. man zshexpn ). Также можно выполнить все одиночные строки sed в одном вызове с несколькими выражениями. sed может быть быстрее, если вы избегаете -r (расширенный регекс) и жадности. Ваш grep может просто вытащить соответствующие строки из нескольких файлов одновременно и записать в промежуточные временные файлы. Знайте свои узкие места и не устраняйте их.

-121--95931-

Для каждого файла можно использовать одну вставку. Множественные разделители awk выполняют эффективное разделение и tr объединяет все строки в памяти, а не на диске.

for f in `ls *.xml` ; 
do 
     echo $f,`grep double $f | awk  -F  '[<>]' '{print $3}' | tr '\n' ','`; 
done

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

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

0
27.01.2020, 21:09

Скорее всего, по историческим причинам и/или по обратной совместимости.

Это часть пакета основных коммунальных услуг GNU, поэтому он будет рядом, пока Ричард Столлман и другие не почувствуют, что необходимо очистить его от существования.

-121--64144-

Посмотрите на содержимое/etc/nsswitch.conf. Возможно, система не настроена на использование DNS для разрешения имен узлов. nslookup и dig не удосуживаются посмотреть, настроена ли система на использование DNS для разрешения имен хостов. Они используют DNS независимо. (Хотя вы не указываете сервер, они будут использовать/etc/resolv.conf, чтобы найти DNS сервер для использования.)

Вы хотите видеть DNS в строке хостов, что-то вроде hosts: files dns

-121--145524-

Вы пишете дважды для каждого файла. Это, вероятно, самая дорогая вечеринка. Вместо этого вы хотите попытаться сохранить все это в памяти, вероятно, в массиве. Тогда напишите один раз в конце концов.

Загляните в ulimit , если вы начнете достигать пределов памяти. Если вы увеличиваете эту рабочую нагрузку до 10-100 раз, вы смотрите на 10-100 ГБ памяти. Вы можете пакетировать это в цикл, который делает так много тысяч за итерацию. Я не уверен, что это должен быть повторяемый процесс, но стать более сложным, если вам нужно, чтобы он был быстрее/надежнее. В противном случае, сшить партии вручную после этого.

Вы также порождаете несколько процессов на файл - каждый канал, который у вас есть. Можно выполнить весь анализ/munging (grep/sed/tr) с помощью одного процесса. После этого Zsh может обрабатывать другие переводы через расширения (см. man zshexpn ). Также можно выполнить все одиночные строки sed в одном вызове с несколькими выражениями. sed может быть быстрее, если вы избегаете -r (расширенный регекс) и жадности. Ваш grep может просто вытащить соответствующие строки из нескольких файлов одновременно и записать в промежуточные временные файлы. Знайте свои узкие места и не устраняйте их.

-1
27.01.2020, 21:09

Это должно помочь:

awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' $path_to_xml/*.xml > final_table.csv

Объяснение:

  • awk: используйте программу awk, я тестировал ее с GNU awk 4.0.1
  • -F '[<>]": используйте < и > в качестве разделителей полей
  • NR! =1 && FNR==1{printf "\n"}: если это не первая строка в целом (NR!=1), а первая строка файла (FNR==1), то выведите новую строку
  • FNR==1{sub(".*/", "", FILENAME); sub(". xml$", "", FILENAME); printf FILENAME}: если это первая строка файла, удалите что-либо до последней / (sub(".*/", "", FILENAME)) в имени файла (FILENAME), удалите трейлинг . xml (sub(". xml$", "", FILENAME)) и выведите результат (printf FILENAME)
  • /double/{printf " %s", $3}, если строка содержит "double" (/double/), выведите пробел, за которым следует третье поле (printf " %s", $3). Используя < и > в качестве разделителей, это будет число (при этом первым полем будет что угодно до первого <, а вторым - double). При желании вы можете отформатировать числа здесь. Например, используя %8.3f вместо %s, будет выведено любое число с 3-мя знаками после запятой и общей длиной (включая точку и знак после запятой) не менее 8.
  • END{printf "\n"}: после последней строки выведите дополнительную новую строку (это может быть необязательно)
  • $path_to_xml/*.xml: список файлов
  • > final_table.csv: поместите результат в final_table. csv, перенаправляя вывод

В случае ошибок "список аргументов на длинный", можно использовать find с параметром -exec для генерации списка файлов, вместо того, чтобы передавать его напрямую:

find $path_to_xml -maxdepth 1 -type f -name '*.xml' -exec awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' {} + > final_table.csv

Explanation:

  • find $path_to_xml: скажите find to list files in $path_to_xml
  • -maxdepth 1: не опускайтесь в подпапки $path_to_xml
  • -типа f: перечислять только обычные файлы (это также исключает сам $path_to_xml)
  • -имя '*. xml': только файлы списка, которые соответствуют шаблону*.xml`, это должно быть процитировано, иначе оболочка попытается расширить шаблон
  • -exec COMMAND {}. +: выполните команду COMMAND с соответствующими файлами в качестве параметров вместо {}. + указывает на то, что могут передаваться сразу несколько файлов, что уменьшает форкировку. Если вы используете \; (; необходимо процитировать, иначе это интерпретируется оболочкой) вместо +, команда выполняется для каждого файла в отдельности.

Также можно использовать xargs вместе с find:

find $path_to_xml -maxdepth 1 -type f -name '*.xml' -print0 |
 xargs -0 awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' > final_table.csv

Explanation

  • -print0: выводится список файлов, разделенных нулевыми символами
  • | (pipe): перенаправляет стандартный вывод find на стандартный вход xargs
  • xargs: строит и выполняет команды со стандартного входа, i. e. запускает команду для каждого переданного аргумента (здесь имена файлов).
  • -0: направляет xargs на предположение, что аргументы разделены нулевыми символами

awk -F '[<>]' '      
      BEGINFILE {sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      ENDFILE {printf "\n"}
    ' $path_to_xml/*.xml > final_table.csv

, где BEGINFILE, ENDFILE вызывается при смене файла (если ваш awk поддерживает это).

5
27.01.2020, 21:09

Пожалуйста, от имени будущих программистов и системных администраторов - НЕ используйте регулярное выражение для синтаксического анализа XML. XML - это структурированный тип данных, и он НЕ подходит для синтаксического анализа регулярных выражений - вы можете «подделать его», представив его обычным текстом, но в XML есть множество семантически идентичных вещей, которые не анализируют то же самое. Вы можете встроить перевод строки и, например, иметь унарные теги.

Таким образом - используйте парсер - я смоделировал некоторые исходные данные, потому что ваш XML недействителен. Дайте мне более полный образец, и я дам вам более полный ответ.

На базовом уровне - мы извлекаем узлы double следующим образом:

#!/usr/bin/env perl

use strict;
use warnings;
use XML::Twig;

my $twig = XML::Twig -> new;
$twig -> parse ( \*DATA ); 

foreach my $double ( $twig -> get_xpath('//double') ) {
   print $double -> trimmed_text,"\n";
}

__DATA__
<root> 
 <subnode> 
   <another_node>
      <double>1.2342</double>
      <double>2.3456</double>
      <some_other_tag>fish</some_other_tag>
   </another_node>
 </subnode>
</root> 

Это печатает:

1.2342
2.3456

Итак, мы расширяем это:

#!/usr/bin/env perl

use strict;
use warnings;
use XML::Twig;
use Text::CSV;

my $twig = XML::Twig->new;
my $csv  = Text::CSV->new;

#open our results file
open( my $output, ">", "results.csv" ) or die $!;
#iterate each XML File. 
foreach my $filename ( glob("/path/to/xml/*.xml") ) {
    #parse it
    $twig->parsefile($filename);
    #extract all the text of all the 'double' elements. 
    my @doubles = map { $_->trimmed_text } $twig->get_xpath('//double');
    #print it as comma separated. 
    $csv->print( $output, [ $filename, @doubles ] );

}
close($output);

Я думаю, что это должно помочь (без образцов данных , Точно не могу сказать). Но обратите внимание - используя синтаксический анализатор XML, мы не запутаемся с некоторым переформатированием XML, которое может быть выполнено совершенно корректно (как в соответствии со спецификацией XML). Используя синтаксический анализатор CSV, мы не собираемся попасть в ловушку каких-либо полей со встроенными запятыми или перевода строки.

Если вы ищете более конкретные узлы - вы можете указать более подробный путь. Как бы то ни было, приведенное выше просто ищет любой экземпляр double . Но вы можете использовать:

get_xpath("/root/subnode/another_node/double")
2
27.01.2020, 21:09

Теги

Похожие вопросы