Как сделать, чтобы строки таблицы PrettyTable были разделены линиями, но без нарушения нумерации строк?

По заданию нужно отсортировать входящий CSV файл и вывести в виде таблички заданные пользователем строки и столбцы при помощи prettytable.

Задание

  • Нужно выводить все данные в виде таблички PrettyTable
  • Каждую строку необходимо пронумеровать
  • В табличке нужно выводить границы вокруг всех ячеек
  • Для текста во всех ячейках таблицы с количеством превышающем 100 символов, сделать обрезку до 100 символов и добавить троеточие в конце "..."
  • Ширину столбцов во всех ячейках сделать в максимальные 20 знакомест
  • В случае отсутствия записей в CSV-файле должна выводится подпись: "Нет данных"
  • Первая строка входных данных - название CSV с вакансиями для обработки
  • Вторая строка (два числа разделенных пробелом) - порядковые номера вакансий с какой по какую выводить в таблицу: 20 30 - выводить с 20 по 30 вакансию - 20 первая вакансия 30 последняя вакансия. Если указано одно число, это стартовый номер с которого выводить и до конца таблицы: 20 - выводить с 20 вакансии и до конца таблицы. Если не указано ни одного значения (пустая строка) - выводить всю таблицу.
  • Третья строка (Заголовки столбцов в таблице разделенные ̈ ,̈ (запятая пробел)) - Названия столбцов, которые нужно выводить в таблице: Название, Опыт работы, Оклад - выводит только указанные столбцы. Если строка пустая, выводить все столбцы.

Первые 5 строк CSV файла:

