You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I am trying to apply formatting to a document that already exists. I am trying to highlight text within the document, and I would like to preserve formatting around the changes I make.
For example, highlight yellow all occurrences of "test" in the paragraph "This is a test. This is only a test"
In this example the paragraph is in one run to start, but will be in four runs after highlighting ["This is a ", 'test", ". This is only a ", "test"].
It is not immediately obvious how to accomplish this goal. There are no utility functions that I can find for copying the formatting of a run, or for splitting a run into multiple runs, or for adding a run to an arbitrary location in the paragraph. I am considering either constructing a whole new document paragraph by paragraph (grossly inefficient) or manually implementing run.split(start, end) and paragraph.addRunAt(n) functions.
Is there a better way that I'm just missing? If I make the functions I described would they be useful to add to the package?
I think your approach is about right. I think the right general architecture is to create functions or possibly wrapper objects that compose a proxy object (like Paragraph or Run) or the lxml element objects themselves. This way your work can be used by others without them having to use a fork or you having to get a commit (which is harder than most folks are willing to undertake).
I think I would work at the paragraph level and consider using character position (within the paragraph) as the key parameter(s). So you could get a sequence of runs between character positions 15 and 42, say, and then apply something to all those. The method that gave those to you would split runs if necessary to start and 15 and end at 42.
I think the formatting copy you could do with a deepcopy on the rPr element, possibly excepting certain attributes like revision-id or whatever might be sensible to skip.
I think it's an interesting problem, pretty abstract, and an opportunity to use some interesting data structures. I'd love to see what you come up with and happy to consider any questions you come up with as you go.
Here's my solution: https://github.com/aekoch/docx_formatting_aekoch.git
I haven't tested all the edge cases, but as you can see from the result.docx document it correctly applies formatting to all of the occurances of "test" in the original document.
I am having trouble copying the format though. The main problem is that both the font property of the run and the rPr property of the element throw an Attribute Error when I try to set them. I manually implemented a deep copy of fonts. That's fine for my purposes, but I don't like that there is no general solution. Any thoughts on where I could actually set the rPr property for a run? I think the ability to copy the formatting of one run to another would be useful enough to add to the package, maybe to the Font class.
The other problem I had was that I needed to access and modify the _p attribute of the paragraph object to get the runs to actually change order. When I changed the order of the runs in the paragraph.runs object it didn't actually affect the order of the runs in the saved document. Messing with the private variables probably isn't a good long term solution, but it was the only way I found to affect the order of the runs.
It seems to me that the central part of aekochs code is the line paragraph._p[index_in_paragraph+1:index_in_paragraph+1] = [new_run.element]
I tried something similar in my code: para._p.insert(runX, new_run._r)
Our goal is to move a run that was appended to the end of the paragraph.runs list to a different place in the list. In my case I was astonished that this did not work. I splitted the third run of six runs, runX was 3, but the new_run ended up in the second position of the list, irrespective of the value of runX!? PyCharm does not let me debug into the insert call, and after hours of trial and error I came up with this solution:
def split_run(para, runs, run, x):
runX = runs.index(run) + 1
t1 = run.text[0:x]
t2 = run.text[x:]
run.text = t1
new_run = add_run_copy(para, run, text=t2)
# the insert does not work as expected, the new_run is always inserted into the same place,
# irrespective of runX
# para._p.insert(runX, new_run._r)
# therefore, we append all runs behind runX AFTER the newly appended run
# i.e. we copy a b t1 c d t2 to a b t1 t2 c d, by appending c, d
# this is all trial and error, and completely obscure...
while runX < len(runs):
para._p.append(runs[runX]._r)
runX += 1
print("splitRes:", " ".join(["<" + run.text + ">" for run in para.runs]))
Btw, add_run_copy is mainly a call of paragraph.add_run, then I try to copy as many properties from the run to the copy as possible:
Moving a run element (w:r) (or any element) must take place at the lxml level using lxml calls. No assignment at the proxy-object level (i.e. Run object) will get that done:
Regarding the "cloning" of run formatting, you might try something like this:
>>>fromcopyimportdeepcopy>>>run_source, run_target=# ---however you get references to these--->>>rPr_source=run_source._r.get_or_add_rPr()
>>>rPr_clone=deepcopy(rPr_source)
>>>run_target._r.rPr=rPr_clone
I'm not sure about the assignment at the end; we might need to do something slightly fancier. Let me know if that doesn't work. It might need something like:
@scanny Hi, I just tried your format cloning method mentioned above. I found that I couldn't set run_target._r. which caused AttributeError: "can't set attribute". Do you have any other methods to realize cloning run formats beside @michaelu123's method?
Hey, funny thing, but this issue is actually part of the challenge project from "Impractical Python Projects" by Lee Vaughn, chapter 6. I comment here in hope that it may be resolved in the future and could be associated with this problem.
Activity
scanny commentedon Jul 16, 2018
I think your approach is about right. I think the right general architecture is to create functions or possibly wrapper objects that compose a proxy object (like
Paragraphor Run) or the lxml element objects themselves. This way your work can be used by others without them having to use a fork or you having to get a commit (which is harder than most folks are willing to undertake).I think I would work at the paragraph level and consider using character position (within the paragraph) as the key parameter(s). So you could get a sequence of runs between character positions 15 and 42, say, and then apply something to all those. The method that gave those to you would split runs if necessary to start and 15 and end at 42.
I think the formatting copy you could do with a deepcopy on the
rPrelement, possibly excepting certain attributes like revision-id or whatever might be sensible to skip.I think it's an interesting problem, pretty abstract, and an opportunity to use some interesting data structures. I'd love to see what you come up with and happy to consider any questions you come up with as you go.
aekoch commentedon Jul 30, 2018
Here's my solution: https://github.com/aekoch/docx_formatting_aekoch.git
I haven't tested all the edge cases, but as you can see from the result.docx document it correctly applies formatting to all of the occurances of "test" in the original document.
I am having trouble copying the format though. The main problem is that both the font property of the run and the rPr property of the element throw an Attribute Error when I try to set them. I manually implemented a deep copy of fonts. That's fine for my purposes, but I don't like that there is no general solution. Any thoughts on where I could actually set the rPr property for a run? I think the ability to copy the formatting of one run to another would be useful enough to add to the package, maybe to the Font class.
The other problem I had was that I needed to access and modify the _p attribute of the paragraph object to get the runs to actually change order. When I changed the order of the runs in the paragraph.runs object it didn't actually affect the order of the runs in the saved document. Messing with the private variables probably isn't a good long term solution, but it was the only way I found to affect the order of the runs.
michaelu123 commentedon Nov 26, 2018
It seems to me that the central part of aekochs code is the line
paragraph._p[index_in_paragraph+1:index_in_paragraph+1] = [new_run.element]I tried something similar in my code:
para._p.insert(runX, new_run._r)Our goal is to move a run that was appended to the end of the paragraph.runs list to a different place in the list. In my case I was astonished that this did not work. I splitted the third run of six runs, runX was 3, but the new_run ended up in the second position of the list, irrespective of the value of runX!? PyCharm does not let me debug into the insert call, and after hours of trial and error I came up with this solution:
Btw, add_run_copy is mainly a call of paragraph.add_run, then I try to copy as many properties from the run to the copy as possible:
But can someone (scanny?) perhaps comment on this code, and why the insert did not work?
scanny commentedon Nov 26, 2018
Moving a run element (
w:r) (or any element) must take place at the lxml level using lxml calls. No assignment at the proxy-object level (i.e.Runobject) will get that done:The basic idea is that when you assign/attach an lxml element to another parent or sibling, that element is moved, not copied.
The various
lxmlmethods you need are documented here:https://lxml.de/api/lxml.etree._Element-class.html
scanny commentedon Nov 26, 2018
Regarding the "cloning" of run formatting, you might try something like this:
I'm not sure about the assignment at the end; we might need to do something slightly fancier. Let me know if that doesn't work. It might need something like:
pxxgogo commentedon Sep 8, 2019
@scanny Hi, I just tried your format cloning method mentioned above. I found that I couldn't set
run_target._r. which causedAttributeError: "can't set attribute". Do you have any other methods to realize cloning run formats beside @michaelu123's method?alllexx88 commentedon Dec 3, 2020
Hi, I created a small project based on @scanny suggestions. It provides functions to:
Here it is: https://github.com/alllexx88/python-docx-split-run
BartoszSiwinski commentedon Jun 5, 2021
Hey, funny thing, but this issue is actually part of the challenge project from "Impractical Python Projects" by Lee Vaughn, chapter 6. I comment here in hope that it may be resolved in the future and could be associated with this problem.
scanny commentedon May 15, 2026
This StackOverflow (SO) answer has code for an
isolate_run()function that helps with this sort of thing.