Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.5. Конвейер на разных языках программирования

Поскольку при использовании конвейера коммуникация между разными процессами осуществляется через stdout и stdin, эти процессы могут быть представлены запущенными программами, написанными на разных языках программирования. Язык реализации при этом может быть как высокоуровневым, таким как, например, Python, так и низкоуровневым, таким как C.

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

Доступную в bash утилиту cat используем для получения в stdout содержимого заданного текстового файла. Содержимое файла будет передано по конвейеру утилите scan, разные версии которой мы реализуем на языках Python и С. Эта утилита будет выполнять поиск по короткой подстроке и возвращать результат в виде строки в формате JSON (JavaScript Object Notation) [15]. Утилита stats, в свою очередь, будет реализована на языке Python и использована для преобразования результата работы утилиты scan в понятное для человека табличное представление.

Организация ввода-вывода в нашем инструменте будет иметь вид, показанный на рис. 4:

Рисунок 4. Организация ввода-вывода в средстве поиска по подстроке

Серым цветом на рис. 4 выделены утилиты, задействованные в конвейере.

Сначала воспользуемся утилитой командной строки Linux cat для получения в stdout строк из файла /var/log/syslog, содержащего сообщения о происходящих в системе событиях [3]. Также подсчитаем число строк в журнале событий:

~$ cat /var/log/syslog | tail -n 3
Jan 18 00:00:26 user-NBD-WXX9 systemd[1]: Started Make remote CUPS printers available locally.
Jan 18 00:00:26 user-NBD-WXX9 systemd[1]: dpkg-db-backup.service: Deactivated successfully.
Jan 18 00:00:26 user-NBD-WXX9 systemd[1]: Finished Daily dpkg database backup service.
~$ cat /var/log/syslog | wc -l
34031

Утилита tail используется для получения последних 3 строк из вывода cat.

1.5.1. Поиск по подстроке на языке Python

Теперь реализуем на Python утилиту scan.py для поиска по подстроке:

import sys
import json

pat = sys.argv[1]
for i, line in enumerate(sys.stdin):
  if pat in line:
      o = dict(line=i, content=line[:-1])
      print(json.dumps(o))

Эта утилита построчно читает стандартный поток ввода stdin, а функция enumerate используется для получения номера прочитанной строки. В том случае, если переданный в качестве параметра командной строки короткий шаблон содержится в прочитанной строке, утилита scan отправляет в stdout строковое представление JSON-объекта, содержащего ключи line и content. Значением для ключа line является номер найденной строки в анализируемом файле. Значением для ключа content является содержимое строки, из которого удалён символ переноса строки на новую \n.

Для проверки работы scan.py попробуем найти подстроку root в файле /var/log/syslog:

~$ cat /var/log/syslog | python scan.py root | tail -n 3
{"line": 33903, "content": "Jan 17 22:30:01 user-NBD-WXX9 CRON[35710]: (root) CMD ([ -x /etc/init.d/anacron ] && if [ ! -d /run/systemd/system ]; then /usr/sbin/invoke-rc.d anacron start >/dev/null; fi)"}
{"line": 33955, "content": "Jan 17 23:17:01 user-NBD-WXX9 CRON[35863]: (root) CMD (   cd / && run-parts --report /etc/cron.hourly)"}
{"line": 33972, "content": "Jan 17 23:30:01 user-NBD-WXX9 CRON[35890]: (root) CMD ([ -x /etc/init.d/anacron ] && if [ ! -d /run/systemd/system ]; then /usr/sbin/invoke-rc.d anacron start >/dev/null; fi)"}

При помощи стандартной утилиты time [3] легко выполнить замеры времени выполнения реализованной утилиты по результатам 100 тестовых запусков поиска подстроки root в текстовом файле /var/log/syslog:

~$ time (for i in $(seq 1 100); do (cat /var/log/syslog | python scan.py root > /dev/null); done)

real    0m2,236s
user    0m1,897s
sys     0m0,583s

