Парсинг документов 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.

If you ever had a need to programmatically examine the text in a Microsoft Word document, getting the text out in the first place can be challenging. Sure, you can manually save your document to a plain text file that’s much easier to process, but if you have multiple documents to examine, that can be painful.

Recently I had such a need and found this Toward Data Science article quite helpful. But let’s take the challenge a little further: suppose you had a document with multiple sections and need to pull the text from specific sections.

Page 1 has my table of contents
Page 2 contains a variety of sections

Let’s suppose I need to pull just the text from the “sub-sections”. In my example, I have three sub-sections: Sub-Section 1, Sub-Section 2, and Sub-Section 3. In my Word document, I’ve styled these headers as “Heading 2” text. Here’s how I went about pull out the text for each of these sections.

Step 1: Import your packages

For my needs, I only need to import zipfile and ElementTree, which is nice as I didn’t need to install any third party packages:

import zipfile
import xml.etree.ElementTree as ET

Step 2: Parse the document XML

doc = zipfile.ZipFile('./data/test.docx').read('word/document.xml')
root = ET.fromstring(doc)

Step 3: Explore the XML for the sections and text you want

You’ll spend most of your time here, trying to figure out what elements hold the contents in which you are interested. The XML of Microsoft documents follows the WordprocessingML standard, which can be quite complicated. I spent a lot of time manually reviewing my XML looking for the elements I needed. You can write out the XML like so:

Step 4: Find all the paragraphs

To solve my problem, I first decided to pull together a collection of all the paragraphs in the document so that I could later iterate across them and make decisions. To make that work a little easier, I also declared a namespace object used by Microsoft’s WordprocessingML standard:

# Microsoft's XML makes heavy use of XML namespaces; thus, we'll need to reference that in our code
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
body = root.find('w:body', ns)  # find the XML "body" tag
p_sections = body.findall('w:p', ns)  # under the body tag, find all the paragraph sections

It can be helpful to actually see the text in each of these sections. Through researching Microsoft’s XML standard, I know that document text is usually contained in “t” elements. So, if I write an XPath query to find all the “t” elements within a given section, I can join the text of all those elements together to get the full text of the paragraph. This code does that:

for p in p_sections:
    text_elems = p.findall('.//w:t', ns)
    print(''.join([t.text for t in text_elems]))
    print()

Step 5: Find all the “Heading 2” sections

Now, let’s iterate through each paragraph section and see if we can figure out which sections have been styled with “Heading 2”. If we can find those Heading 2 sections, we’ll then know that the subsequent text is the text we need.

Through researching more the XML standard, I found that if I search for pStyle elements that contain the value “Heading2”, these will be the sections I’m after. To make my code a little cleaner, I wrote functions to both evaluate each section for the Heading 2 style and extract the full text of the section:

def is_heading2_section(p):
    """Returns True if the given paragraph section has been styled as a Heading2"""
    return_val = False
    heading_style_elem = p.find(".//w:pStyle[@w:val='Heading2']", ns)
    if heading_style_elem is not None:
        return_val = True
    return return_val


def get_section_text(p):
    """Returns the joined text of the text elements under the given paragraph tag"""
    return_val = ''
    text_elems = p.findall('.//w:t', ns)
    if text_elems is not None:
        return_val = ''.join([t.text for t in text_elems])
    return return_val


section_labels = [get_section_text(s) if is_heading2_section(s) else '' for s in p_sections]

Now, if I print out my section_labels list, I see this:

My section_labels list

Step 6: Finally, extract the Heading 2 headers and subsequent text

Now, I can use simple list comprehension to glue together both the section headers and associated text of the three sub-sections I’m after:

section_text = [{'title': t, 'text': get_section_text(p_sections[i+1])} for i, t in enumerate(section_labels) if len(t) > 0]

And that list looks like this:

My section_text list

You can download my code here.

Project description

Parse all contents of a docx file with python-docx

Installation

python3 -m pip install docx-parser

Features:

  • paragraph: text paragraph, with style_id
  • multipart: paragraph with image or hyperlink
  • table: table data with merged_cells

Examples

  • CMD
docx_parser --help

# parse image as file
docx_parser tests/demo.docx -D tests/media -o tests/out.file.jl

# parse image as base64 string
  docx_parser tests/demo.docx -A base64 -o tests/out.base64.jl
  • Python
