Bash сценарії та великі файли (помилка): введення з вбудованим читанням з перенаправлення дає несподіваний результат


16

У мене дивна проблема з великими файлами і bash. Це контекст:

  • У мене великий файл: 75G і 400 000 000+ рядків (це файл журналу, моє погано, я даю йому рости).
  • Перші 10 символів кожного рядка - це часові позначки у форматі РРРР-ММ-DD.
  • Я хочу розділити цей файл: один файл на день.

Я спробував із наступним сценарієм, який не спрацював. Моє запитання про те, що цей сценарій не працює, не альтернативні рішення .

while read line; do
  new_file=${line:0:10}_file.log
  echo "$line" >> $new_file
done < file.log

Після налагодження я знайшов проблему в new_fileзмінній. Цей сценарій:

while read line; do
  new_file=${line:0:10}_file.log
  echo $new_file
done < file.log | uniq -c

дає результат нижче (я кладу xes, щоб зберегти дані конфіденційними, інші символи - справжні). Зауважте, dhі коротші рядки:

...
  27402 2011-xx-x4
  27262 2011-xx-x5
  22514 2011-xx-x6
  17908 2011-xx-x7
...
3227382 2011-xx-x9
4474604 2011-xx-x0
1557680 2011-xx-x1
      1 2011-xx-x2
      3 2011-xx-x1
...
     12 2011-xx-x1
      1 2011-xx-dh
      1 2011-xx-x1
      1 208--
      1 2011-xx-x1
      1 2011-xx-dh
      1 2011-xx-x1    
...

Це не проблема у форматі мого файлу . Сценарій cut -c 1-10 file.log | uniq -cдає лише дійсні позначки часу. Цікаво, що частина вищезазначеного результату стає з cut ... | uniq -c:

3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1

Ми можемо бачити, що після підрахунку uniq 4474604 мій початковий сценарій не вдався.

Я потрапив на межу в баші, яку я не знаю, чи знайшов помилку в баші (це здається малоймовірним), чи я щось зробив не так?

Оновлення :

Проблема виникає після читання 2G файлу. Це шви readі перенаправлення не люблять файли більшого розміру, ніж 2G. Але все одно шукаємо більш точне пояснення.

Оновлення2 :

Це остаточно схоже на помилку. Його можна відтворити за допомогою:

yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c

але це прекрасно працює як вирішення (схоже, я знайшов корисне використання cat):

cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c 

Помилка подана до GNU та Debian. Змінені версії bash4.1.5 на Debian Squeeze 6.0.2 та 6.0.4.

echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu

Оновлення3:

Завдяки Андреасу Швабу, який швидко відреагував на мій звіт про помилку, саме цей патч - це рішення цієї поведінки. Файл, який зазнав впливу, lib/sh/zread.cяк раніше вказував Жилл:

