วิธีที่ดีที่สุดในการจำลอง "กลุ่มโดย" จากทุบตี?


231

สมมติว่าคุณมีไฟล์ที่มีที่อยู่ IP หนึ่งที่อยู่ในแต่ละบรรทัด:

10.0.10.1
10.0.10.1
10.0.10.3
10.0.10.2
10.0.10.1

คุณต้องการเชลล์สคริปต์ที่นับจำนวนที่อยู่ IP แต่ละรายการที่ปรากฏในไฟล์ สำหรับอินพุตก่อนหน้านี้คุณต้องการเอาต์พุตต่อไปนี้:

10.0.10.1 3
10.0.10.2 1
10.0.10.3 1

วิธีหนึ่งในการทำเช่นนี้คือ:

cat ip_addresses |uniq |while read ip
do
    echo -n $ip" "
    grep -c $ip ip_addresses
done

อย่างไรก็ตามมันอยู่ไกลจากการมีประสิทธิภาพ

คุณจะแก้ปัญหานี้อย่างมีประสิทธิภาพยิ่งขึ้นด้วยการใช้ bash ได้อย่างไร

(สิ่งหนึ่งที่เพิ่มเติม: ฉันรู้ว่ามันสามารถแก้ไขได้จาก perl หรือ awk ฉันสนใจวิธีที่ดีกว่าในการทุบตีไม่ใช่ในภาษาเหล่านั้น)

ข้อมูลเพิ่มเติม:

สมมติว่าไฟล์ต้นฉบับคือ 5GB และเครื่องที่ใช้อัลกอริทึมมี 4GB การเรียงลำดับจึงไม่ใช่โซลูชันที่มีประสิทธิภาพไม่อ่านไฟล์มากกว่าหนึ่งครั้ง

ฉันชอบโซลูชันที่เหมือน hashtable - ทุกคนสามารถให้การปรับปรุงโซลูชันนั้นได้หรือไม่

ข้อมูลเพิ่มเติม # 2:

บางคนถามว่าทำไมฉันถึงต้องทำอย่างนี้ด้วยวิธีทุบตีเมื่อมันง่ายขึ้นเช่นใน Perl เหตุผลก็คือในเครื่องที่ฉันต้องทำ Perl นี้ไม่สามารถใช้ได้สำหรับฉัน มันเป็นเครื่องลินุกซ์ที่สร้างขึ้นเองโดยไม่มีเครื่องมือส่วนใหญ่ที่ฉันคุ้นเคย และฉันคิดว่ามันเป็นปัญหาที่น่าสนใจ

ดังนั้นโปรดอย่าตำหนิคำถามเพียงเพิกเฉยถ้าคุณไม่ชอบ :-)


ฉันคิดว่าทุบตีเป็นเครื่องมือที่ผิดสำหรับงาน Perl อาจจะเป็นทางออกที่ดีกว่า
Francois Wolmarans

คำตอบ:


412
sort ip_addresses | uniq -c

สิ่งนี้จะพิมพ์การนับก่อน แต่นอกเหนือจากที่ควรจะเป็นสิ่งที่คุณต้องการ


71
ซึ่งคุณสามารถไพพ์ไปที่ "sort -nr" เพื่อเรียงลำดับจากมากไปน้อยจากมากไปน้อย iesort ip_addresses | uniq -c | sort -nr
Brad Parks

15
และsort ip_addresses | uniq -c | sort -nr | awk '{ print $2, $1 }'เพื่อรับที่อยู่ IP ในคอลัมน์แรกและนับเป็นวินาที
Raghu Dodda

ปรับแต่งอีกหนึ่งส่วนสำหรับการจัดเรียง:sort -nr -k1,1
Andrzej Martyna

50

วิธีที่รวดเร็วและสกปรกมีดังนี้:

cat ip_addresses | sort -n | uniq -c

หากคุณต้องการใช้ค่าใน bash คุณสามารถกำหนดคำสั่งทั้งหมดให้กับตัวแปร bash แล้ววนซ้ำผลลัพธ์

PS

หากละเว้นคำสั่ง sort คุณจะไม่ได้ผลลัพธ์ที่ถูกต้องเนื่องจาก uniq จะดูเฉพาะบรรทัดที่ต่อเนื่องกันเท่านั้น


มันมีประสิทธิภาพที่คล้ายกันมากคุณยังคงมีพฤติกรรมกำลังสอง
Vinko Vrsalovic

ความหมายสมการกำลังสอง O (n ^ 2) ?? นั่นจะขึ้นอยู่กับอัลกอริทึมการเรียงลำดับแน่นอนว่ามันไม่น่าจะใช้ bogo-sort เช่นนั้น
paxdiablo