Название,Описание,Навыки,Опыт работы,Премиум-вакансия,Компания,Оклад,Название региона,Дата публикации вакансии
Web-программист,"NEW! Web-программист(без опыта и с опытом)   Обязанности: - верстка сайтов на основе готовых макетов PHP, HTML и CSS - подключение собственной системы управления(обучение) - развитие собственного движка и системы управления - установка движка организации на проектах (обучение) Требования:  PHP7.4+ (знания ООП и паттернов проектирования) MySQL(MariaDB,PostgresQL) CSS(LESS,SASS) JS(TypeScript,NodeJS) знание основ Figma / Photoshop(для верстки с макетов из PSD) остальное при собеседовании.  Условия: - работа с офисе на Московское шоссе (удаленка после испытательного срока) - 5 дней с 9-00 до 18-00 (после вхождения в работу график может быть плавающим) - чай кофе печеньки - дружный коллектив - отпуск ежегодный оплачиваемый   В целом рассматриваем как с опытом работы так и без опыта. Если человек готов учиться и развиваться, он предан и любит программировать и верстать сайты мы его научим и подготовим за счет компании.   Испытательный срок от 2 до 4 недель(при собеседовании) Зарплата 2 раза в месяц: аванс и оклад. Оклад фиксированный + % от объема выполненных задач за месяц.","Управление проектами, Ведение переговоров, Разработка ПО, Разработка технических заданий, Управление интернет-проектами, Разработка концепции, Информационные технологии, Организаторские навыки, Проведение презентаций, Обучение персонала, Английский язык, Работа в команде, Подбор персонала",Нет опыта,Нет,Асташенков Г. А.,30 000 - 80 000 (Рубли) (С вычетом налогов),Ульяновск,17:32:31 31/05/2022
Специалист в отдел информационного развития (Junior),"Обязанности:   Работа с конфигурацией 1С Предприятие 8.3;   Создание отчетов в 1С;   Прямое общение с пользователями для формирования ТЗ;   Прямое общение с разработчиками для объяснения ТЗ;   Администрирование 1С, контроль производительности, оптимизация работы;   Обнаружение и исправление сбоев 1С, СУБД;   Консультация и поддержка пользователей   Требования:   Знание основ бухгалтерского, финансового, налогового, кадрового учета;   Опыт работы с типовыми конфигурациями 1С Предприятие 8.3;   Опыт администрирования СУБД;   Знание одной из конфигураций: ERP, КА;   Аналитический склад ума;   Ответственность, коммуникабельность   Будет плюсом:   Сертификат аттестации 1C   Знание конфигураций: ERP, КА   Условия:  Официальное оформление с полным соблюдением ТК РФ; Рабочий день с 09-00 до 17-30 по графику 5/2; Оборудованное рабочее место - производительный компьютер, телефон, корпоративная мобильная связь; Возможность повышения квалификации, обучение за счет компании; Выплаты з/п 2 раза в месяц, без задержек; З/п от 45 тысяч рублей по результатам собеседования","1С: Предприятие 8, ERP-системы на базе 1С, 1С программирование, Создание конфигурации 1С",От 1 года до 3 лет,Нет,Стройатомсфера,45 000 - 60 000 (Рубли) (С вычетом налогов),Воронеж,17:32:41 31/05/2022
Инженер по нагрузочному тестированию,"Вам предстоит:  Работать в составе команды опытных инженеров нагрузочного тестирования (будет daily meeting), которая занимается тестированием модельных подсистем в составе большой микросервисной АС. Разрабатывать и актуализировать средства автоматизированного нагрузочного тестирования. Скрипты, заглушки, инструменты тестирования и обработки данных. Тестировать компоненты ПО в соответствии с планом работ релизного цикла и требованиями методик испытания. Документировать ход тестирования (confluence) и регистрировать дефекты (Jira). Дорабатывать и изменять методики испытаний исходя из требований и выявленных дефектов (confluence). Автоматизировать типовые задачи разработки или тестирования по своему направлению. Jenkins CI/CL. Общаться с продуктовой командой разработки приложения по найденным дефектам производительности, тонкостям работы application, эмуляции бизнес-процессов.    Требования:    Опыт выполнения проекта по нагрузочному тестированию от 1 года (разработка скриптов, эмуляторов, утилит генерации тестовых данных).   Знание одного из языков программирования JAVA, C#, Perl, Python, C++ на уровне не ниже среднего. Опыт работы с одной из СУБД: Oracle, MSSQL. Опыт работы с ОС Unix (HP-UX, AIX), Windows. Опыт работы с технологиями/протоколами: WEB, SOAP, REST, JSON, XML Опыт работы c git Опыт работы с Jira, Confluence Знание SQL на уровне DML/DDL Знание основ регулярных выражений Perl Опыт работы (на уровне анализа информации на преднастроенной панели данных) с инструментами мониторинга (Grafana, Zabbix) Желателен опыт работы (анализ, добавление, поиск, чтение данных в базе) с Influx DB. MS Word, Excel на уровне продвинутого пользователя. Знание продуктов MF Performance Center, MF Load Runner.  Будет плюсом:  Опыт в IT подразделениях банковской сферы. Знание методологий тестирования  Мы предлагаем:  Возможность работать в офисе или по смешанному формату (офис/удаленка); Быструю адаптацию благодаря программам обучения; Команду Agile, готовую поддержать ваши инициативы; Прозрачную систему бонусов и премий; Мощное железо; Доступ к базе знаний Корпоративного университета; Масштабные и высоконагруженные проекты с использованием новых технологий;; Трудоустройство согласно трудовому кодексу; Отсутствие строгого дресс-кода Нематериальная мотивация (ДМС доступен с первого дня работы, Бесплатная подписка СберПрайм+; Льготные ставки по ипотеке «ДомКлик»; Льготные ставки по потребительским кредитам; Бесплатный тренажерный зал)","Java, SQL, Git, Python, Atlassian Jira, C/C++",От 1 года до 3 лет,Нет,СБЕР,70 000 - 150 000 (Рубли) (С вычетом налогов),Санкт-Петербург,17:32:49 31/05/2022
Стажер бизнес-аналитик,"Чем предстоит заниматься?  Подготовка описания задач; Выявление и детализация требований от бизнес-заказчиков; Участие во встречах с бизнесом, технической командой и другими заинтересованными сторонами; Участие в разработке и оценке решения по задаче; Консультирование команды задачи во время тестирования.  Мы ожидаем от Вас:  В том числе выпускник/студент последнего года обучения экономического или технического ВУЗов; Аналитические способности, умение структурировать информацию; Знания основ современных ИТ-технологий; Хорошие коммуникативные навыки.  Своему будущему Юникуму мы готовы предложить:   Начало стажировки - 1июля;   Срок окончания - 28 декабря;   Стажировка оплачиваемая: 50 000 руб. до вычета налогов (при условии полной занятости);   Гибкий график для тех, кто учится;   Оформление в штат Банка;   ДМС и бесплатные обеды (при работе в офисе);   Тренинги по развитию софт скиллс;   Возможность продолжить карьеру в рамках крупнейшего международного Банка.","Техническое обслуживание, Настройка ПК, Ремонт ПК, Настройка сетевых подключений, Настройка ПО",Нет опыта,Нет,UniCredit Bank,70 000 - 150 000 (Рубли) (С вычетом налогов),Москва,17:32:50 31/05/2022
Middle system analyst / системный аналитик (SA),"Cotvec - IT-компания, которая занимается консалтингом и разработкой программного обеспечения для организаций банковско-финансового сектора. Мы гордимся нашей молодой, сплоченной и профессиональной командой, которая с каждым днем повышает свои компетенции, генерирует яркие идеи и находит эффективные пути реализации даже самых нестандартных задач. Мы ищем middle/middle+ system analyst Обязанности: - Сбор, формализация, согласование, оценка (трудозатраты) требований к функционалу разрабатываемой системы;- Формирование требований к интеграционному взаимодействию системы;- Описание ожидаемого поведения функционала разрабатываемой системы;- Постановка задач на разработку, поддержка разработки;- Участие в проведении тестирования разработанного функционала,- Сдача работы заказчику, проведение демонстраций функционала;- Разработка проектной и сопроводительной системной документации в Jira/Confluence, в т.ч. детальных схем процессов и алгоритмов;- Коммуникация с другими командами разработки, аналитиками. Необходимые навыки: - Понимание принципов разработки ПО: этапы и участие в них аналитика, клиент - серверное взаимодействие, особенности работы веб и мобильных приложений;- Знания/практический опыт работы с UML (диаграмма баз данных, диаграмма состояний и переходов, диаграмма последовательности и др.).- Знание/практический опыт работы с SQL- Опыт постановки задач разработчикам.- Понимание технологий интеграции REST, SOAP и форматов обмена данными XML/XSD, JSON/JSON-Schema.- Навыки работы с нотациями (BPMN, IDEF0 и др.).- Навыки работы с инструментами прототипирования и проектирования пользовательского интерфейса (Figma, Axure RP).Будет плюсом:- Опыт проектирования БД (SQL/NoSQL), опыт написания простых/средней сложности запросов в БД.- Опыт участия в проектирования архитектурных решений и описания интеграционных взаимодействий Условия: - Для сотрудников компании предусмотрена компенсация медицинского обслуживания в рамках программы Добровольного медицинского страхования;- Заработная плата привязана к курсу доллара (по уровню заработной платы отталкиваемся от пожеланий кандидата);- Возможность работать по гибридному графику работы или удалённо (обсуждается с руководителем);- 29 календарных дней трудового отпуска;- 100% оплата больничного с первого дня;- Уютный офис с оборудованным рабочим местом, кухней и зоной отдыха;- Материальные выплаты к памятным датам;- Внешнее и внутреннее корпоративное обучение;- Корпоративная программа лояльности;- Корпоративная библиотека;- Корпоративные мероприятия, тимбилдинги.","UML, XML, SQL, Atlassian Jira, SOAP, REST, Xsd, JSON API",От 1 года до 3 лет,Нет,Котвек,70 000 - 150 000 (Рубли) (С вычетом налогов),Минск,17:33:08 27/06/2022