from docx_parser import DocumentParser

infile = 'tests/demo.docx'
doc = DocumentParser(infile)
for _type, item in doc.parse():
    print(_type, item)

ToDo

  • parse text style: color, bgcolor, font, bold, italic …
  • parse paragraph format

Download files

Download the file for your platform. If you’re not sure which to choose, learn more about installing packages.

Source Distribution

Built Distribution

С помощью модуля python-docx можно создавать и изменять документы MS Word с расширением .docx. Чтобы установить этот модуль, выполняем команду

> pip install python-docx

При установке модуля надо вводить python-docx, а не docx (это другой модуль). В то же время при импортировании модуля python-docx следует использовать import docx, а не import python-docx.

Чтение документов MS Word

Файлы с расширением .docx обладают развитой внутренней структурой. В модуле python-docx эта структура представлена тремя различными типами данных. На самом верхнем уровне объект Document представляет собой весь документ. Объект Document содержит список объектов Paragraph, которые представляют собой абзацы документа. Каждый из абзацев содержит список, состоящий из одного или нескольких объектов Run, представляющих собой фрагменты текста с различными стилями форматирования.

import docx

doc = docx.Document('example.docx')

# количество абзацев в документе
print(len(doc.paragraphs))

# текст первого абзаца в документе
print(doc.paragraphs[0].text)

# текст второго абзаца в документе
print(doc.paragraphs[1].text)

# текст первого Run второго абзаца
print(doc.paragraphs[1].runs[0].text)
6
Название документа
Простой абзац с жирным и курсивным текстом
Простой абзац с 

Получаем весь текст из документа:

text = []
for paragraph in doc.paragraphs:
    text.append(paragraph.text)
print('n'.join(text))
Название документа
Простой абзац с жирным и курсивным текстом
Заголовок, уровень 1
Выделенная цитата
Первый элемент маркированного списка
Первый элемент нумерованного списка

Стилевое оформление

В документах MS Word применяются два типа стилей: стили абзацев, которые могут применяться к объектам Paragraph, стили символов, которые могут применяться к объектам Run. Как объектам Paragraph, так и объектам Run можно назначать стили, присваивая их атрибутам style значение в виде строки. Этой строкой должно быть имя стиля. Если для стиля задано значение None, то у объекта Paragraph или Run не будет связанного с ним стиля.

Стили абзацев

  • Normal
  • Body Text
  • Body Text 2
  • Body Text 3
  • Caption
  • Heading 1
  • Heading 2
  • Heading 3
  • Heading 4
  • Heading 5
  • Heading 6
  • Heading 7
  • Heading 8
  • Heading 9
  • Intense Quote
  • List
  • List 2
  • List 3
  • List Bullet
  • List Bullet 2
  • List Bullet 3
  • List Continue
  • List Continue 2
  • List Continue 3
  • List Number
  • List Number 2
  • List Number 3
  • List Paragraph
  • Macro Text
  • No Spacing
  • Quote
  • Subtitle
  • TOCHeading
  • Title

Стили символов

  • Emphasis
  • Strong
  • Book Title
  • Default Paragraph Font
  • Intense Emphasis
  • Subtle Emphasis
  • Intense Reference
  • Subtle Reference
paragraph.style = 'Quote'
run.style = 'Book Title'

Атрибуты объекта Run

Отдельные фрагменты текста, представленные объектами Run, могут подвергаться дополнительному форматированию с помощью атрибутов. Для каждого из этих атрибутов может быть задано одно из трех значений: True (атрибут активизирован), False (атрибут отключен) и None (применяется стиль, установленный для данного объекта Run).

  • bold — Полужирное начертание
  • underline — Подчеркнутый текст
  • italic — Курсивное начертание
  • strike — Зачеркнутый текст

Изменим стили для всех параграфов нашего документа:

import docx

doc = docx.Document('example.docx')

# изменяем стили для всех параграфов
for paragraph in doc.paragraphs:
    paragraph.style = 'Normal'

doc.save('restyled.docx')

А теперь восстановим все как было:

import docx

os.chdir('C:\example')

doc1 = docx.Document('example.docx')
doc2 = docx.Document('restyled.docx')

# получаем из первого документа стили всех абзацев
styles = []
for paragraph in doc1.paragraphs:
    styles.append(paragraph.style)