ในกรณีที่ดีที่สุดมันจะเป็น O (n log (n)) ซึ่งแย่กว่าสองรอบ (ซึ่งก็คือสิ่งที่คุณได้รับจากการใช้แฮชเล็กน้อย) ฉันควรจะพูดว่า 'superlinear' แทนที่จะเป็นกำลังสอง
Vinko Vrsalovic

และมันก็ยังอยู่ในขอบเขตเดียวกันกับสิ่งที่ OP ขอให้ปรับปรุงประสิทธิภาพอย่างชาญฉลาด ...
Vinko Vrsalovic

11
uuoc ใช้ cat ไร้ประโยชน์

22

สำหรับการสรุปหลาย ๆ ฟิลด์ขึ้นอยู่กับกลุ่มของฟิลด์ที่มีอยู่ใช้ตัวอย่างด้านล่าง: (แทนที่ $ 1, $ 2, $ 3, $ 4 ตามความต้องการของคุณ)

cat file

US|A|1000|2000
US|B|1000|2000
US|C|1000|2000
UK|1|1000|2000
UK|1|1000|2000
UK|1|1000|2000

awk 'BEGIN { FS=OFS=SUBSEP="|"}{arr[$1,$2]+=$3+$4 }END {for (i in arr) print i,arr[i]}' file

US|A|3000
US|B|3000
US|C|3000
UK|1|9000

2
+1 เพราะมันแสดงสิ่งที่ต้องทำเมื่อไม่จำเป็นต้องนับเท่านั้น
user829755

1
+1 เพราะsortและuniqง่ายที่สุดสำหรับการนับ แต่ไม่ช่วยเมื่อคุณต้องการคำนวณ / รวมค่าฟิลด์ ไวยากรณ์อาร์เรย์ของ awk นั้นมีประสิทธิภาพมากและเป็นกุญแจสำคัญในการจัดกลุ่มที่นี่ ขอบคุณ!
odony

1
อีกสิ่งหนึ่งที่ดูออกว่า awk ของprintฟังก์ชั่นที่ดูเหมือนว่าจะ downscale 64 บิตจำนวนเต็ม 32 บิตดังนั้นสำหรับค่า int เกิน 2 ^ 31 คุณอาจต้องการที่จะใช้printfกับ%.0fรูปแบบแทนการprintมี
odony

