LINUX.ORG.RU
ФорумTalks

Идея проекта: аналог grep для поиска чисел

 , , ,


2

2

Привет, ЛОР!

По-моему, было бы очень полезно иметь утилиту, работающую как grep, но предназначенную для поиска чисел в пределах некоторого интервала от заданного значения. Например, если мы запускаем numgrep 1234 file.txt, то утилита вычленяет в каждой строке файла всё, что можно прочитать как число, сравнивает каждое из чисел с 1234, и если модуль разности не более 0.5, то печатает всю найденную строку. Такими числами могли бы быть, например, 1234.1, 1233.99, 1.2344e3. По умолчанию можно было бы в качестве доверительного интервала брать 0.5 от последней значащей цифры искомого числа, т.е. для 1234.5 было бы не +/-0.5, а +/-0.05, ну и, конечно, должна быть возможность задать интервал явно.

Вроде бы идея лежит на поверхности и не особо сложна в реализации (особенно если не сильно париться о скорости работы), но я не нашёл ничего готового на эту тему.

★★★★★

Последнее исправление: annulen (всего исправлений: 2)

Поставить автозамену «по-умолчанию» на правильный вариант вроде простая идея, но вот никто до сих пор не написал.

Irma ★★
()
Ответ на: комментарий от annulen

На остальные ошибки не указала из коварства и вредности.

Irma ★★
()
Ответ на: комментарий от dataman

Спасибо, поглядел. Там всё жёстко заточено на целые числа, и в парсинге строки, и в сравнении с найденных значений с пределами поиска. Получается эффективно, но для десятичных дробей и экспоненциальной записи совершенно не то.

annulen ★★★★★
() автор топика

Надо, чтоб в шестнадцатиричном виде тоже сравнивало и печатало, причём и с 0x в начале и без (с возможностью отключения шестнадцатиричного вида ключом, и даже включением ТОЛЬКО шестнадцатиричного).

CrX ★★★★★
()
Ответ на: комментарий от dataman

Тут не получится несколькими строчками обойтись, у грепа очень много всяких полезных ключей, которые периодически бывают нужны

annulen ★★★★★
() автор топика

Нашёл вот такую штуку с очень правильной задумкой: AppBase::Grep - A base for grep-like CLI utilities. Примеры использования: App::grep::url, App::grep::email.

Каждый может искать то, что ему надо: кто-то числа с плавающей точкой, кто-то шестнадцатеричные числа, кто-то ip-адреса, а код обработки «греповых» ключей командной строки (и частично их внутренностей) при этом общий (см. abgrep)

annulen ★★★★★
() автор топика

Сейчас напишу с помощью Python и ChatGPT. Жди

rtxtxtrx ★★
()
Ответ на: комментарий от annulen

Лови:

import argparse

def is_number_in_range(number, start, end):
    try:
        num = float(number)
        return start <= num <= end
    except ValueError:
        return False

def process_file(file_path, start, end):
    with open(file_path, 'r') as file:
        for line_number, line in enumerate(file, start=1):
            numbers = line.split()  # Разделяем строку на числа
            for number in numbers:
                if is_number_in_range(number, start, end):
                    print(f'Число {number} найдено на строке {line_number}')

def main():
    parser = argparse.ArgumentParser(description="Утилита для поиска чисел в диапазоне в файле")
    parser.add_argument('file', help='Путь к файлу')
    parser.add_argument('start', type=float, help='Начало диапазона')
    parser.add_argument('end', type=float, help='Конец диапазона')
    
    args = parser.parse_args()

    process_file(args.file, args.start, args.end)

if __name__ == "__main__":
    main()

А вот и на C:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int is_number_in_range(double number, double start, double end) {
    return number >= start && number <= end;
}

void process_file(const char* file_path, double start, double end) {
    FILE *file = fopen(file_path, "r");
    if (file == NULL) {
        perror("Ошибка открытия файла");
        return;
    }

    char line[1024];
    int line_number = 0;

    while (fgets(line, sizeof(line), file)) {
        line_number++;
        char *token = strtok(line, " \t\n");  // Разделение строки на токены по пробелам и табуляциям

        while (token != NULL) {
            char *endptr;
            errno = 0;
            double num = strtod(token, &endptr);

            if (endptr != token && errno == 0) {  // Проверяем, что преобразование прошло успешно
                if (is_number_in_range(num, start, end)) {
                    printf("Число %.2f найдено на строке %d\n", num, line_number);
                }
            }

            token = strtok(NULL, " \t\n");
        }
    }

    fclose(file);
}

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "Использование: %s <файл> <начало диапазона> <конец диапазона>\n", argv[0]);
        return 1;
    }

    const char* file_path = argv[1];
    double start = atof(argv[2]);
    double end = atof(argv[3]);

    process_file(file_path, start, end);

    return 0;
}

rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 1)
Ответ на: комментарий от rtxtxtrx

numbers = line.split() # Разделяем строку на числа

Нее, это уж совсем по-ламерски. Никто не говорил, что в строке присутствуют только числа, и что эти числа внутри неё окружены пробелами. Может быть любой текст, числа могут быть в скобках и соседствовать с любыми другими символами.

annulen ★★★★★
() автор топика

Сляпал примитив/прототип на коленке, наверное что-то неправильно, и будет искать не всегда то. Ну и ладно. Зато зелёненьким подсвечивает. Это я к тому что напиши сам, будет чем, вечер, день, неделю, год занять хихи :) src

#!/usr/bin/env lua

local help =
[[
Usage:  numgrep number range filename

   number    - Какое число ты пытаешься найти (например 3)
   range     - Какой диапазон смещения от этого числа допустим? (например +/- 0.5)
   filename  - Где ищем?
]]

local number = tonumber(arg[1]);
local range  = tonumber(arg[2])
local success,file   = pcall(io.open,arg[3]);

if not number then
   print(help);
   os.exit();
end
---
if not range then
   print(help);
   os.exit();
end
---
if not file then
   print(help);
   os.exit();
end

local max = number+range
local min = number-range

for line in file:lines() do
    local found = false
    for num in line:gmatch('(%d+%.%d+)') do
        num = tonumber(num)
        if num >= min and num <= max or num == number  then
           print((line:gsub('('..num..')','\x1B[32m%1\x1B[39m')))
        end
        found = true
    end
    if not found then
    for num in line:gmatch('(%d+)') do
        num = tonumber(num)
        if num >= min and num <= max or num == number  then
           print((line:gsub('('..num..')','\x1B[32m%1\x1B[39m')))
        end
    end
    end
    found = false
end

out

dron@gnu:~/Рабочий-стол/numgrep$ ./numgrep 3 0.5 test.txt 
3.5
3.1
3.2
3
3.3
3.4
dron@gnu:~/Рабочий-стол/numgrep$ 

test.txt

1
2
3.5
3.1
3.2
3
3.3
3.4
3.6
3.7
3.8
3.9
4
5.1
5.2
5.3
5.33
50.33
-5.5
6
7
8
9
10
LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от annulen

Так допиши что-нибудь типа:

if is_number_in_range(re.sub(r'\D+', '', number), start, end):

Можно еще числа регулярками искать в строке:

for number in re.findall(r'-?\d+(\.\d+)?', line):

На C я не знаю как проще всего числа выдрать, а что-то впустую у чатгпт я не спрашиваю

rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 3)
Ответ на: комментарий от dataman

Я скептически отношусь к софту, прибитому гвоздями к x86. Но каким-нибудь дата-майнерам такое может быть полезно, да.

annulen ★★★★★
() автор топика

За всю карьеру, ЕМНИП, такой необходимости или не возникало или я её не помню

Как пришли к такой задаче?

ahdenchik
()
Ответ на: комментарий от ahdenchik

В тегах наука может данные какие с масс спектрометра близкие, но где важна точность, типа 123 +/- 0.15 это ванадий, а 123.65 +/- 0.17 это титан, и ему нужно в 32648 файлах каждый из которых это данные по исследуемому образцу определить в каких образцах есть ванадий, а затем в тех что нашёл, узнать в каких нет свинца, определить нужные образцы и с бумагой пойти к научному руководителю сказать что из 32648 образцов породы, нужно проводить дальнейшие исследования для образцов №645 и № 232, а всё остальное на помойку закопать.

Ну или ещё вариант, есть архив с файлами логов, событие генерирует лог с сообщением + время в секундах. Можно взять нужную дату и посмотреть какие были события в определённой дате и времени N секунд до и N секунд после.

Навыдумывал вот :)

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от annulen

