Попробуйте что-то вроде:
hdoc.xpath("//p[./preceding-sibling::h3[contains(text(),'title1')] and ./following-sibling::h3[contains(text(),'title2')]]/text()")
hdoc.xpath("//p[./preceding-sibling::h3[contains(text(),'title2')] and ./following-sibling::h3[contains(text(),'title3')]]/text()")
hdoc.xpath("//p[./preceding-sibling::h3[contains(text(),'title3')] and ./following-sibling::h3[contains(text(),'title4')]]/text()")
hdoc.xpath("//p[./preceding-sibling::h3[contains(text(),'title4')] and not(./following-sibling::h3)]/text()")
Если вы не хотите зависеть от текста каждого h3, вы можете получить их с количеством h3, которое каждый элемент имел раньше:
# For elements between title1 and title2
hdoc.xpath('//p[count(preceding-sibling::h3)=1]/text() | //table[count(preceding-sibling::h3)=2]//td/text()')
# For elements between title2 and title3
hdoc.xpath('//p[count(preceding-sibling::h3)=2]/text() | //table[count(preceding-sibling::h3)=2]//td/text()')
...