accessing bookmarks using python-docx

Viewed 401

I am using the python-docx module to read and edit a .docm file, The file contains bookmarks, how do I access all the bookmarks already stored using that module, there doesnt seem to be any methods within the doc object.

1 Answers

As commented by @D Malan, now in 2021-november it is still an open issue in the python-docx.

Meanwhile we can live with our own implementation.

Please create a file named docxbookmark.py in a folder accessible as an import:

from docx.document import Document as _innerdoclass
from docx import Document as _innerdocfn
from docx.oxml.shared import qn
from lxml.etree import Element as El

class Document(_innerdoclass):
    def _bookmark_elements(self, recursive=True):
        if recursive:
            startag = qn('w:start')
            bkms = []
            def _bookmark_elements_recursive(parent):
                if parent.tag == startag:
                    bkms.append(parent)
                for el in parent:
                    _bookmark_elements_recursive(el)
            _bookmark_elements_recursive(self._element)
            return bkms
        else:
            return self._element.xpath('//'+qn('w:bookmarkStart'))
    def bookmark_names(self):
        """
        Gets a list of bookmarks
        """
        return [v for bkmkels in self._bookmark_elements() for k,v in bkmkels.items() if k.endswith('}name')]
    def add_bookmark(self, bookmarkname):
        """
        Adds a bookmark with bookmark with name bookmarkname to the end of the file
        """
        el = [el for el in self._element[0] if el.tag.endswith('}p')][-1]
        el.append(El(qn('w:bookmarkStart'),{qn('w:id'):'0',qn('w:name'):bookmarkname}))
        el.append(El(qn('w:bookmarkEnd'),{qn('w:id'):'0'}))
    def __init__(self, innerDocInstance = None):
        super().__init__(Document, None)
        if innerDocInstance is not None and type(innerDocInstance) is _innerdoclass:
            self.__body = innerDocInstance.__body
            self._element = innerDocInstance._element
            self._part = innerDocInstance._part

def DocumentCreate(docx=None):
    """
    Return a |Document| object loaded from *docx*, where *docx* can be
    either a path to a ``.docx`` file (a string) or a file-like object. If
    *docx* is missing or ``None``, the built-in default document "template"
    is loaded.
    """
    return Document(_innerdocfn(docx))

Now we can use our facade implementation just like the old one, along with those new add_bookmark and bookmark_names.

To add a bookmark in a new file, import our implementation and use add_bookmark on the document object:

from docxbookmark import DocumentCreate as Document

doc = Document()
document.add_paragraph('First Paragraph')
document.add_bookmark('FirstBookmark')
document.add_paragraph('Second Paragraph')
document.save('docwithbookmarks.docx')

To see bookmarks in a document, import our implementation and use bookmark_names on the document object:

from docxbookmark import DocumentCreate as Document

doc = Document('docwithbookmarks.docx')
doc.bookmark_names()

The returned list is simplier than other objects, it shows only strings not objects. There is an internal _bookmark_elements which will return lxml nodes which are not the same as python-docx objects.

Just a few tests were made, probably not working in many cases. Please tell in the comments if it didn't work.

Related