План этого занятия

  • Пакетная обработка файлов
  • Создание каталогов
  • Связывание данных
  • Работа с Git

Для работы нам потребуется архив с файлами древнерусского корпуса в формате CONLL-U(D). CONLL-формат – это почти то же самое, что знакомый нам TSV, то есть по сути табличка с табуляциями-разделителями.

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

В каждом большом файле, которые у нас есть, имеется строка вида # newdoc id = texts/afz3/AFZ_3_2_030 Она означает, что всё, что ниже этой строки до следующей аналогичной, должно быть выделено в отдельный файл, который называется AFZ_3_2_030.txt, а файл этот должен лежать в директории afz3, которая в свою очередь должна быть вложена в директорию texts.

Первая часть задания: файлы и папки

Таким образом, нужно написать программу, которая:

  1. По очереди открывает большие файлы,
  2. автоматически создает директории с нужными названиями,
  3. кладёт в них маленькие файлы (появившися в результате разрезания больших).

Вторая часть задания: Git

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

Для этого нужно создать новый публичный репозиторий на Gitub, склонировать его на локальный компьютер, с помощью файлового менеджера положить в него код и папку texts с получившимися маленькими файлами, сделать git add, git commit и git push, убедиться в том, что результаты работы видны в веб-интерфейсе гитхаба.

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

Грузить исходные большие файлы в репозиторий не нужно. Некоторые из них настолько большие, что гитхаб может запретить их загрузку. Поэтому убедитесь, что их нет в репозитории. А лучше воспользоваться запретом на индексацию папки, где лежат большие файлы. Для этого есть .gitignore, см. подробнее.

Третья часть задания: связывание данных

Заниматься этой частью задания имеет смысл только если вы справились с первой и второй.

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

Здесь лежит словарь древнерусских глаголов, устроенный как тот же TSV: это таблица с табуляцией-разделителем. Во второй колонке там находится словоформа, а в третьей - соответствующая ей лемма древнерусского глагола.

Таким образом, нужно написать программу, которая:

  1. Загружает словарь в виде питоновского словаря вида “словоформа: лемма”,
  2. Обходит все маленькие файлы из первого задания,
  3. Добавляет к CONLL новую колонку (она будет последней),
  4. “Просматривает” все словоформы на наличие словоформ из словаря.
  5. В случае “попадания” добавляет в новую колонку информацию о лемме; если словоформа в словаре не нашлась, тогда поле остаётся пустым (но табуляция перед этим пустым значением всё равно должна быть!)

Если результат получился, его тоже нужно закоммитить и запушить в Git-репозиторий.

Примеры решения

Первое задание: вариант 1

#!/usr/bin/env python3

import os


# Местоположение распакованного архива с исходными файлами
LARGE_DIR = 'mid_rus_conll'
# Папку, куда будет сложен результат разбиения
SMALL_DIR = 'mid_rus_small'


def extract_small_fpath(line):
    """Возвращает путь до «маленького» файла по строке «# newdoc id = ...»"""
    line = line.strip()
    fpath_part = line.split(' = ')[1]
    fpath = os.path.join(SMALL_DIR, fpath_part) + '.txt'
    return fpath


def write_small_file(fpath, lines):
    """Записывает список строк lines в fpath, создавая необходимые папки"""
    fdir = os.path.dirname(fpath)
    os.makedirs(fdir, exist_ok=True)
    with open(fpath, 'w', encoding='utf-8') as fh:
        fh.writelines(lines)


def split_file(large_fpath):
    """Принимает путь до большого файла и разбивает его на маленькие
    
    Эта реализация разбиения не использует регулярные выражения и сканирует
    «большой» файл построчно. Такой подход хорош, если входные данные слишком
    большие и не влезают в оперативную память. Принцип работы следующий:
    строки исходного файла сохраняются в список small_file_lines до тех пор,
    пока не встретится строка с «newdoc id», в этом случае текущий
    small_file_lines записывается в файл с именем small_fpath, известным до
    этого. Затем small_fpath заменяется на название нового файла, а
    small_file_lines заменяется на пустой список. При таком подходе нужно не
    забыть проверить, что мы встретили строку с «newdoc id» в первый раз, и
    не забыть записать найденное содержимое после конца чтения файла
    """
    small_fpath = None
    with open(large_fpath, encoding='utf-8') as fh:
        for line in fh:
            if line.startswith('# newdoc id = '):
                if small_fpath is not None:
                    write_small_file(small_fpath, small_file_lines)
                small_fpath = extract_small_fpath(line)
                small_file_lines = []
            else:
                small_file_lines.append(line)
    write_small_file(small_fpath, small_file_lines)


def main():
    # Создаём папку для результата, если её ещё нет
    os.makedirs(SMALL_DIR, exist_ok=True)
    # Перебираем исходные файлы и разбиваем их по очереди
    for fname in os.listdir(LARGE_DIR):
        # На всякий случай проверяем, нет ли «лишних» файлов или папок
        if not fname.endswith('.txt'):
            continue
        fpath = os.path.join(LARGE_DIR, fname)
        split_file(fpath)


# Один из общепринятых способов запуска скриптов на питоне
if __name__ == '__main__':
    main()

Первое задание: вариант 1.1

Функцию split_file() из предыдущего варианта можно заменить следующей функцией, использующей re.findall для разбиения исходного файла. Такой вариант короче, но требует внимательного написания регулярного выражения. В качестве награды за такие усилия, программа с данной функцией работает в два раза быстрее (на моём ноутбуке).

def split_file(large_fpath):
    """Принимает путь до большого файла и разбивает его на маленькие
    
    Данный вариант функции написан так, чтобы можно было его вставить в
    предыдущую программу без изменений. Он мог бы быть чище, при некотором
    изменении других функций.

    Разбор регулярного выражения:
        '# newdoc id = (\S+)\n' - находит строку с началом новой части файла
            и запоминает необходимую часть путь. Вместо '\S' можно
            использовать любое другое подходящее выражение

        '((?:(?!# newdoc id).*\n)+)' - запоминает строки, находящиеся внутри
            части файла.
        '(?!# newdoc id)' - указывает, что в этой части регулярного выражения
            не должна встречаться строка '# newdoc id'
        '(?!# newdoc id).*\n' - строка вместе с переносом строки, в
            которой не встречается '# newdoc id'
        '(?:(?!# newdoc id).*\n)+' - несколько подряд идущих строк, в
            которых не встречается '# newdoc id'. Вместо обычных скобок 
            использовано выражение (?:...), которое не запоминает содержимое,
            а только группирует его.
    """
    # Лучше импортировать новые модули в начале файла, но можно и так
    import re

    with open(large_fpath, 'encoding='utf-8') as fh:
        large_text = fh.read()
    # Список из найденных пар (кортежей из двух элементов): пути маленького
    # файла и его содержимого
    parts = re.findall(
        r'# newdoc id = (\S+)\n((?:(?!# newdoc id).*\n)+)',
        large_text
    )
    for fpath, text in parts:
        # Путь до файла создадим прямо тут, в этом варианте не используется
        # extract_small_fpath()
        fpath = os.path.join(SMALL_DIR, fpath) + '.txt'
        # text в массиве, потому что функция write_small_file() принимает
        # список записываемых строк
        write_small_file(fpath, [text]) 


Улучшить эту страницу