Search on blog:

Potokowanie (czyli piping) w Pythonie

Potokowanie

Potokowanie w Linux to przekazywanie wyników jednego programu do następnego zamiast wysyłać je na ekran.

Przykładowo w poniższym ls zamiast wyświetlać listę plików przekazuje ją do sort, która sortuje listę w odwrotnej kolejności i przekazuje do head, która wyświetla pierwszy 5 wierszy.

$ ls | sort -r | head -5

Można to jeszcze połączyć z przekierowaniem wyjścia do pliku > lub plik do wejścia <

$ sort -r < input.txt | head > output.txt

lub podać < na początku (co działa w Bash ale nie w Fish, które używam)

$ < input.txt sort -r | head > output.txt

W ten sposób z kilku małych programów można zbudować coś większego.

Potokowanie w Python

Program w Pythonie też można wykorzystać w takim potokowaniu.

Wystarczy, że pobiera on dane ze standardowego wejścia sys.stdin za pomocą sys.stdin.readline() (ewentualnie sys.stdin.read(), sys.stdin.readlines()) lub input() a wyniki przekazuje na standardowe wyjście sys.stdout za pomocą sys.stdout.writelines() (ewentualnie sys.stdout.write()) lub print().

Oprócz tego program może wymagać obsługi przerwania SIGPIPE, które jest wysyła przykładowo przez head aby poinformować, że rezygnuje z pobierania reszty danych z potoku.

Fakt, że potokownie działa z input() i print() sprawia, że program który uruchomić samodzielnie i

import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

while True:
    try:
        # pobieranie kolejnej lini (z usunięciem "\n")
        # aż do pojawienia sie EOF (End Of File)
        line = input()
    except EOFError:
        break # lub exit(0)

    # miejsce na modyfikowanie lini

    # wypisanie lini z dodanie "\n"
    print(line)

bez komentarzy

import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

while True:
    try:
        line = input()
    except EOFError:
        break # lub exit(0)

    # miejsce na modyfikowanie lini

    print(line)

Z wykorzystaniem sys.stdin.readline() wymaga sprawdzania czy linia jest pusta zamiast łapania wyjątku EOFError. Ponieważ readline() zwraca linię wraz z końcowym n więc wygodniejsze może być użycie sys.stdout.write() zamiast print(..., end='') choć sys.stdout.write() wymaga samodzielnego konwertowania elementów na string i dodawania spacji między elementami .

dlugosc = len(line)

print('Długość linii:', dlugosc, ' znaków w', line, end='')

# tak to się odbywa wewnątrz `print()`
sys.stdout.write('Długość linii:')
sys.stdout.write(' ')               # spacje między elementami
sys.stdout.write(str(dlugosc))      # ręczna zamiana na string
sys.stdout.write(' ')               # spacje między elementami
sys.stdout.write('znaków w')
sys.stdout.write(' ')               # spacje między elementami
sys.stdout.write(line)
sys.stdout.write(end)               # przejscie do mowej linii

# tak można to sobie skrócić
sys.stdout.write('Długość linii: ') # spacje między elementami
sys.stdout.write(str(dlugosc))      # ręczna zamiana na string
sys.stdout.write(' znaków w ')      # spacje między elementami
sys.stdout.write(line)              # line zawiera już `\n`

# lub nawet
sys.stdout.write(f'Długość linii: {dlugosc} znaków w {line}')

# a dla długich linii można pominąć wstawianie linii do `f''`
sys.stdout.write(f'Długość linii: {dlugosc} znaków w ')
sys.stdout.write(line)
import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

import sys

while True:
    line = sys.stdin.readline()
    if not line:
       break

    # miejsce na modyfikowanie lini

    sys.stdout.write(line)

Z wykorzystaniem samego sys.stdin' i `for nie wymaga nawet sprawdzania pustej linii.

import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

import sys

for line in sys.stdin:

    # miejsce na modyfikowanie lini

    sys.stdout.write(line)

Przykłady

Numerowanie linii - modyfikacja jest po wypisaniu linii a nie przed

import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

number = 1

while True:
    try:
        line = input()
    except EOFError:
        break

    print(number, line)

    number += 1
import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

import sys

number = 0