diff --git a/lib/sh/zread.c b/lib/sh/zread.c index 0fd1199..3731a41 100644
--- a/lib/sh/zread.c
+++ b/lib/sh/zread.c @@ -161,7 +161,7 @@ zsyncfd (fd)
      int fd; {   off_t off;
-  int r;
+  off_t r;

  off = lused - lind;   r = 0;

rМінлива використовується для зберігання значення, що повертається lseek. Оскільки lseekповертає зсув з початку файлу, коли він перевищує 2 Гб, intзначення негативне, що призводить до того, що тест if (r >= 0)не виходить там, де він повинен був досягти успіху.


1
Чи можете ви повторити проблему з меншими наборами вхідних даних? Чи завжди такі самі вхідні рядки призводять до цих проблем?
larsks

@larks: гарне запитання. Проблема завжди починається з рядка № 13.520.918 (насправді двічі для тестів). Розмір файлу перед цим рядком становить 2.147.487.726. Здається, що тут є обмеження на 32 біти, але не зовсім так, як ми трохи перевищуємо 2 ^ 31 (2.147.483.648), але прямо при межі буфера 4K (2 ^ 31 + 4K = 2.147.487.744). Попередній і наступний рядки є нормальними рядками від 100 до 200 символів.
jfg956

Тестовано на 2-му файлі (приблизно однакового розміру): проблема починається з рядка № 13.522.712, а файл - до цього рядка 2.147.498.679 байт. Це вказує на бік межі readзаяви у bash.
jfg956

Відповіді:


13

Ви знайшли помилку в bash, сортів. Це відома помилка з відомим виправленням.

Програми представляють зміщення у файлі у вигляді змінної у певному цілому типі з кінцевим розміром. У старі часи всі використовували intмайже все, а intтип обмежувався 32 бітами, включаючи біт знаків, щоб він міг зберігати значення від -2147483648 до 2147483647. В даний час існують різні назви типів для різних речей , в т.ч.off_t для зміщення у файлі.

За замовчуванням off_t- це 32-розрядний тип на 32-бітній платформі (дозволяє до 2 ГБ) і 64-розрядний тип на 64-бітній платформі (дозволяє до 8 ЕБ). Однак звичайно компілювати програми за допомогою параметра LARGEFILE, який перемикає тип off_tна ширину 64 біт і робить програмний виклик придатним для реалізації функцій, таких якlseek .

Здається, що ви працюєте з bash на 32-бітній платформі, а ваш bash binary не компілюється з підтримкою великих файлів. Тепер, коли ви читаєте рядок із звичайного файлу, bash використовує внутрішній буфер для читання символів у партіях для продуктивності (детальніше див. Джерело в builtins/read.def). Коли рядок буде завершено, bash вимагає lseekповернути зміщення файлу назад до положення кінця рядка, якщо якась інша програма піклується про позицію у цьому файлі. Заклик lseekвідбуватися у zsyncfcфункції вlib/sh/zread.c .

Я не читав джерела дуже детально, але вважаю, що щось не відбувається гладко в точці переходу, коли абсолютне зміщення від’ємне. Таким чином, bash закінчує читання при неправильних зміщеннях, коли він поповнює свій буфер, після того, як він пройде позначку 2 Гб.

Якщо мій висновок невірний, і ваш bash насправді працює на 64-бітній платформі або компілюється з підтримкою bigfile, це, безумовно, помилка. Повідомте про це своєму розповсюдженню або вище за течією .

Оболонка не є правильним інструментом для обробки таких великих файлів у будь-якому випадку. Це буде повільно. Використовуйте sed, якщо можливо, інакше awk.


1
Мерсі Жиль. Чудова відповідь: повна, з достатньою інформацією, щоб зрозуміти проблему навіть людям без сильного CS-фону (32 біти ...). (Ларки також допомагають ставити запитання щодо номера рядка, і це слід визнати.) Після цього у мене теж була 32-бітна проблема та завантаження джерела, але ще не було до цього рівня аналізу. Merci encore, et bonne journée.
jfg956

4

Я не знаю про неправильне, але це, звичайно, суперечливо. Якщо рядки введення виглядають так:

YYYY-MM-DD some text ...

Тоді справді немає причин для цього:

new_file=${line:0:4}-${line:5:2}-${line:8:2}_file.log

Ви робите багато підрядкових робіт, щоб закінчити щось, що виглядає ... саме так, як це вже виглядає у файлі. Як щодо цього?

while read line; do
  new_file="${line:0:10}_file.log"
  echo "$line" >> $new_file
done

Це просто захоплює перші 10 символів з рядка. Ви також можете bashповністю відмовитися і просто використовувати awk:

awk '{print > ($1 "_file.log")}' < file.log

Це захоплює дату в $1 (перший стовпець, що розмежовується пробілом у кожному рядку) і використовує її для створення імені файлу.

Зауважте, що можливо, у ваших файлах є якісь помилкові рядки журналу. Тобто проблема може бути з введенням, а не з вашим сценарієм. Ви можете розширити awkсценарій до фіктивних рядків типу цього:

awk '
$1 ~ /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ {
    print > ($1 "_file.log")
    next
}

{
    print "INVALID:", $0
}
'

Це записує рядки, які відповідають YYYY-MM-DDвашим файлам журналу, і рядки прапорів, які не починаються із позначки часу на stdout.


У моєму файлі немає фіктивних рядків: cut -c 1-10 file.log | uniq -cдає очікуваний результат. Я використовую, ${line:0:4}-${line:5:2}-${line:8:2}тому що я поміщу файл у каталог ${line:0:4}/${line:5:2}/${line:8:2}, і я спростив проблему (я оновлю операцію проблеми). Я знаю, що awkможе допомогти мені тут, але я зіткнувся з іншими проблемами, використовуючи це. Те, що я хочу, - це зрозуміти проблему bash, а не знайти альтернативні рішення.
jfg956

Як ви вже говорили ... якщо ви "спростите" проблему у питанні, ви, мабуть, не отримаєте потрібних відповідей. Я все ще думаю, що вирішення цього питання з bash насправді не є правильним способом обробки такого роду даних, але немає причини, щоб це не працювало.
larsks

Спрощена проблема дає несподіваний результат, який я представив у питанні, тому не думаю, що це надмірне спрощення. Більше того, спрощена проблема дає аналогічний результат, як cutтвердження, яке працює. Оскільки я хочу порівнювати яблука з яблуками, а не з апельсинами, мені потрібно зробити речі максимально схожими.
jfg956

1
Я залишив у вас запитання, яке може допомогти з'ясувати, де справи йдуть не так ...
larsks

2

Звучить так, що ви хочете зробити:

awk '
{  filename = substr($0, 0, 10) "_file.log";  # input format same as output format
   if (filename != lastfile) {
       close(lastfile);
       print 'finished writing to', lastfile;
   }
   print >> filename;
   lastfile=filename;
}' file.log

closeЗберігає таблиці відкритих файлів від заповнення.


Дякую за рішення awk. Я вже приходжу з чимось подібним. Моє запитання полягало в тому, щоб зрозуміти обмеження базу, а не знайти альтернативне рішення.
jfg956
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.