Анализ документов Word с использованием Python

В своей работе мы часто анализируем большой объем данных. Давайте рассмотрим, как можно автоматизировать процесс анализа документов на примере библиотеки docx (способной обрабатывать документы в формате. docx).

А также расскажем другие возможности, которые предлагает Python: как отделить текст с нужным стилем форматирования? Как извлечь все изображения из документа?

Для установки библиотеки в командной строке необходимо ввести:

> pip install python-docx

После успешной установки библиотеки, её нужно импортировать в Python. Обратите внимание, что несмотря на то, что для установки использовалось название python-docx, при импорте следует называть библиотеку docx:

import docx

Как правило, мы обращаемся к автоматизации, когда нам нужно извлечь нужную информацию не из одного, а сразу из многих документов. Чтобы иметь возможность обработать все документы, для начала нужно собрать список таких документов. Здесь сможет помочь библиотека os, с помощью которой можно рекурсивно обойти директории, в которых хранятся документы. Предположим, что все они находятся внутри директории, где расположен скрипт:

import os paths = [] folder = os.getcwd() for root, dirs, files in os.walk(folder): for file in files: if file.endswith('docx') and not file.startswith('~'): paths.append(os.path.join(root, file))

Мы прошли по всем директориям и занесли в список paths все файлы с расширением. docx. Файлы, начинавшиеся с тильды, игнорировались (эти временные файлы возникают лишь тогда, когда в Windows открыт какой-либо из документов). Теперь, когда у нас уже есть список всех документов, можно начинать с ними работать:

for path in paths: doc = docx.Document(path)

В блоке выше на каждом шаге цикла в переменную doc записывается экземпляр, представляющий собой весь документ. Мы можем посмотреть основные свойства такого документа:

properties = doc.core_properties print('Автор документа:', properties.author) print('Автор последней правки:', properties.last_modified_by) print('Дата создания документа:', properties.created) print('Дата последней правки:', properties.modified) print('Дата последней печати:', properties.last_printed) print('Количество сохранений:', properties.revision)

Из основных свойств можно получить автора документа, основные даты, количество сохранений документа и пр. Обратите внимание, что даты и время будут указаны в часовом поясе UTC+0.

Теперь поговорим о том, как можно проанализировать содержимое документа. Файлы с расширением docx обладают развитой внутренней структурой, которая в библиотеке docx представлена следующими объектами:

Объект Document, представляющий собой весь документ

  • Список объектов Paragraph – абзацы документа
    * Список объектов Run – фрагменты текста с различными стилями форматирования (курсив, цвет шрифта и т.п.)
  • Список объектов Table – таблицы документа
    * Список объектов Row – строки таблицы
    * Список объектов Cell – ячейки в строке
    * Список объектов Column – столбцы таблицы
    * Список объектов Cell – ячейки в столбце
  • Список объектов InlineShape – иллюстрации документа

Работа с текстом документа

Для начала давайте разберёмся, как работать с текстом документа. В библиотеке docx это возможно через обращение к абзацам документа. Можно получить как сам текст абзаца, так и его характеристики: тип выравнивания, величину отступов и интервалов, положение на странице.

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

text = [] for paragraph in doc.paragraphs: text.append(paragraph.text) print('\n'.join(text))

Как мы видим, для получения текста абзаца нужно просто обратиться к объекту paragraph.text. Но что же делать, если нужно извлечь только абзацы с определёнными характеристиками и далее работать именно с ними? Рассмотрим основные характеристики абзацев, которые можно проанализировать.

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

for paragraph in doc.paragraphs: print('Выравнивание абзаца:', paragraph.alignment)

Значения alignment будут соответствовать одному из основных стилей выравнивания: LEFT (0), center (1), RIGHT (2) или justify (3). Однако если пользователь не установил стиль выравнивания, значение параметра alignment будет None.

Кроме того, можно получить и значения отступов у абзацев документа:

for paragraph in doc.paragraphs: formatting = paragraph.paragraph_format print('Отступ перед абзацем:', formatting.space_before) print('Отступ после абзаца:', formatting.space_after) print('Отступ слева:', formatting.left_indent) print('Отступ справа:', formatting.right_indent) print('Отступ первой строки абзаца:', formatting.first_line_indent)