Команда seq 1 100 генерирует последовательность из 100 целых чисел. Оператор > позволяет перенаправить stdout в специальный файл /dev/null, который удаляет все записанные в него данные.

Попробуем ускорить поиск подстроки в текстовом файле, формируя JSON без создания временного словаря и без использования функции dumps из модуля json стандартной библиотеки языка Python. Обновим содержимое файла scan.py:

import sys

pat = sys.argv[1] 
for i, line in enumerate(sys.stdin):
  if pat in line:
      print(f'{{"line": {i}, "content": "{line[:-1]}"}}')

Убедимся, что поведение утилиты не изменилось, и повторно измерим время её работы:

~$ cat /var/log/syslog | python scan.py root | tail -n 3
{"line": 33903, "content": "Jan 17 22:30:01 user-NBD-WXX9 CRON[35710]: (root) CMD ([ -x /etc/init.d/anacron ] && if [ ! -d /run/systemd/system ]; then /usr/sbin/invoke-rc.d anacron start >/dev/null; fi)"}
{"line": 33955, "content": "Jan 17 23:17:01 user-NBD-WXX9 CRON[35863]: (root) CMD (   cd / && run-parts --report /etc/cron.hourly)"}
{"line": 33972, "content": "Jan 17 23:30:01 user-NBD-WXX9 CRON[35890]: (root) CMD ([ -x /etc/init.d/anacron ] && if [ ! -d /run/systemd/system ]; then /usr/sbin/invoke-rc.d anacron start >/dev/null; fi)"}
~$ time (for i in $(seq 1 100); do (cat /var/log/syslog | python scan.py root > /dev/null); done)

real    0m1,668s
user    0m1,393s
sys     0m0,510s

Время выполнения утилиты scan.py снизилось, а результат выполнения не изменился.

1.5.2. Поиск по подстроке на языке C

Попробуем ускорить утилиту, реализовав на языке C сравнение строк как 64-битных целых чисел – в этом случае длина подстроки для поиска будет ограничена 8 символами, но процесс поиска должен ускориться. Создадим файл scan.c со следующим содержимым:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

#define MAX_LINE 65536

int found(char *line, uint64_t pat, uint64_t mask) {
    for (char *p = line; *p; p++) {
        if ((*((uint64_t *) p) & mask) == pat) {
            return 1;
        }
    }
    return 0;
}

int main(int argc, char **argv) {
    char line[MAX_LINE + sizeof(uint64_t)];
    assert(argc == 2);
    unsigned pat_size = strlen(argv[1]);
    assert(pat_size <= sizeof(uint64_t));
    uint64_t mask = (-1llu) >> (sizeof(uint64_t) - pat_size) * 8;
    uint64_t pat = *((uint64_t *) argv[1]) & mask;
    for (int i = 0; fgets(line, MAX_LINE, stdin) != NULL; i++) {
        if (found(line, pat, mask)) {
            line[strcspn(line, "\n")] = 0;
            printf("{\"line\": %d, \"content\": \"%s\"}\n", i, line);
        }
    }
    return 0;
}

В функции main сначала выделяется память для MAX_LINE + 8 символов и вычисляется размер шаблона для поиска – конструкция assert(pat_size <= 8) позволяет убедиться, что указанный пользователем шаблон занимает не более 8 байт, то есть включает в себя не более 8 ASCII-символов. После этого указатель на первый символ строкового шаблона, имеющий тип char*, преобразуется в указатель на беззнаковое целое, занимающее 8 байт – этот указатель имеет тип uint64_t*. На значение, полученное по указателю с типом uint64_t*, при помощи оператора побитового «и» & накладывается маска mask для того, чтобы установить равными 0 те биты, которые не относятся к указанному пользователем шаблону в том случае, если шаблон занимает меньше 8 байт.

Затем запускается цикл обработки строк, получаемых из stdin – чтение строк из стандартного ввода осуществляется при помощи стандартной функции fgets, определённой в заголовочном файле stdio.h. Функция found выполняет быстрый поиск шаблона pat в строке line по методу скользящего окна. Подстроки p, состоящие из 8 символов, сравниваются с шаблоном pat как 64-битные целые числа. В случае совпадения в stdout печатается JSON-строка, содержащая номер найденной строки и её содержимое, как и в версии утилиты, реализованной на языке Python.

