Skip to content

Multi-column pages that read in the wrong order

Sometimes you have data that flows over multiple columns, or pages, or just isn’t arranged in a “normal” top-to-bottom way. This page has three columns of text with two tables buried in them — and a plain extract_text() walks straight across all three columns at once, interleaving sentences that have nothing to do with each other.

from natural_pdf import PDF
pdf = PDF("pdfs/multicolumn.pdf")
page = pdf.pages[0]
page.show()

Multi-column pages that read in the wrong order

Look at the first few hundred characters of a naive extraction — each line jumps from column to column:

print(page.extract_text(layout=True)[:500])
This is some text             25     8115      XXX21        6783

          This is some text             26     8448      XXX22        7116
                                        27     8781      XXX23        7449
          This is some text             28     9114      XXX24        7782
                                        29     9447      XXX25        8115

The fix is to reflow the page: cut out each column as a region, then paste the pieces back together end-to-end so “below” means “further along in reading order.”

page.region() cuts a rectangle by coordinates. Three vertical slices, each a third of the page wide:

left = page.region(left=0, right=page.width/3, top=0, bottom=page.height)
mid = page.region(left=page.width/3, right=page.width/3*2, top=0, bottom=page.height)
right = page.region(left=page.width/3*2, right=page.width, top=0, bottom=page.height)
page.highlight(left, mid, right)

Cut the page into columns

A Flow connects separate regions (or whole pages) vertically or horizontally. Stacking the three columns vertically turns the page into one long single-column document:

from natural_pdf.flows import Flow
stacked = [left, mid, right]
flow = Flow(segments=stacked, arrangement="vertical")
flow.show()

Stack them with a Flow

Now any spatial operation — “find something below this” — follows reading order, even when that means jumping from the bottom of one column to the top of the next:

region = (
flow
.find('text:contains("Table one")')
.below(
until='text:contains("Table two")',
include_endpoint=False
)
)
region.show()

Stack them with a Flow

The tables sit under bold headers. Find the headers first — the width > 10 filter skips some tiny empty boxes that also register as bold:

flow.find_all('text[width>10]:bold').show()

Pull out every table

Then take each header and sweep down until the next bold header or the closing paragraph, whichever comes first. The | in the selector means “either of these”:

regions = (
flow
.find_all('text[width>10]:bold')
.below(
until='text[width>10]:bold|text:contains("Here is a bit")',
include_endpoint=False
)
)
regions.show()

Pull out every table

Each region is one table, so extract_table() on a region can’t grab the wrong one:

regions[0].extract_table().to_df()
index number
0 1 123
1 2 456
2 3 789
3 4 1122
4 5 1455
5 6 1788
6 7 2121
7 8 2454
8 9 2787
9 10 3120
10 11 3453
11 12 3786
12 13 4119
13 14 4452
14 15 4785
15 16 5118
16 17 5451
17 18 5784
18 19 6117
19 20 6450
20 21 6783
21 22 7116
22 23 7449
23 24 7782
24 25 8115
25 26 8448
26 27 8781
27 28 9114
28 29 9447
29 30 9780
30 31 10113
31 32 10446
32 33 10779
33 34 11112
34 35 11445
35 36 11778
36 37 12111
37 38 12444
38 39 12777

And if the tables share a structure, combine them into one DataFrame:

import pandas as pd
dfs = regions.apply(lambda region: region.extract_table().to_df())
merged = pd.concat(dfs, ignore_index=True)
merged
index number
0 1 123
1 2 456
2 3 789
3 4 1122
4 5 1455
... ... ...
73 XXX35 11445
74 XXX36 11778
75 XXX37 12111
76 XXX38 12444
77 XXX39 12777

78 rows × 2 columns

This page splits into exact thirds, so hardcoded coordinates work. When column widths vary — or you can’t hand-measure every document in a stack — page.analyze_layout() can detect text blocks for you (it downloads YOLO weights on first use), and the detected regions feed into a Flow the same way. The same Flow trick also spans pages: pass pdf.pages as the segments and a table that breaks across a page boundary becomes one table.