Как и в предыдущем примере, если отступы не были установлены, значения параметров будут None. В остальных случаях они будут представлены в виде целого числа в формате EMU (английские метрические единицы). Этот формат позволяет конвертировать число как в метрическую, так и в английскую систему мер. Привести полученные числа в привычный формат довольно просто, достаточно просто добавить нужные единицы исчисления после параметра (например, formatting.space_before.cm или formatting.space_before.pt). Главное помнить, что такое преобразование нельзя применять к значениям None.

Наконец, можно посмотреть на положение абзаца на странице. В меню Абзац… на вкладке Положение на странице находятся четыре параметра, значения которых также можно посмотреть при помощи библиотеки docx:

for paragraph in doc.paragraphs: formatting = paragraph.paragraph_format print('Не отрывать от следующего абзаца:', formatting.keep_with_next) print('Не разрывать абзац:', formatting.keep_together) print('Абзац с новой страницы:', formatting.page_break_before) print('Запрет висячих строк:', formatting.widow_control)

Параметры будут иметь значение None для случаев, когда пользователь не устанавливал на них галочки, и True, если устанавливал.

Мы рассмотрели основные способы, которыми можно проанализировать абзац в документе. Но бывают ситуации, когда мы точно знаем, что информация, которую нужно извлечь, написана курсивом или выделена определённым цветом. Как быть в таком случае?

Можно получить список фрагментов с различными стилями форматирования (список объектов Run). Попробуем, к примеру, извлечь все фрагменты, написанные курсивом:

for paragraph in doc.paragraphs: for run in paragraph.runs: if run.italic: print(run.text)

Очень просто, не так ли? Посмотрим, какие ещё стили форматирования можно извлечь:

for paragraph in doc.paragraphs: for run in paragraph.runs: print('Полужирный текст:', run.bold) print('Подчёркнутый текст:', run.underline) print('Зачёркнутый текст:', run.strike) print('Название шрифта:', run.font.name) print('Цвет текста, RGB:', run.font.color.rgb) print('Цвет заливки текста:', run.font.highlight_color)

Если пользователь не менял стиль форматирования (отсутствует подчёркивание, используется стандартный шрифт и т.п.), параметры будут иметь значение None. Но если стиль определённого параметра изменялся, то:

  • параметры italic, bold, underline, strike будут иметь значение True;
  • параметр font.name – наименование шрифта;
  • параметр font.color.rgb – код цвета текста в RGB;
  • параметр font.highlight_color – наименование цвета заливки текста.

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

Абзацы и их фрагменты могут быть оформлены в определённом стиле, соответствующем стилям Word (например, Normal, Heading 1, Intense Quote). Чем это может быть полезно? К примеру, обращение к стилям абзаца может пригодиться при выделении нумерованных или маркированных списков. Каждый элемент таких списков считается отдельным абзацев, однако каждому из них приписан особый стиль – List Paragraph. С помощью кода ниже можно извлечь только элементы списков:

for paragraph in doc.paragraphs: if paragraph.style.name == 'List Paragraph': print(paragraph.text)

Чтобы закрепить полученные знания, давайте разберём менее тривиальный случай. Предположим, что у нас есть множество документов с похожей структурой, из которых нужно извлечь названия продуктов. Проанализировав документы, мы установили, что продукты встречаются только в абзацах, начинающихся с новой страницы и выровненных по ширине. Притом сами названия написаны с использованием полужирного начертания, шрифт Arial Narrow. Посмотрим, как можно проанализировать документы:

for path in paths: doc = docx.Document(path) product_names = [] for paragraph in doc.paragraphs: formatting = paragraph.paragraph_format if formatting.page_break_before and paragraph.alignment == 3: product_name, is_sequential = '', False for run in paragraph.runs: if run.bold and run.font.name == 'Arial Narrow': is_sequential = True product_name += run.text elif is_sequential == True: product_names.append(product_name) product_name, is_sequential = '', False

В блоке кода выше последовательно обрабатываются все файлы из списка paths, преобразовываемые в ходе обработки в объект Document. В каждом документе происходит перебор абзацев и выполняются проверки: абзац должен начинаться с новой страницы и быть выровненным по ширине. Если проверки прошли успешно, внутри абзаца происходит уже перебор фрагментов с различными типами форматированием и проверки на начертание и шрифт.

Обратим внимание на переменную is_sequential, которая помогает определить, идут ли фрагменты, прошедшие проверку, друг за другом. Фрагменты с символами разных типов (буквы и числа, кириллица и латиница) разбиваются на несколько, но поскольку в названии продукта одновременно могут встретиться символы всех типов, все последовательно идущие фрагменты соединяются в один. Он и заносится в результирующий список product_names.

