هل هناك طريقة "قانونية" للقيام بذلك؟ أنا أستخدم head -n | tail -1
الذي يقوم بالخدعة ، لكنني أتساءل عما إذا كانت هناك أداة Bash تستخرج بشكل خاص خطًا (أو مجموعة من الخطوط) من ملف.
أعني بكلمة "قانوني" برنامج وظيفته الرئيسية هو القيام بذلك.
head
وأنبوب التوجيه مع tail
سيكون بطيئًا في ملف ضخم. أود أن أقترح sed
مثل هذا:
sed 'NUMq;d' file
حيث NUM
هو رقم السطر الذي تريد طباعته ؛ لذلك ، على سبيل المثال ، sed '10q;d' file
لطباعة السطر العاشر من file
.
تفسير:
سيتم إنهاء NUMq
على الفور عندما يكون رقم السطر NUM
.
d
سيحذف السطر بدلاً من طباعته ؛ يتم منع ذلك في السطر الأخير لأن q
يتسبب في تخطي بقية البرنامج النصي عند الإنهاء.
إذا كان لديك NUM
في متغير ، فستحتاج إلى استخدام علامات اقتباس مزدوجة بدلاً من واحدة:
sed "${NUM}q;d" file
sed -n '2p' < file.txt
سوف يطبع الخط الثاني
sed -n '2011p' < file.txt
خط 2011
sed -n '10,33p' < file.txt
السطر 10 حتى السطر 33
sed -n '1p;3p' < file.txt
الخط الأول والثالث
وما إلى ذلك وهلم جرا...
لإضافة خطوط مع sed ، يمكنك التحقق من هذا:
لديّ موقف فريد حيث يمكنني تحديد الحلول المقترحة في هذه الصفحة ، ولذا فإنني أكتب هذه الإجابة كتوحيد للحلول المقترحة مع أوقات تشغيل مضمنة لكل منها.
اقامة
لدي 3.261 غيغابايت ASCII ملف بيانات نصي مع زوج واحد من مفاتيح القيمة لكل صف. يحتوي الملف على 3،339،550،320 صفًا في المجموع ويتحدى فتح أي محرر قمت بتجربته ، بما في ذلك الانتقال إلى Vim. أحتاج إلى تعيين هذا الملف من أجل التحقيق في بعض القيم التي اكتشفتها تبدأ فقط حول الصف ~ 500،000،000.
لأن الملف به العديد من الصفوف:
أفضل سيناريو هو حل يستخلص سطرًا واحدًا فقط من الملف دون قراءة أي من الصفوف الأخرى في الملف ، لكن لا يمكنني التفكير في كيفية تحقيق ذلك في Bash.
لأغراض عقلاني ، لن أحاول قراءة الخطوط البالغ عددها 500.000.000 التي أحتاجها لمشكلتي. بدلاً من ذلك ، سأحاول استخراج الصف 50،000،000 من 3،339،550،320 (مما يعني أن قراءة الملف الكامل ستستغرق 60x أطول من اللازم).
سوف أستخدم time
المدمج لقياس أداء كل أمر.
خط الأساس
أولاً ، دعنا نرى كيف حل head
tail
:
$ time head -50000000 myfile.ascii | tail -1
pgm_icnt = 0
real 1m15.321s
خط الأساس للصف 50 مليون هو 00: 01: 15.321 ، إذا ذهبت مباشرة للصف 500 مليون فمن المحتمل أن يكون حوالي 12.5 دقيقة.
يقطع
أنا مشكوك في هذا ، لكن الأمر يستحق العناء:
$ time cut -f50000000 -d$'\n' myfile.ascii
pgm_icnt = 0
real 5m12.156s
استغرق هذا واحد 00: 05: 12.156 لتشغيل ، وهو أبطأ بكثير من الأساس! لست متأكدًا من قراءته للملف بالكامل أو حتى 50 مليون خط قبل إيقافه ، لكن بغض النظر عن ذلك ، لا يبدو هذا حلاً قابلاً للتطبيق للمشكلة.
AWK
لقد قمت بتشغيل الحل فقط باستخدام exit
لأنني لم أنتظر حتى يتم تشغيل الملف الكامل:
$ time awk 'NR == 50000000 {print; exit}' myfile.ascii
pgm_icnt = 0
real 1m16.583s
تم تشغيل هذا الرمز في 00: 01: 16.583 ، وهو أبطأ من ثانية واحدة تقريبًا ، ولكن لا يزال هذا التحسن غير أساسي في الأساس. على هذا المعدل ، إذا تم استبعاد أمر الخروج ، فربما استغرق الأمر حوالي 76 دقيقة تقريبًا لقراءة الملف بأكمله!
بيرل
قمت بتشغيل حل بيرل الحالي أيضًا:
$ time Perl -wnl -e '$.== 50000000 && print && exit;' myfile.ascii
pgm_icnt = 0
real 1m13.146s
تم تشغيل هذا الرمز في 00: 01: 13.146 ، والذي هو ~ 2 ثانية أسرع من خط الأساس. إذا قمت بتشغيله على 500.000.000 كاملة ، فربما يستغرق الأمر حوالي 12 دقيقة.
sed
الإجابة العليا على السبورة ، ها هي نتائجي:
$ time sed "50000000q;d" myfile.ascii
pgm_icnt = 0
real 1m12.705s
تم تشغيل هذا الرمز في 00: 01: 12.705 ، وهو أسرع بثلاث ثوانٍ من الخط الأساسي ، وأسرع ~ 0.4 ثانية من Perl. إذا كنت أقوم بتشغيلها على الصفوف الكاملة البالغة 500.000.000 ، فربما استغرق الأمر 12 دقيقة تقريبًا.
mapfile
لدي bash 3.1 وبالتالي لا يمكنني اختبار حل ملف mapfile.
استنتاج
يبدو ، في معظم الأحيان ، أنه من الصعب تحسين الحل head
tail
. في أحسن الأحوال ، يوفر حل sed
زيادة بنسبة 3٪ تقريبًا في الكفاءة.
(النسب المئوية محسوبة بالصيغة % = (runtime/baseline - 1) * 100
)
الصف 50،000،000
sed
Perl
head|tail
awk
cut
صف 500،000،000
sed
Perl
head|tail
awk
cut
الصف 3،338،559،320
sed
Perl
head|tail
awk
cut
مع awk
، فهو سريع جدًا:
awk 'NR == num_line' file
عندما يكون هذا صحيحًا ، يتم تنفيذ السلوك الافتراضي لـ awk
: {print $0}
.
إذا كان حجم ملفك ضخمًا ، فمن الأفضل exit
بعد قراءة السطر المطلوب. بهذه الطريقة يمكنك توفير وقت وحدة المعالجة المركزية.
awk 'NR == num_line {print; exit}' file
إذا كنت ترغب في إعطاء رقم السطر من متغير bash يمكنك استخدام:
awk 'NR == n' n=$num file
awk -v n=$num 'NR == n' file # equivalent
واو ، كل الاحتمالات!
جرب هذا:
sed -n "${lineNum}p" $file
أو واحدة من هذه اعتمادا على إصدار Awk الخاص بك:
awk -vlineNum=$lineNum 'NR == lineNum {print $0}' $file
awk -v lineNum=4 '{if (NR == lineNum) {print $0}}' $file
awk '{if (NR == lineNum) {print $0}}' lineNum=$lineNum $file
( قد تضطر إلى تجربة الأمر nawk
أو gawk
).
هل هناك أداة تعمل فقط على طباعة هذا الخط المحدد؟ ليست واحدة من الأدوات القياسية. ومع ذلك ، فإن sed
هو الأقرب والأبسط للاستخدام.
# print line number 52
sed '52!d' file
يتم وضع علامة على Bash لهذا السؤال ، وإليك طريقة Bash (≥4): mapfile
مع خيار -s
(تخطي) و -n
(العد).
إذا كنت بحاجة إلى الحصول على السطر 42 من ملف file
:
mapfile -s 41 -n 1 ary < file
في هذه المرحلة ، سيكون لديك صفيف ary
- الحقول التي تحتوي على سطور file
(بما في ذلك السطر الجديد الزائد) ، حيث تخطينا أول 41 سطرًا (-s 41
) ، وتوقفت بعد قراءة سطر واحد (-n 1
). هذا هو حقا الخط 42. لطباعته:
printf '%s' "${ary[0]}"
إذا كنت بحاجة إلى مجموعة من الخطوط ، فقل النطاق من 42 إلى 666 (ضمنيًا) ، ثم قل أنك لا تريد أن تفعل الرياضيات بنفسك ، ثم قم بطباعتها على stdout:
mapfile -s $((42-1)) -n $((666-42+1)) ary < file
printf '%s' "${ary[@]}"
إذا كنت بحاجة إلى معالجة هذه السطور أيضًا ، فليس من المناسب حقًا تخزين السطر الجديد الزائد. في هذه الحالة ، استخدم خيار -t
(تقليم):
mapfile -t -s $((42-1)) -n $((666-42+1)) ary < file
# do stuff
printf '%s\n' "${ary[@]}"
يمكنك الحصول على وظيفة تفعل ذلك لك:
print_file_range() {
# $1-$2 is the range of file $3 to be printed to stdout
local ary
mapfile -s $(($1-1)) -n $(($2-$1+1)) ary < "$3"
printf '%s' "${ary[@]}"
}
لا توجد أوامر خارجية ، فقط بنى Bash!
وفقًا لاختباراتي ، من حيث الأداء وسهولة القراءة ، فإن توصيتي هي:
tail -n+N | head -1
N
هو رقم السطر الذي تريده. على سبيل المثال ، ستطبع tail -n+7 input.txt | head -1
السطر السابع من الملف.
ستقوم tail -n+N
بطباعة كل شيء بدءًا من السطر N
، و head -1
ستتوقف بعد سطر واحد.
البديل head -N | tail -1
ربما يكون أكثر قابلية للقراءة قليلاً. على سبيل المثال ، سيؤدي هذا إلى طباعة السطر السابع:
head -7 input.txt | tail -1
عندما يتعلق الأمر بالأداء ، لا يوجد اختلاف كبير بالنسبة للأحجام الصغيرة ، ولكن سيتفوق عليه في tail | head
(من الأعلى) عندما تصبح الملفات ضخمة.
من المهم معرفة sed 'NUMq;d'
، ولكنني أزعم أن عدد الأشخاص الخارجين عن الصندوق سيكون مفهوما أكثر من حل الرأس/الذيل كما أنه أبطأ من الذيل/الرأس.
في اختباراتي ، تفوقت كل من إصدارات ذيول/الرؤوس على sed 'NUMq;d'
باستمرار. وهذا يتماشى مع المعايير الأخرى التي تم نشرها. من الصعب العثور على حالة كانت فيها ذيول/رؤوس سيئة بالفعل. كما أنه ليس من المستغرب أن تكون هذه العمليات التي تتوقع أن تكون الأمثل بشدة في نظام يونيكس الحديثة.
للحصول على فكرة حول اختلافات الأداء ، هذه هي الأرقام التي أحصل عليها لملف ضخم (9.3 جيجا بايت):
tail -n+N | head -1
: 3.7 ثانيةhead -N | tail -1
: 4.6 ثانيةsed Nq;d
: 18.8 ثانيةقد تختلف النتائج ، لكن الأداء head | tail
و tail | head
يمكن مقارنته عمومًا بالمدخلات الأصغر ، ويكون sed
دائمًا أبطأ بعامل كبير (حوالي 5x أو نحو ذلك).
لإعادة إنتاج المؤشر ، يمكنك تجربة ما يلي ، ولكن حذر من أنه سيؤدي إلى إنشاء ملف 9.3G في دليل العمل الحالي:
#!/bin/bash
readonly file=tmp-input.txt
readonly size=1000000000
readonly pos=500000000
readonly retries=3
seq 1 $size > $file
echo "*** head -N | tail -1 ***"
for i in $(seq 1 $retries) ; do
time head "-$pos" $file | tail -1
done
echo "-------------------------"
echo
echo "*** tail -n+N | head -1 ***"
echo
seq 1 $size > $file
ls -alhg $file
for i in $(seq 1 $retries) ; do
time tail -n+$pos $file | head -1
done
echo "-------------------------"
echo
echo "*** sed Nq;d ***"
echo
seq 1 $size > $file
ls -alhg $file
for i in $(seq 1 $retries) ; do
time sed $pos'q;d' $file
done
/bin/rm $file
فيما يلي إخراج التشغيل على الجهاز (ThinkPad X1 Carbon مع SSD و 16 G من الذاكرة). أفترض في النهاية أن كل شيء سيأتي من ذاكرة التخزين المؤقت وليس من القرص:
*** head -N | tail -1 ***
500000000
real 0m9,800s
user 0m7,328s
sys 0m4,081s
500000000
real 0m4,231s
user 0m5,415s
sys 0m2,789s
500000000
real 0m4,636s
user 0m5,935s
sys 0m2,684s
-------------------------
*** tail -n+N | head -1 ***
-rw-r--r-- 1 phil 9,3G Jan 19 19:49 tmp-input.txt
500000000
real 0m6,452s
user 0m3,367s
sys 0m1,498s
500000000
real 0m3,890s
user 0m2,921s
sys 0m0,952s
500000000
real 0m3,763s
user 0m3,004s
sys 0m0,760s
-------------------------
*** sed Nq;d ***
-rw-r--r-- 1 phil 9,3G Jan 19 19:50 tmp-input.txt
500000000
real 0m23,675s
user 0m21,557s
sys 0m1,523s
500000000
real 0m20,328s
user 0m18,971s
sys 0m1,308s
500000000
real 0m19,835s
user 0m18,830s
sys 0m1,004s
يمكنك أيضًا استخدام طباعة sed وإنهاء:
sed -n '10{p;q;}' file # print line 10
يمكنك أيضًا استخدام Perl لهذا:
Perl -wnl -e '$.== NUM && print && exit;' some.file
الحل الأسرع للملفات الكبيرة دائمًا هو الذيل | head ، بشرط أن تكون المسافة بين:
S
E
من المعروف. ثم ، يمكننا استخدام هذا:
mycount="$E"; (( E > S )) && mycount="+$S"
howmany="$(( endline - startline + 1 ))"
tail -n "$mycount"| head -n "$howmany"
كم هو مجرد عدد الخطوط المطلوبة.
مزيد من التفاصيل في https://unix.stackexchange.com/a/216614/79743
كمتابعة للإجابة المرجعية المفيدة للغاية لـ CaffeineConnoisseur ... كنت مهتمًا بمدى سرعة مقارنة طريقة "mapfile" بأخرى (حيث لم يتم اختبارها) ، لذلك جربت مقارنة السرعة السريعة والقذرة بنفسي لدي باش 4 مفيد. ألقيت في اختبار لطريقة "الذيل | الرأس" (بدلاً من الرأس | الذيل) المذكورة في أحد التعليقات على الإجابة العليا أثناء وجودي فيها ، حيث يغني الناس الثناء. ليس لدي أي شيء تقريبًا بحجم ملف testfile المستخدم ؛ أفضل ما يمكن أن أجده في غضون مهلة قصيرة هو ملف نسب 14M (خطوط طويلة مفصولة بمسافات بيضاء ، أقل بقليل من 12000 سطر).
نسخة مختصرة: يظهر ملف mapfile بشكل أسرع من طريقة القطع ، ولكنه أبطأ من أي شيء آخر ، لذا فإنني أسميها ملفقة. ذيل | يبدو الرأس ، OTOH ، أنه يمكن أن يكون الأسرع ، على الرغم من وجود ملف بهذا الحجم ، فإن الفرق ليس كبيرًا مقارنةً بـ sed.
$ time head -11000 [filename] | tail -1
[output redacted]
real 0m0.117s
$ time cut -f11000 -d$'\n' [filename]
[output redacted]
real 0m1.081s
$ time awk 'NR == 11000 {print; exit}' [filename]
[output redacted]
real 0m0.058s
$ time Perl -wnl -e '$.== 11000 && print && exit;' [filename]
[output redacted]
real 0m0.085s
$ time sed "11000q;d" [filename]
[output redacted]
real 0m0.031s
$ time (mapfile -s 11000 -n 1 ary < [filename]; echo ${ary[0]})
[output redacted]
real 0m0.309s
$ time tail -n+11000 [filename] | head -n1
[output redacted]
real 0m0.028s
أتمنى أن يساعدك هذا!
جميع الإجابات المذكورة أعلاه الإجابة مباشرة على السؤال. ولكن هنا حل أقل مباشرة ولكن فكرة أكثر أهمية يحتمل ، لإثارة الفكر.
نظرًا لأن أطوال الأسطر تعسفية ، فكل بايتات الملف قبل السطر nth need to read. إذا كان لديك ملف ضخم أو كنت بحاجة إلى تكرار هذه المهمة عدة مرات ، وكانت هذه العملية تستغرق وقتًا طويلاً ، فعليك التفكير بجدية فيما إذا كنت تريد تخزين بياناتك بطريقة مختلفة في المقام الأول.
الحل الحقيقي هو امتلاك فهرس ، على سبيل المثال في بداية الملف ، مع الإشارة إلى المواضع التي تبدأ منها الخطوط. يمكنك استخدام تنسيق قاعدة بيانات ، أو مجرد إضافة جدول في بداية الملف. بدلاً من ذلك ، قم بإنشاء ملف فهرس منفصل لمرافقة ملفك النصي الكبير.
مثلا يمكنك إنشاء قائمة بمواقع الأحرف للخطوط الجديدة:
awk 'BEGIN{c=0;print(c)}{c+=length()+1;print(c+1)}' file.txt > file.idx
ثم اقرأ بـ tail
، والذي فعليًا seek
s مباشرة إلى النقطة المناسبة في الملف!
مثلا للحصول على خط 1000:
tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
إذا حصلت على عدة أسطر بواسطة\n (سطر جديد عادة) يمكنك استخدام "قطع" كذلك:
echo "$data" | cut -f2 -d$'\n'
سوف تحصل على السطر الثاني من الملف. -f3
يمنحك السطر الثالث.
الكثير من الإجابات الجيدة بالفعل. أنا شخصيا أذهب مع awk. للراحة ، إذا كنت تستخدم bash ، فما عليك سوى إضافة ما يلي إلى ~/.bash_profile
. وفي المرة التالية التي تقوم فيها بتسجيل الدخول (أو إذا قمت بتصدير ملف .bash_profile الخاص بك بعد هذا التحديث) ، فستتوفر لديك وظيفة nthty جديدة "nth" متاحة لنقل ملفاتك عبرها.
قم بتنفيذ هذا أو ضعه في ملف ~/.bash_profile (في حالة استخدام bash) وأعد فتح bash (أو قم بتنفيذ source ~/.bach_profile
)
# print just the nth piped in line nth () { awk -vlnum=${1} 'NR==lnum {print; exit}'; }
ثم ، لاستخدامها ، ببساطة الأنابيب من خلال ذلك. . منها مثلا،:
$ yes line | cat -n | nth 5 5 line
باستخدام ما ذكره الآخرون ، أردت أن تكون هذه وظيفة سريعة وممتعة في شل باش.
قم بإنشاء ملف: ~/.functions
أضف إليها المحتويات:
getline() { line=$1 sed $line'q;d' $2 }
ثم أضف هذا إلى ~/.bash_profile
:
source ~/.functions
الآن عند فتح نافذة bash جديدة ، يمكنك فقط استدعاء الوظيفة على النحو التالي:
getline 441 myfile.txt
لطباعة السطر nth باستخدام sed مع متغير كرقم سطر:
a=4
sed -e $a'q:d' file
هنا علامة '-e' هي لإضافة نص إلى أمر ليتم تنفيذه.
لقد وضعت بعض الإجابات أعلاه في برنامج نصي للباش قصير يمكنك وضعه في ملف يسمى get.sh
وارتباط /usr/local/bin/get
(أو أي اسم آخر تفضله).
#!/bin/bash
if [ "${1}" == "" ]; then
echo "error: blank line number";
exit 1
fi
re='^[0-9]+$'
if ! [[ $1 =~ $re ]] ; then
echo "error: line number arg not a number";
exit 1
fi
if [ "${2}" == "" ]; then
echo "error: blank file name";
exit 1
fi
sed "${1}q;d" $2;
exit 0
تأكد من أنه قابل للتنفيذ مع
$ chmod +x get
ربطه لإتاحته على PATH
بـ
$ ln -s get.sh /usr/local/bin/get
استمتع بمسؤولية!
P