как python (для js на базе npm ) вариант возможен и несколько строк по типу

#import re
import na_full-grep-equivalent  # типо мост до всеключевого грепа

1. match на числа силами из всеключевого грепа

2. ваша приправа-фильтр с рэнж матчингом чисел 

т.е самый лобовой вариант это конвеер первое уже наличный grep для получение разобранных подходящих строк

а ваше изделие дополнительная отфильтровка приходящих «json» :)

зы: Грег Ульсон Проектирование программного обеспечения на примере инструментов на JavaScript

Software Design by Example A Tool-Based Introduction with JavaScript By Greg Wilson

https://third-bit.com/sdxpy/ <- Software Design by Example a tool-based introduction with Python

а уж если модно молодёжно ( с лагом в лет 10):

https://datascienceatthecommandline.com/

Data Science at the Command Line Obtain, Scrub, Explore, and Model Data with Unix Power Tools

qulinxao3 ★★
()

Я бы предложил пойти юникс-путём и вместо numgrep сделать утилиту, которая их входа вычленяет числа и печатает их построчно. В принципе это можно через банальный grep и сделать. А вторая утилита пускай уже эти числа сравнивает, это можно хоть на bash сделать.

vbr ★★★★
()
Последнее исправление: vbr (всего исправлений: 1)
Ответ на: комментарий от vbr

Это интересная идея, но боюсь, что плохо применимая на практике. В грепе хорошо то, что он показывает найденную строчку целиком, плюс можно добавить контекста ключами -C,-A,-B

annulen ★★★★★
() автор топика
Ответ на: комментарий от qulinxao3

Как это алгоритмически делать я понимаю. Мой вопрос был в том, не изобретаю ли я велосипед (похоже нет, иначе мне бы уже накидали ссылок на десяток тулзов на расте), и в том, как попроще сделать из этого надёжный и полнофункциональный практический инструмент (пока склоняюсь к варианту с AppBase::Grep)

А за ссылки спасибо, почитаю

annulen ★★★★★
() автор топика
Ответ на: комментарий от buddhist

Для таких задач придумали awk

Нет. Ну то есть написать однострочник для поиска числа в csv’шке легко, а для чего-то, более похожего на моё описание, awk не годится от слова совсем. В нём даже getopt из коробки нет, о чём вообще речь.

annulen ★★★★★
() автор топика
Последнее исправление: annulen (всего исправлений: 1)
Ответ на: комментарий от qulinxao3

в целом генератор отчётов ваш проект

Да я часто всякие генераторы отчётов пишу на перле и питоне, а хочется универсальный юникс-вейный инструмент иметь

annulen ★★★★★
() автор топика
Ответ на: комментарий от LINUX-ORG-RU

Почти в точку:) Только у меня положения пиков в ИК-спектрах.

А вообще такое может быть полезно при работе с любыми экспериментальными данными, и с всякими расчётами вроде квантовохимических, где значащих цифр много, а реальная точность сильно меньше.

annulen ★★★★★
() автор топика

Мне стала интересна задача, набросал портяночку :)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>

const size_t STRNUMBUF = 32;
const char NUMBERCONTENTS[] = "-.,01223456789";

typedef struct numparser_t
{
	int is_nan;
	double value;
} numparser_t;

typedef struct session_t
{
	char * fpath;
	double number;
	double threshold;
	int num_provided;
	int thresh_provided;
} session_t;

static numparser_t
try_parse_num (const char * const in_str)
{
	numparser_t retvalue = {0};
	if (in_str == NULL)
		return retvalue;

	double tmpf = atof(in_str);
	if (tmpf != 0.0)
	{
		retvalue.value = tmpf;
		return retvalue;
	}

	if (strpbrk(in_str, "0") == NULL)
	{
		retvalue.is_nan = 1;
		return retvalue;
	}

	return retvalue;
}

static void
print_help (char * whoami)
{
	puts("Usage:");
	printf("\t%s [-t <NUMBER>] [-n <NUMBER>] -f <FILEPATH>\n", whoami);
	printf("\t%s -h\n", whoami);
	puts("Where:");
	puts("\t-t:\tThreshold number");
	puts("\t-n:\tNumber to search for. Looks for '0' if not provided");
	puts("\t-f:\tFilepath to look in");
	puts("\t-h:\tPrint help (this message)");
}