Работа с таблицами

Мы рассмотрели способы, которыми можно обрабатывать текст в документах, а теперь давайте перейдём к обработке таблиц. Любую таблицу можно перебирать как по строкам, так и по столбцам. Посмотрим, как можно построчно получить текст каждой ячейки в таблице:

for table in doc.tables: for row in table.rows: for cell in row.cells: print(cell.text)

Если же во второй строке заменить rows на columns, то можно будет аналогичным образом прочитать таблицу по столбцам. Текст в ячейках таблицы тоже состоит из абзацев. Если мы захотим проанализировать абзацы или фрагменты внутри ячейки, то можно будет воспользоваться всеми методами объектов Paragraph и Run.

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

for table in doc.tables: for index, row in enumerate(table.rows): if index == 0: row_text = list(cell.text for cell in row.cells) if 'Продукт' not in row_text or 'Стоимость' not in row_text: break for cell in row.cells: print(cell.text)

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

for table in doc.tables: unique, merged = set(), set() for row in table.rows: for cell in row.cells: tc = cell._tc cell_loc = (tc.top, tc.bottom, tc.left, tc.right) if cell_loc in unique: merged.add(cell_loc) else: unique.add(cell_loc) print(merged)

Воспользовавшись этим кодом, можно получить все координаты объединённых ячеек для каждой из таблиц документа. Кроме того, разница координат tc.top и tc.bottom показывает, сколько строк в объединённой ячейке, а разница tc.left и tc.right – сколько столбцов.

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

import re pattern = re.compile('w:fill=\"(\S*)\"') for table in doc.tables: for row in table.rows: for cell in row.cells: match = pattern.search(cell._tc.xml) if match: if match.group(1) == 'FFFF00': print(cell.text)

В этом блоке кода мы выделили только те ячейки, фон которых был окрашен в жёлтый цвет ( #FFFF00 в формате RGB).

Работа с иллюстрациями

В библиотеке docx также реализована возможность работы с иллюстрациями документа. Стандартными способами можно посмотреть только на размеры изображений:

for shape in doc.inline_shapes: print(shape.width, shape.height)

Однако при помощи сторонней библиотеки docx2txt и анализа xml-кода абзацев становится возможным не только выгрузить все иллюстрации документов, но и определить, в каком именно абзаце они встречались:

import os import docx import docx2txt for path in paths: splitted = os.path.split(path) folders = [os.path.splitext(splitted[1])[0]] while splitted[0]: splitted = os.path.split(splitted[0]) folders.insert(0, splitted[1]) images_path = os.path.join('images', *folders) os.makedirs(images_path, exist_ok=True) doc = docx.Document(path) docx2txt.process(path, images_path) rels = {} for rel in doc.part.rels.values(): if isinstance(rel._target, docx.parts.image.ImagePart): rels[rel.rId] = os.path.basename(rel._target.partname) for paragraph in doc.paragraphs: if 'Graphic' in paragraph._p.xml: for rId in rels: if rId in paragraph._p.xml: print(os.path.join(images_path, rels[rId])) print(paragraph.text)

В этом блоке мы выводим путь к изображению, которое сохранено на диске, и текст параграфа, в котором встретилось изображение. Все изображения находятся внутри директории images, а именно — в поддиректориях, соответствующих расположению исходного файла Word.

1717
9 комментариев

Не могу сказать, что такой способ намного более эффективен, но в своё время гнал их в HTML Word`ом и там уже разбирал по кускам.

2
Ответить

Это было актуально во времена, когда формат *.doc был бинарным и проприетарным.
Сейчас, когда *.docx является, по сути, XML, завёрнутым в ZIP-архив, достаточно парсить его как текстовый xml- документ.
Объектная модель word-документа благодаря этому стала практически идентичной объектной модели веб-страницы. Соответственно, к ним можно применять схожие подходы - в этом смысле описанная в статья питон-библиотека docx выступает аналогом, например, джаваскрипт-библиотеки jQuery или любой подобной.
Так ведь?

2
Ответить

Rtf...боль

2
Ответить

Добротно. Респект от диназавра.

1
Ответить

Спасибо. Полезно!

1
Ответить

if paragraph.text.find('TEST') > -1:
    print(paragraph.text)

1
Ответить

А как просто найти слово в тексте  ?  Все форумы облазил не могу понять ...... 

for paragraph in document.paragraphs:
if "TEST" in paragraph.text:
print paragraph.text

Ответить