1
คนที่มองหา "จัดกลุ่มตาม" ด้วยการเรียงสตริงแทนการเพิ่มจำนวนจะแทนที่arr[$1,$2]+=$3+$4ด้วยเช่นarr[$1,$2]=(arr[$1,$2] $3 "," $4). I needed this to provide a grouped-by-package list of files (two columns only) and used: arr [$ 1] = (arr [$ 1] $ 2) `ด้วยความสำเร็จ
Stéphane Gourichon

20

วิธีการแก้ปัญหาที่ยอมรับเป็นที่กล่าวถึงโดยผู้ตอบอื่น:

sort | uniq -c

มันสั้นและกระชับกว่าที่เขียนใน Perl หรือ awk

คุณเขียนว่าคุณไม่ต้องการใช้การเรียงลำดับเนื่องจากขนาดของข้อมูลมีขนาดใหญ่กว่าขนาดหน่วยความจำหลักของเครื่อง อย่าดูถูกดูแคลนคุณภาพการใช้งานของคำสั่งการเรียงลำดับ Unix การเรียงลำดับถูกใช้เพื่อจัดการข้อมูลจำนวนมาก (คิดว่าข้อมูลการเรียกเก็บเงินดั้งเดิมของ AT&T) บนเครื่องที่มีหน่วยความจำ 128k (นั่นคือ 131,072 ไบต์) (PDP-11) เมื่อจัดเรียงพบข้อมูลมากกว่าขีด จำกัด ที่ตั้งไว้ (มักปรับให้ใกล้กับขนาดของหน่วยความจำหลักของเครื่อง) มันจะเรียงลำดับข้อมูลที่อ่านในหน่วยความจำหลักและเขียนลงในไฟล์ชั่วคราว จากนั้นทำซ้ำการกระทำด้วยข้อมูลชิ้นต่อไป ในที่สุดก็ทำการเรียงลำดับการผสานในไฟล์ระดับกลางเหล่านั้น ซึ่งช่วยให้การเรียงลำดับสามารถทำงานกับข้อมูลที่มีขนาดใหญ่กว่าหน่วยความจำหลักของเครื่องได้หลายเท่า


ก็ยังแย่กว่าการแฮชนับใช่มั้ย คุณรู้หรือไม่ว่าอัลกอริธึมการเรียงลำดับใดที่ใช้เรียงลำดับข้อมูลในหน่วยความจำ มันแตกต่างกันในกรณีข้อมูลตัวเลข (ตัวเลือก -n)?
Vinko Vrsalovic

ขึ้นอยู่กับวิธีการเรียงลำดับ (1) ทั้ง GNU sort (ใช้กับ Linux distributions) และ BSD sort นั้นมีความยาวมากเพื่อใช้อัลกอริทึมที่เหมาะสมที่สุด
Diomidis Spinellis

9
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

คำสั่งนี้จะให้ผลลัพธ์ที่คุณต้องการ


4

ดูเหมือนว่าคุณจะต้องใช้รหัสจำนวนมากเพื่อจำลองการแฮชใน bash เพื่อให้ได้พฤติกรรมเชิงเส้นหรือติดกับรุ่นซุปเปอร์คลินิคกำลังสอง

ในบรรดาเวอร์ชั่นเหล่านั้นทางออกของsauaนั้นดีที่สุด (และง่ายที่สุด):

sort -n ip_addresses.txt | uniq -c

ผมพบว่าhttp://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-11/0118.html แต่มันน่าเกลียดเหมือนนรก ...


ฉันเห็นด้วย. นี่คือทางออกที่ดีที่สุดจนถึงขณะนี้และวิธีแก้ไขปัญหาที่คล้ายคลึงกันนั้นเป็นไปได้ใน perl และ awk ใครสามารถให้การใช้งานที่สะอาดกว่าในการทุบตี?
Zizzencs

ไม่ใช่ที่ฉันรู้ คุณสามารถปรับปรุงการใช้งานได้ดีขึ้นในภาษาที่รองรับแฮชซึ่งคุณใช้สำหรับ $ ip (@ips) {$ hash {$ ip} = $ hash {$ ip} + 1; } จากนั้นเพียงพิมพ์คีย์และค่าต่างๆ
Vinko Vrsalovic

4

โซลูชัน (จัดกลุ่มตามเช่น mysql)

grep -ioh "facebook\|xing\|linkedin\|googleplus" access-log.txt | sort | uniq -c | sort -n

ผลลัพธ์

3249  googleplus
4211 linkedin
5212 xing
7928 facebook

3

คุณอาจใช้ระบบไฟล์เป็นตารางแฮช รหัสหลอกดังนี้

for every entry in the ip address file; do
  let addr denote the ip address;

  if file "addr" does not exist; then
    create file "addr";
    write a number "0" in the file;
  else 
    read the number from "addr";
    increase the number by 1 and write it back;
  fi
done

ในที่สุดสิ่งที่คุณต้องทำคือสำรวจไฟล์ทั้งหมดและพิมพ์ชื่อไฟล์และตัวเลขในไฟล์ อีกทางเลือกหนึ่งแทนการนับคุณสามารถเพิ่มช่องว่างหรือบรรทัดใหม่ในแต่ละครั้งที่ไฟล์และท้ายที่สุดเพียงแค่ดูขนาดไฟล์เป็นไบต์


3

ฉันรู้สึกว่าอาเรย์เชื่อมโยง awk นั้นมีประโยชน์ในกรณีนี้เช่นกัน

$ awk '{count[$1]++}END{for(j in count) print j,count[j]}' ips.txt

กลุ่มโดยโพสต์ที่นี่


Yepp โซลูชั่น awk ที่ยอดเยี่ยม แต่ awk ไม่สามารถใช้ได้บนเครื่องที่ฉันกำลังทำอยู่
Zizzencs

1

โซลูชันอื่น ๆ ส่วนใหญ่นับซ้ำกัน หากคุณต้องการจัดกลุ่มคู่ค่าคีย์ลองสิ่งนี้:

นี่คือข้อมูลตัวอย่างของฉัน:

find . | xargs md5sum
fe4ab8e15432161f452e345ff30c68b0 a.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt

สิ่งนี้จะพิมพ์คู่ค่าคีย์ที่จัดกลุ่มโดยการตรวจสอบ md5

cat table.txt | awk '{print $1}' | sort | uniq  | xargs -i grep {} table.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 a.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt

1

บริสุทธิ์ (ไม่มีทางแยก!)

มีวิธีคือการใช้ ฟังก์ชัน วิธีนี้เร็วมากเพราะไม่มีทางแยก! ...

... ในขณะที่กลุ่มที่อยู่ IPยังเล็กอยู่!

countIp () { 
    local -a _ips=(); local _a
    while IFS=. read -a _a ;do
        ((_ips[_a<<24|${_a[1]}<<16|${_a[2]}<<8|${_a[3]}]++))
    done
    for _a in ${!_ips[@]} ;do
        printf "%.16s %4d\n" \
          $(($_a>>24)).$(($_a>>16&255)).$(($_a>>8&255)).$(($_a&255)) ${_ips[_a]}
    done
}

หมายเหตุ: ที่อยู่ IP จะถูกแปลงเป็น 32bits ค่าจำนวนเต็มไม่ได้ลงนามใช้เป็นดัชนีสำหรับอาร์เรย์ ใช้bash arraysแบบง่ายๆไม่ใช่อาเรย์แบบเชื่อมโยง (ซึ่งมีราคาแพงกว่า)!

time countIp < ip_addresses 
10.0.10.1    3
10.0.10.2    1
10.0.10.3    1
real    0m0.001s
user    0m0.004s
sys     0m0.000s

time sort ip_addresses | uniq -c
      3 10.0.10.1
      1 10.0.10.2
      1 10.0.10.3
real    0m0.010s
user    0m0.000s
sys     0m0.000s

บนโฮสต์ของฉันการทำเช่นนั้นเร็วกว่าการใช้ส้อมมากถึงที่อยู่ประมาณ 1'000 แต่ใช้เวลาประมาณ 1 วินาทีทั้งหมดเมื่อฉันพยายามเรียงลำดับที่อยู่ 10'000 ครั้ง


0

ฉันทำแบบนี้แล้ว:

perl -e 'while (<>) {chop; $h{$_}++;} for $k (keys %h) {print "$k $h{$k}\n";}' ip_addresses

แต่ uniq อาจใช้ได้ผลสำหรับคุณ


อย่างที่ฉันบอกในโพสต์ต้นฉบับ perl ไม่ใช่ตัวเลือก ฉันรู้ว่ามันเป็นเรื่องง่ายใน perl ไม่มีปัญหากับที่ :-)
Zizzencs

0

ฉันเข้าใจว่าคุณกำลังมองหาบางอย่างใน Bash แต่ในกรณีที่มีคนอื่นกำลังมองหาบางอย่างใน Python คุณอาจต้องการพิจารณาสิ่งนี้:

mySet = set()
for line in open("ip_address_file.txt"):
     line = line.rstrip()
     mySet.add(line)

เนื่องจากค่าในชุดนั้นไม่เหมือนใครโดยค่าเริ่มต้นและ Python ค่อนข้างดีสำหรับสิ่งนี้คุณอาจได้รับรางวัลที่นี่ ฉันยังไม่ได้ทดสอบโค้ดดังนั้นจึงอาจเป็นบั๊กได้ แต่สิ่งนี้อาจช่วยคุณได้ และถ้าคุณต้องการนับเหตุการณ์ที่เกิดขึ้นการใช้ dict แทนที่จะเป็นเซตก็ง่ายที่จะนำไปใช้

แก้ไข: ฉันเป็นผู้อ่านที่มีหมัดดังนั้นฉันจึงตอบผิด นี่คือตัวอย่างข้อมูลที่มีพจน์ที่จะนับการเกิดขึ้น

mydict = {}
for line in open("ip_address_file.txt"):
    line = line.rstrip()
    if line in mydict:
        mydict[line] += 1
    else:
        mydict[line] = 1

ตอนนี้พจนานุกรม mydict ถือรายการของ IP ที่ไม่ซ้ำกันเป็นกุญแจและจำนวนครั้งที่พวกเขาเกิดขึ้นเป็นค่าของพวกเขา


มันไม่นับอะไรเลย คุณต้องใช้พจน์ที่เก็บคะแนน

Doh ขออภัยในการอ่านคำถามที่ไม่ดี ตอนแรกฉันมีเรื่องเล็ก ๆ น้อย ๆ เกี่ยวกับการใช้ dict เพื่อเก็บจำนวนครั้งที่ที่อยู่ IP แต่ละรายการเกิดขึ้น แต่ลบออกเพราะดีฉันไม่ได้อ่านคำถามมาก * พยายามที่จะตื่นขึ้นมาอย่างถูกต้อง
wzzrd

2
มีสิ่งใดบ้างitertools.groupby()ที่รวมกับsorted()ทำในสิ่งที่ OP ต้องการ
jfs

มันเป็นทางออกที่ดีในงูหลามซึ่งไม่สามารถใช้ได้สำหรับ :-) นี้
Zizzencs

-8

การเรียงอาจถูกละเว้นหากคำสั่งนั้นไม่มีนัยสำคัญ

uniq -c <source_file>

หรือ

echo "$list" | uniq -c

ถ้ารายการแหล่งที่มาเป็นตัวแปร


1
ในการชี้แจงเพิ่มเติมจากหน้า man unq: หมายเหตุ: 'uniq' ไม่ตรวจพบบรรทัดที่ซ้ำกันยกเว้นว่าอยู่ติดกัน คุณอาจต้องการเรียงลำดับอินพุตก่อนหรือใช้ 'sort -u' โดยไม่มี 'uniq'
converter42
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.