Скомпилируем новую утилиту scan.c и проверим её работу:

~$ clang -O3 -o scan scan.c
~$ ls
scan  scan.c  scan.py
~$ cat /var/log/syslog | ./scan root | tail -n 3
{"line": 33903, "content": "Jan 17 22:30:01 user-NBD-WXX9 CRON[35710]: (root) CMD ([ -x /etc/init.d/anacron ] && if [ ! -d /run/systemd/system ]; then /usr/sbin/invoke-rc.d anacron start >/dev/null; fi)"}
{"line": 33955, "content": "Jan 17 23:17:01 user-NBD-WXX9 CRON[35863]: (root) CMD (   cd / && run-parts --report /etc/cron.hourly)"}
{"line": 33972, "content": "Jan 17 23:30:01 user-NBD-WXX9 CRON[35890]: (root) CMD ([ -x /etc/init.d/anacron ] && if [ ! -d /run/systemd/system ]; then /usr/sbin/invoke-rc.d anacron start >/dev/null; fi)"}
$ time (for i in $(seq 1 100); do (cat /var/log/syslog | ./scan root > /dev/null); done)

real    0m0,543s
user    0m0,452s
sys     0m0,321s

Время выполнения утилиты scan.c, реализованной на языке C, в 4 раза ниже, чем время выполнения первой реализации scan.py на языке Python, и в 3 раза ниже, чем время выполнения ускоренной реализации scan.py на языке Python.

1.5.3. Вывод статистики на языке Python

Полученную последовательность найденных строк в формате JSON легко преобразовать в понятное человеку представление при помощи стороннего модуля для Python tabulate, перед использованием модуль tabulate необходимо установить из реестра пакетов PyPI (Python Package Index) [16].

Реализуем утилиту stats.py следующим образом:

import sys
import json
from tabulate import tabulate

print(tabulate([json.loads(line).values() for line in sys.stdin],
               headers=['Строка', 'Содержимое'],
               maxcolwidths=[None, 50]))

Утилита stats.py читает все строки в формате JSON из stdin, извлекает из JSON-объектов значения, и передаёт на вход функции tabulate сформированный список, содержащий вложенные списки значений. После этого сформированная модулем tabulate [16] таблица выводится в stdout:

~$ cat /var/log/syslog | ./scan root | tail -n 3 | python stats.py
  Строка  Содержимое
--------  -------------------------------------------------
   33903  Jan 17 22:30:01 user-NBD-WXX9 CRON[35710]: (root)
          CMD ([ -x /etc/init.d/anacron ] && if [ ! -d
          /run/systemd/system ]; then /usr/sbin/invoke-rc.d
          anacron start >/dev/null; fi)
   33955  Jan 17 23:17:01 user-NBD-WXX9 CRON[35863]: (root)
          CMD (   cd / && run-parts --report
          /etc/cron.hourly)
   33972  Jan 17 23:30:01 user-NBD-WXX9 CRON[35890]: (root)
          CMD ([ -x /etc/init.d/anacron ] && if [ ! -d
          /run/systemd/system ]; then /usr/sbin/invoke-rc.d
          anacron start >/dev/null; fi)

1.5.4. Упражнения

Задача 1. Что будет, если в обрабатываемой с помощью scan строке появится символ двойной кавычки? Исправьте соответствующим образом код.

Задача 2. Замените в конвейере формат JSON на CSV (Comma-Separated Values). Заголовки колонок должны передаваться программе как параметры командной строки.

Задача 3. Сравните производительность scan с вариантом реализации, использующим стандартную функцию strstr.

Задача 4. Почему grep оказывается быстрее scan? Улучшите код scan с использованием POSIX-функции read, чтобы приблизиться к показателям grep.

Задача 5. Реализуйте scan на языке, отличном от Python и C. Составьте таблицу с оценками производительности всех полученных вариантов scan.