В целом, я просто считываю входные данные и из них вывожу только конкретные строки и столбцы согласно заданию. У меня вроде всё хорошо с сортировкой, но почему-то между строками нет разделительной линии. Мне кажется, что дело только в функции create_table.

Подскажите, пожалуйста, как это исправить (см. ниже код и ошибку). По совету нейросети пробовал добавить hrules = 1 - линии появились, но меняется номер строки, а сама строка обрезается без дополнительной пустой строки, как в ожидаемом ответе.

import csv
from prettytable import PrettyTable
from typing import List, Tuple, Dict


def csv_reader(file_name: str) -> Tuple[List[str], List[List[str]]]:
    with open(file_name, 'r', encoding='utf-8-sig') as file:
        reader = csv.reader(file)
        headers = next(reader)
        data = list(reader)
    return headers, data


file_name = input()
data = csv_reader(file_name)


def parse_range(range_string):
    range_string = range_string.strip()
    if not range_string:
        return None, None

    parts = range_string.split()
    if len(parts) == 1:
        return int(parts[0]), None
    elif len(parts) == 2:
        return int(parts[0]), int(parts[1])
    return None, None


range_string = input()
f,t = parse_range(range_string)


def parse_columns(string):
    if string == "":
        return None
    headers=string.split(',')
    return list(map(lambda x: x.strip(),headers))


columns = input()
headers = parse_columns(columns)


def filter_rows(data,start,end):
    if start == None and end == None:
        return data
    elif end == None:
        return data[start-1:]
    else:
        return data[start-1:end]


rows = filter_rows(data[1],f,t)


def filter_columns(data,headers):
    if not headers:
        return data
    else:
        return headers


columns = filter_columns(data[0],headers)


def truncate_text(text, max_length=100):
    if len(str(text)) <= max_length:
        return str(text)
    return str(text)[:max_length] + "..."