# применяем стили ко всем абзацам второго документа
for i in range(len(doc2.paragraphs)):
    doc2.paragraphs[i].style = styles[i]

doc2.save('restored.docx')

Изменим форматирвание объектов Run второго абзаца:

import docx

doc = docx.Document('example.docx')

# добавляем стиль символов для runs[0]
doc.paragraphs[1].runs[0].style = 'Intense Emphasis'
# добавляем подчеркивание для runs[4]
doc.paragraphs[1].runs[4].underline = True

doc.save('restyled2.docx')

Запись докуменов MS Word

Добавление абзацев осуществляется вызовом метода add_paragraph() объекта Document. Для добавления текста в конец существующего абзаца, надо вызвать метод add_run() объекта Paragraph:

import docx

doc = docx.Document()

# добавляем первый параграф
doc.add_paragraph('Здравствуй, мир!')

# добавляем еще два параграфа
par1 = doc.add_paragraph('Это второй абзац.')
par2 = doc.add_paragraph('Это третий абзац.')

# добавляем текст во второй параграф
par1.add_run(' Этот текст был добавлен во второй абзац.')

# добавляем текст в третий параграф
par2.add_run(' Добавляем текст в третий абзац.').bold = True

doc.save('helloworld.docx')

Оба метода, add_paragraph() и add_run() принимают необязательный второй аргумент, содержащий строку стиля, например:

doc.add_paragraph('Здравствуй, мир!', 'Title')

Добавление заголовков

Вызов метода add_heading() приводит к добавлению абзаца, отформатированного в соответствии с одним из возможных стилей заголовков:

doc.add_heading('Заголовок 0', 0)
doc.add_heading('Заголовок 1', 1)
doc.add_heading('Заголовок 2', 2)
doc.add_heading('Заголовок 3', 3)
doc.add_heading('Заголовок 4', 4)

Аргументами метода add_heading() являются строка текста и целое число от 0 до 4. Значению 0 соответствует стиль заголовка Title.

Добавление разрывов строк и страниц

Чтобы добавить разрыв строки (а не добавлять новый абзац), нужно вызвать метод add_break() объекта Run. Если же требуется добавить разрыв страницы, то методу add_break() надо передать значение docx.enum.text.WD_BREAK.PAGE в качестве единственного аргумента:

import docx

doc = docx.Document()

doc.add_paragraph('Это первая страница')
doc.paragraphs[0].runs[0].add_break(docx.enum.text.WD_BREAK.PAGE)
doc.add_paragraph('Это вторая страница')

doc.save('pages.docx')

Добавление изображений

Метод add_picture() объекта Document позволяет добавлять изображения в конце документа. Например, добавим в конец документа изображение kitten.jpg шириной 10 сантиметров:

import docx

doc = docx.Document()

doc.add_paragraph('Это первый абзац')
doc.add_picture('kitten.jpg', width = docx.shared.Cm(10))

doc.save('picture.docx')

Именованные аргументы width и height задают ширину и высоту изображения. Если их опустить, то значения этих аргументов будут определяться размерами самого изображения.

Добавление таблицы

import docx

doc = docx.Document()

# добавляем таблицу 3x3
table = doc.add_table(rows = 3, cols = 3)
# применяем стиль для таблицы
table.style = 'Table Grid'

# заполняем таблицу данными
for row in range(3):
    for col in range(3):
        # получаем ячейку таблицы
        cell = table.cell(row, col)
        # записываем в ячейку данные
        cell.text = str(row + 1) + str(col + 1)

doc.save('table.docx')

import docx

doc = docx.Document('table.docx')

# получаем первую таблицу в документе
table = doc.tables[0]

# читаем данные из таблицы
for row in table.rows:
    string = ''
    for cell in row.cells:
        string = string + cell.text + ' '
    print(string)
11 12 13 
21 22 23 
31 32 33 

Дополнительно

  • Документация python-docx

Поиск:
MS • Python • Web-разработка • Word • Модуль

Каталог оборудования

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Производители

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Функциональные группы

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Время прочтения: 9 мин.

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

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

> 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.

Понравилась статья? Поделить с друзьями:
  • Парсинг данных с сайта в excel что это такое
  • Пароль на редактирование файла word
  • Парсинг данных с сайта в excel бесплатно
  • Парсинг данных с сайта в excel python
  • Парсинг всех товаров в excel