static void
parse_input (session_t * config, int argc, char * argv[])
{
	config->threshold = 0.5;
	for (int i = 1; i < argc; ++i)
	{
		char * curarg = argv[i];
		size_t len = strlen(curarg);
		if (curarg[0] == '-' && len >= 2)
		{
			switch (curarg[1])
			{
				case 'f':
					if (i == argc - 1)
						goto parse_input_err_no_file;
					else
						config->fpath = argv[i + 1];
					break;
				case 'n':
					if (i == argc - 1 )
						goto parse_input_err_no_number;
					else
					{
						numparser_t num = try_parse_num(argv[i + 1]);
						if (num.is_nan != 0)
							goto parse_input_err_invalid_number;
						else
						{
							config->number = num.value;
							config->num_provided = 1;
						}
					}

					break;
				case 't':
					if (i == argc - 1 )
						goto parse_input_err_no_number;
					else
					{
						numparser_t num = try_parse_num(argv[i + 1]);
						if (num.is_nan != 0)
							goto parse_input_err_invalid_number;
						else
						{
							config->threshold = num.value;
							config->thresh_provided = 1;
						}
					}

					break;
				case 'h':
					print_help(argv[0]);
					break;
			}
		}
	}

	parse_input_err_no_file:
	if (config->fpath == NULL)
	{
		fputs("No file is provided.\n", stderr);
		exit(1);
	}
	else if (access(config->fpath, F_OK | R_OK) != 0)
	{
		fputs("Failed to access the file.\n", stderr);
		exit(1);
	}

	if (config->num_provided == 0)
		fprintf(stderr, "No number is provided, assuming '%.2f'\n", config->number);
	else
		fprintf(stderr, "Using provided number '%.2f'\n", config->number);

	if (config->thresh_provided == 0)
		fprintf(stderr, "No threshold is provided, assuming '%.2f'\n", config->threshold);
	else
		fprintf(stderr, "Using provided threshold '%.2f'\n", config->threshold);

	return;
	parse_input_err_no_number:
	{
		fputs("No number is provided for a flag that requires number argument.\n", stderr);
		exit(1);
	}

	parse_input_err_invalid_number:
	{
		fputs("Invalid number is provided for a flag that requires number argument.\n", stderr);
		exit(1);
	}
}

static void
try_parse_arr (const session_t * const config, const char * const in_str)
{
	numparser_t result = try_parse_num(in_str);
	if (result.is_nan != 0)
		return;

//	printf("Trying to compare [%s]...\n", in_str);
	if (result.value - config->threshold <= config->number && result.value + config->threshold >= config->number)
		printf("%f\n", result.value);
}

int
main (int argc, char * argv[])
{
	session_t config = {0};
	parse_input(&config, argc, argv);

	FILE * fp = fopen(config.fpath, "r");
	if (fp == NULL)
	{
		fprintf(stderr, "Unexpeced error during opening of file [%s]!\n", config.fpath);
		exit(1);
	}

	char numbuf[STRNUMBUF];
	memset(numbuf, 0, STRNUMBUF);
	int inanumber = 0;
	unsigned bufindex = 0;
	while (!feof(fp))
	{
		int curcharint = fgetc(fp);

		if (ferror(fp) != 0)
		{
			fputs("Something went terrible wrong.\n", stderr);
			fclose(fp);
			exit(1);
		}

		char curchar = curcharint;
		char curchararr[1];
		curchararr[0] = curchar;
		int couldbenum = (strpbrk(curchararr, NUMBERCONTENTS) == NULL) ? 0 : 1;
		if ((couldbenum == 0 && inanumber != 0) || (bufindex + 1 >= STRNUMBUF))
		{
			try_parse_arr(&config, numbuf);
			inanumber = 0;
			memset(numbuf, 0, STRNUMBUF);
			bufindex = 0;
		}

		if (couldbenum != 0)
		{
			inanumber = 1;
			numbuf[bufindex++] = curchar;
		}
	}

	fclose(fp);
}

Bfgeshka ★★★★★
()
Ответ на: комментарий от annulen

https://www.awk.dev/ <- это 2 издание Кернигана и Ахо(прем Тью) и примкнувшего к ним Wайнбергера

один из корней perl и есть(был) awk

