Привет, хабр! История началась банально: скачал датасет для нейронки по Computer Vision. Описание обещало 50k изображений .png с разметкой в .txt. Скачал — разочарование.

Полез искать .txt файлы с описанием объектов на .png. tree показал 20 тысяч дублирующихся objects.txt, labels.txt подряд в каждом втором каталоге. Экран кончился на 300-й строке, структуру не видно.

Проблема в деталях

text$ tree dataset/
├── images/
│   ├── img001.png
│   └── img002.png
├── labels/           │   ├── objects.txt    <- 20k раз подряд
│   ├── labels.txt     <- 20k раз подряд  │   └── ...
└── annotations/
    └── objects.txt    <- ещё 10k

$ tree dataset/ ├── images/ │ ├── img001.png │ └── img002.png ├── labels/ │ ├── objects.txt <- 20k раз подряд │ ├── labels.txt <- 20k раз подряд │ └── ... └── annotations/ └── objects.txt <- ещё 10k

Командная строка умерла — не помещается 20k файлов. grep *.txt ломает отступы tree. Нужен фильтр, который покажет структуру + только первое упоминание каждого типа файла.

Решение: 40 строк C++

cpp#include <cstdio>
#include <iostream>
#include <regex>
#include <string>

std::regex wildcardToRegex(const std::string& mask) {
  std::string regexStr = "^";
  for (char c : mask) {
    if (c == '*') regexStr += ".*";
    else if (c == '?') regexStr += ".";
    else if (c == '.' || c == '+' || c == '(' || c == ')') {
      regexStr += '\\'; regexStr += c;
    } else regexStr += c;
  }
  return std::regex(regexStr + "$");
}

int main(int argc, char* argv[]) {
  std::string mask = argv[1];
  std::regex pattern = wildcardToRegex(mask);
  std::cout << "Маска: " << mask << "\n\n";
    std::string s;
  bool skip = false;
    while (std::getline(std::cin, s)) {
    if (std::regex_match(s, pattern)) {
      if (skip) continue;  // пропускаем дубли подряд
      skip = true;
    } else {
      skip = false;
    }
    printf("%s\n", s.c_str());  // ANSI цвета tree живы!
  }
}

Компиляция (секунда):

bashg++ -O2 -o treefilter treefilter.cpp
sudo cp treefilter /usr/local/bin/

g++ -O2 -o treefilter treefilter.cpp sudo cp treefilter /usr/local/bin/

Как пользоваться для датасетов

text# Только разметка
tree dataset/ | treefilter "*.txt"

# Изображения
tree dataset/ | treefilter "*.png|*.jpg"

# JSON аннотации
tree dataset/ | treefilter "*.json"

# Глубже в поддиректории
tree -L 3 dataset/ | treefilter "labels/*.txt"

Результат — магия!

textdataset/
├── images/
│   ├── *.png          <- только первое упоминание
├── labels/            │   ├── objects.txt    <- только первое
│   └── labels.txt     <- только первое
└── annotations/
    └── *.json         <- читаемо!

dataset/ ├── images/ │ ├── .png <- только первое упоминание ├── labels/ │ ├── objects.txt <- только первое │ └── labels.txt <- только первое └── annotations/ └── .json <- читаемо!

Зачем это круто для ML/Data Science

  • Читаемая структура датасетов на 100GB+

  • 0.1 сек на гигабайт вывода (C++ speed™)

  • Универсальный — работает с find, ls, любым выводом

  • ANSI-цвета tree сохраняются

Бонус: умная дедупликация

Раскомментируй в коде — ловит похожие имена с конца строки:

cppsize_t others = 0;
for (size_t i = 0; i < std::min(s.size(), last_line.size()); ++i) {
  if (s[s.size() - i - 1] == last_line[last_line.size() - i - 1]) ++others;
}
if (others * 2 >= s.size()) continue;  // file1.txt, file2.txt = дубли

Планы на развитие

Потихоньку добавлю фичи:

  • Счетчик совпадающих строк: objects.txt [x15000]

  • Группировка по директориям

  • Интерактивный режим+/- для папок

  • Конфиг масок для типичных датасетов (COCO, YOLO, VOC)

  • JSON/YAML вывод структуры

Студенческий лайфхак №47: датасет не помещается на экране? Пиши фильтр за вечер. Теперь tree dataset/ -C | treefilter "*.txt" — и вся структура как на ладони! 🚀 https://github.com/aleksejbiriulin/tree_group