def create_table(data, allcolumns, columns):
    if not data:
        return None
    if not columns:
        columns = allcolumns

    table = PrettyTable()
    indices = [allcolumns.index(col) for col in columns if col in allcolumns]
    table.field_names = ["№"] + columns

    for idx, row in enumerate(data, 1):
        truncated_row = [truncate_text(row[i]) for i in indices]
        table.add_row([idx] + truncated_row)

    table.align = 'l'
    table.max_width = 20
    return table


table = create_table(rows, data[0], headers)
print(table) 
Тест 2

Разное количество строк: out = 17, correct = 18
Строка 11 отличается: out: 
>| 2 | Оператор станков с   | Обязанности:         | Работа в команде,    | От 1 года до 3 лет | Нет              | Опытное              | 30 000 - 50 000      | Нижний Новгород  | 17:09:02 31/05/2022      |< 
correct: 
>+---+----------------------+----------------------+----------------------+--------------------+------------------+----------------------+----------------------+------------------+--------------------------+<


╭─────────────────────────────────┨OUT RANGE 6 - 15┠────────────────────────────────╮
│|   | ситуационного центра | радиовещания         | Internet Explorer,   |         │
│|   |                      | мониторинг интернет- | MS Outlook,          |         │
│|   |                      | сайтов, электронной  | Исполнительность,    |         │
│|   |                      | почты работа с...    | Внимательность,      |         │
│|   |                      |                      | Отв...               |         │
│| 2 | Оператор станков с   | Обязанности:         | Работа в команде,    | От 1 год│
│|   | ПУ                   | Ведение процесса     | Чтение чертежей,     |         │
│|   |                      | обработки с пульта   | Internet,            |         │
│|   |                      | управления на        | Электронная почта    |         │
│|   |                      | налаженных станках с |                      |         │
╰───────────────────────────────────────────────────────────────────────────────────╯

╭───────────────────────────────┨CORRECT RANGE 6 - 15┠──────────────────────────────╮
│|   | ситуационного центра | радиовещания         | Internet Explorer,   |         │
│|   |                      | мониторинг интернет- | MS Outlook,          |         │
│|   |                      | сайтов, электронной  | Исполнительность,    |         │
│|   |                      | почты работа с...    | Внимательность,      |         │
│|   |                      |                      | Отв...               |         │
│+---+----------------------+----------------------+----------------------+---------│
│| 2 | Оператор станков с   | Обязанности:         | Работа в команде,    | От 1 год│
│|   | ПУ                   | Ведение процесса     | Чтение чертежей,     |         │
│|   |                      | обработки с пульта   | Internet,            |         │
│|   |                      | управления на        | Электронная почта    |         │
╰───────────────────────────────────────────────────────────────────────────────────╯
Неправильный ответ

Вот вывод если добавить hrules=1 в create_table:

Тест 5   

Разное количество строк: out = 8, correct = 10
Строка 4 отличается: out: 
>| 1 | Оператор станков с   | Работа в команде,    | Нет              | Нижний Новгород  |< 
correct: 
>| 2 | Оператор станков с   | Работа в команде,    | Нет              | Нижний Новгород  |<


╭─────────────────────────────────┨OUT RANGE 1 - 8┠─────────────────────────────────╮
│+---+----------------------+----------------------+------------------+-------------│
│| № | Название             | Навыки               | Премиум-вакансия | Название рег│
│+---+----------------------+----------------------+------------------+-------------│
│| 1 | Оператор станков с   | Работа в команде,    | Нет              | Нижний Новго│
│|   | ПУ                   | Чтение чертежей,     |                  |             │
│|   |                      | Internet,            |                  |             │
│|   |                      | Электронная почта    |                  |             │
│+---+----------------------+----------------------+------------------+-------------│
╰───────────────────────────────────────────────────────────────────────────────────╯

╭───────────────────────────────┨CORRECT RANGE 1 - 8┠───────────────────────────────╮
│+---+----------------------+----------------------+------------------+-------------│
│| № | Название             | Навыки               | Премиум-вакансия | Название рег│
│+---+----------------------+----------------------+------------------+-------------│
│| 2 | Оператор станков с   | Работа в команде,    | Нет              | Нижний Новго│
│|   | ПУ                   | Чтение чертежей,     |                  |             │
│|   |                      | Internet,            |                  |             │
│|   |                      | Электронная почта    |                  |             │
│|   |                      |                      |                  |             │
╰───────────────────────────────────────────────────────────────────────────────────╯
Неправильный ответ

Ответы (2 шт):