если grep выставив ключи отдает не только строки с подходящими шаблонами но и может выдавать структуру строка - найденные маски то при наличии perl awk менее универсален

но и grep излишен есть же perl

однако интерфейсы e perl и grep в части ключей патернматчинга вроде как не 100% подмножество одно другому - т.е симетричная разность не равна ни a-b ни b-a поотдельности

поэтому если нужен полный grep в текущей инкарнации + «рэнж-чек нужных шаблонов» --> делай «DATA PIPE» :)

qulinxao3 ★★
()
perl -nE 'INIT { $n = shift }; for (/[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g) { say $& if abs($& - $n) < 0.5 }' 1234 filename
Jini ★★
()

Я благодаря твоей теме вспомнил, что неплохо бы было написать сканер .git папочек чтобы искать в них закомиченные пароли (в тч те, которые лошки удалили, но gc забыли запустить):

❯ gitgrep.py '(?i)\bssh\S*pass' results/git-dumps
results/git-dumps/<censored>/.git/objects/54/0ecb54fa4b220e11c0ff32a199d3c5cf078bb3:140: if ( ! @ssh2_auth_password( $this->link, $this->options['username'], $this->options['password'] ) ) {

Можешь исходники для своих каких-то целей за основу взять:

#!/usr/bin/env python
import argparse
import re
import sys
import typing
import zlib
from contextlib import suppress
from functools import partial
from pathlib import Path

print_err = partial(print, file=sys.stderr)


def is_compressed(f: typing.BinaryIO) -> bool:
    try:
        return f.read(2) in [b"\x1f\x8b", b"\x78\x9c", b"\x78\x01", b"\x78\xda"]
    finally:
        f.seek(0)


def readlines(file_path: Path) -> typing.Iterable[str]:
    try:
        with file_path.open("rb") as f:
            if is_compressed(f):
                decompressor = zlib.decompressobj()
                buffer = b""
                while chunk := f.read(4096):
                    decompressed = decompressor.decompress(chunk)
                    buffer += decompressed
                    lines = buffer.split(b"\n")
                    for line in lines[:-1]:
                        yield line.decode("utf-8", errors="ignore")
                    buffer = lines[-1]
                if buffer:
                    yield buffer.decode("utf-8", errors="ignore")
            else:
                yield from file_path.open("r", encoding="utf-8", errors="ignore")
    except Exception as e:
        print_err(f"Error reading {file_path}: {e}")


def grep(pattern: re.Pattern, file_path: Path) -> None:
    try:
        for line_number, line in enumerate(readlines(file_path), 1):
            matches = list(pattern.finditer(line))
            if not matches:
                continue
            highlighted_line = line
            for match in reversed(matches):
                start, end = match.span()
                highlighted_line = (
                    highlighted_line[:start]
                    + f"\033[91m{highlighted_line[start:end]}\033[0m"
                    + highlighted_line[end:]
                )
            print(f"{file_path}:{line_number}: {highlighted_line.strip()}")
    except Exception as e:
        print_err(f"Error reading {file_path}: {e}")


def recursive_grep(pattern: re.Pattern, directory: Path) -> None:
    for file in directory.glob("**/.git/**/*"):
        if file.is_file():
            # index нам не особо нужен
            if file.name == "index":
                continue
            grep(pattern, file)


def main(argv: typing.Sequence[str] | None = None) -> None:
    parser = argparse.ArgumentParser(
        description="Search for a regex pattern in .git files"
    )
    parser.add_argument(
        "pattern",
        type=re.compile,
        help="Regular expression pattern to search for",
    )
    parser.add_argument(
        "path",
        nargs="?",
        default=Path.cwd(),
        type=Path,
        help="Directory path to search (default: current directory)",
    )

    args = parser.parse_args(argv)

    with suppress(KeyboardInterrupt):
        recursive_grep(args.pattern, args.path)


if __name__ == "__main__":
    sys.exit(main())

rtxtxtrx ★★
()
Ответ на: комментарий от annulen

А не лучше ли просто данные нормализовать к однообразной форме?

BceM_IIpuBeT ★★☆☆☆
()
Ответ на: комментарий от LINUX-ORG-RU

Про ванадий лучше получилось. Логи видимо уже в догонку. :)

anc ★★★★★
()
Закрыто добавление комментариев для недавно зарегистрированных пользователей (со score < 50)