while True:
    line = sys.stdin.readline()
    if not line:
       break

    number += 1

    sys.stdout.write(f'{number} ')
    sys.stdout.write(line)
    #sys.stdout.write(f'{number} {line}')
import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

import sys

number = 0

for line in sys.stdin:

    number += 1

    sys.stdout.write(f'{number} ')
    sys.stdout.write(line)
    #sys.stdout.write(f'{number} {line}')

Uwagi

Uwaga: sys.stdin.readline() będzie pobierać po jednej lini (i przekazywać dalej) a sys.stdin.read(), sys.stdin.readlines() będzie czekać na całość danych co może wymagać wciśkania Ctrl+D dla potwierdzenia końca danych.

---

I tu właście mógłby być koniec wpisu.

TODO:

Rozpoznanie potokowania

sys.stdin.isatty() pozwala rozpoznać czy standardowe wejście jest przypisane do terminala (True) czy też potokowane (False).

Aby zobaczyć wynik na ekranie trzeba przekierować print() na standardowe wyjście błędu.

import sys

print('przekierowanie stdin :', not sys.stdin.isatty(),  file=sys.stderr)
print('przekierowanie stdout:', not sys.stdout.isatty(), file=sys.stderr)
$ script.py

przekierowanie stdin : False
przekierowanie stdout: False

$ ls | script.py

przekierowanie stdin : True
przekierowanie stdout: False

$ script.py | head

przekierowanie stdin : False
przekierowanie stdout: True

$ ls | script.py | head

przekierowanie stdin : True
przekierowanie stdout: True

$ script.py < /dev/urandom

przekierowanie stdin : True
przekierowanie stdout: False

$ script.py < /dev/urandom

przekierowanie stdin : False
przekierowanie stdout: True

$ script.py < /dev/urandom > /dev/null

przekierowanie stdin : True
przekierowanie stdout: True

W przypadku sprawdzania także stderr potrzeba przekierować print() do pliku i potem go wyświetlić.

import sys

fh = open('wynik.txt', 'w')
print('przekierowanie stdin :', not sys.stdin.isatty(),  file=fh)
print('przekierowanie stdout:', not sys.stdout.isatty(), file=fh)
print('przekierowanie stderr:', not sys.stderr.isatty(), file=fh)
fh.close()
$ script.py ; cat wynik.txt

przekierowanie stdin : False
przekierowanie stdout: False
przekierowanie stderr: False

$ script.py < /dev/urandom ; cat wynik.txt

przekierowanie stdin : True
przekierowanie stdout: False
przekierowanie stderr: False

$ script.py > /dev/null ; cat wynik.txt

przekierowanie stdin : False
przekierowanie stdout: True
przekierowanie stderr: False

$ script.py < /dev/urandom > /dev/null ; cat wynik.txt

przekierowanie stdin : True
przekierowanie stdout: True
przekierowanie stderr: False

W przypadku przekierowania stderr do stdout kolejność przekierowań ma znaczenie

$ script.py 2>&1 > /dev/null ; cat wynik.txt

przekierowanie stdin : False
przekierowanie stdout: True
przekierowanie stderr: False

$ script.py > /dev/null 2>&1 ; cat wynik.txt

przekierowanie stdin : False
przekierowanie stdout: True
przekierowanie stderr: True

Lub trzeba stosować &> dla przekierowania obu

$ script.py &> /dev/null ; cat wynik.txt

przekierowanie stdin : False
przekierowanie stdout: True
przekierowanie stderr: True

Parametry

Program może także przyjmować argumenty w tradycyjny sposób za pomoca sys.argv i wykorzystywać moduły takie jak standardowy argparse.

Poniższa przeróbka pozwala numerować linie od podanej wartości

import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

import sys

number = 0

if len(sys.argv) > 1:
    try:
        number = int(sys.argv[1])
    except
        sys.stderr(f'Parametr musi być liczbą całkowitą: {sys.argv[1}\n')
        exit(0)

for line in sys.stdin:

    number += 1

    sys.stdout.write(f'{number} ')
    sys.stdout.write(line)

Numeracja linii zacznie się od wartości 5

$ ls | script.py 5 | head
If you like it
Buy a Coffee