Автор решения: Николай Зызда

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

→ Ссылка
Автор решения: Vitalizzare

prettytable 3.16.0

Исправление исходного кода

Ваш код в целом рабочий. Чтобы привести результат к ожидаемому виду, нужно:

  • Установить параметр table.hrules = prettytable.HRuleStyle.ALL, где HRuleStyle.ALL - это элемент перечисления Enum с порядковым номером 1, задающий печать границ между ячейками. Конечно, можно ограничиться hrules = 1, но поскольку код мы больше читаем, а не пишем, то вместо 1 лучше поставить HRuleStyle.ALL.

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

Например:

# Добавляем параметр start для передачи начала нумерации строк
def create_table(data, allcolumns, columns, start=1):
    ...
    # используем start при нумерации формируемых строк
    for idx, row in enumerate(data, start):
        ...
    ...
    return table

# При вызове create_table дополнительно передаем start=f,
# где f - заданная строка, с которой начинает вывод таблицы
start = 1 if f is None else f
table = create_table(rows, data[0], headers, start)

Что касается отсутствия пустых строк внизу ячеек, то здесь явного решения нет. Возможно, это встроенное поведения одной из версий prettytable, или же это особенность тестовых данных проверяющего движка учебной платформы. Если второе, то я бы предположил, что от вас ожидают использования встроенных возможностей prettytable по выдаче на печать ограниченного количество строк и столбцов. В текущей версии prettytable 3.16.0, при вычислении высоты строки принимаются в расчет все столбцы. Как результат, когда вы печатаете ограниченный набор столбцов, задав параметр table.fields = [ ... ], их высота будет зависеть от содержимого скрытых ячеек.

Используем возможности PrettyTable

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

import prettytable   # version 3.16.0 


# Параметры по умолчанию
MAX_LEN = 100    # ограничение на длину текста в ячейке
MAX_WIDTH = 20   # ограничение на ширину столбца
ALIGN = 'l'      # выравниваем по левому краю
INDEX = '№'      # имя поля индекса

# Пользовательские настройки
filename = 'vacancies.csv'   # путь к файлу vacancies.csv
start, end = 20, 30          # номер первой и последней печатаемой строки
fields = ['Название', 'Опыт работы', 'Оклад']   # печатаемые столбцы


def get_long_fields(table: PrettyTable, limit: int = MAX_LEN) -> list[str]:
    '''Return field names where the maximum cell length is longer then limit'''
    cell_len = ((len(str(cell)) for cell in rec) for rec in table.rows)
    field_max_len = dict(zip(table.field_names, map(max, zip(*cell_len))))
    return [field for field, max_length in field_max_len.items() 
            if max_length > limit]    


# Создаем фабрику по производству функций, обрезающих текст ячейки,
# которая будет использована в свойстве PrettyTable.custom_format.
# Такая функция должна принимать имя поля и содержимое ячейки,
# а возвращать строку (с отформатированным содержимым).
# Здесь мы работаем только со строками, поэтому не тратим силы 
# на приведение содержимого к типу str.
def truncate_text(limit: int = MAX_LEN) -> callable[[str, str], str]: 
    return lambda field, value: value[:limit] + '...' if len(value) > limit else value


# Читаем CSV сразу в таблицу. ВАЖНО: передаем delimiter=',' 
# чтобы исключить работу csv.Sniffer (параметры исходного файла
# стандартные, но содержимое ячеек длинное и синтаксически сложное,
# из-за чего Sniffer с дефолтными настройками ошибётся)
table = prettytable.from_csv(open(filename), delimiter=',')

# custom_format - это словарь, ключи котого - имена полей, 
# а значения - функции, форматирующие значения поля.
# Добавим обработку полей, которые превышают лимит длины.
custom_format = dict.fromkeys(get_long_fields(table), truncate_text())

# Добавляем автоматическую нумерацию с единицы, указав имя поля индексов 
table.add_autoindex(INDEX)

# Задаём параметры, которые в текущей версии prettytable 3.16.0 
# не работают как аргумент метода `PrettyTable.get_string`
table.max_width = MAX_WIDTH
table.align = ALIGN
table.custom_format = custom_format

# Выводим таблицу на печать через get_string
# (передаваемые здесь аргументы можно задать жестко как свойства table)
print(table.get_string(
    hrules = prettytable.HRuleStyle.ALL,   # вывести все линии между ячейками
    start = start - 1,                     # печатать начиная со start включительно
    end = end,                             # закончить на строке end включительно
    fields = [INDEX, *fields],             # только индекс и столбцы fields
))
→ Ссылка