Skip to content

Arabic election results across four pages

This PDF holds election results from the Tunisian region of Mednine: one long table split over four pages, with spanning header cells, rotated headers, and — the real test — Arabic script, which reads right-to-left. Natural PDF handles RTL text ordering during extraction, so the words come out in logical order rather than reversed.

from io import BytesIO
import requests
from natural_pdf import PDF
url = "https://pub-4e99d31d19cb404d8d4f5f7efa51ef6e.r2.dev/pdfs/mednine/mednine.pdf"
pdf = PDF(BytesIO(requests.get(url).content))
pdf.show(cols=3)

Arabic election results across four pages

One wrinkle before the PDF itself: this host rejects downloads from Python’s built-in urllib (a plain PDF(url) gets a 403), so fetch the bytes with requests and hand them over — PDF() accepts a path, a URL, or any file-like object.

Since the PDF is all one big table with nothing else in the way, the whole job is: treat four pages as one page, extract one table, clean up in pandas.

Multi-page flows

A Flow connects pages (or regions) vertically or horizontally. Stack all four pages on top of each other:

from natural_pdf.flows import Flow
flow = Flow(pdf.pages, arrangement='vertical')
flow.show(width=300)

Build a flow

With a flow, extract_table() automatically combines the table across every segment:

df = flow.extract_table().to_df(header=None)
df
0 1 2 3 4 5 6 7 8 9 10 11
0 الانتخاابت الترشيعية 2014 None None None None None None None None None None None
1 ورقة كشف نتائج دائرة مدنين None None None None None None None None None None None
2 مجموع األصوات\nاملصرح بها\nبالدائرة\nاالنتخابية توزيع األصوات املصرح بها حسب املعتمدية None None None None None None None None القائمات املترشحة ا\nل\nرت\nت\nي\nب
3 None مدنين\nالشمالية مدنين\nالجنوبية سيدي مخلوف جرجيس جربة ميدون جربة حومة\nالسوق جربة أجيم بني خداش بن قردان None None
4 426 63 43 56 58 49 55 33 32 37 قائمة من أجل تحقيق سلطة الشعب 1
5 3 001 419 806 197 289 217 616 59 118 280 قائمة حزب التحالف الديمقراطي 2
6 387 50 60 42 64 28 41 20 42 40 قائمة حزب العدالة والتنمية 3
7 1 193 135 87 43 170 167 327 39 181 44 قائمة الحزب الجمهوري 4
8 430 117 62 31 43 28 42 23 38 46 قائمة حزب الغد 5
9 3 066 93 127 123 1 422 322 640 68 36 235 قائمة حزب آفاق تونس 6
10 203 14 15 22 33 23 30 6 7 53 قائمة حزب االنفتاح والوفاء 7
11 192 14 13 14 20 18 21 7 7 78 قائمة اتحاد الشباب الديمقراطي التونس ي 8
12 1 617 53 59 33 127 282 883 110 25 45 قائمة حزب التيار الديمقراطي 9
13 267 23 32 21 33 25 18 15 21 79 قائمة حزب األمان 10
14 4 686 254 505 182 2 535 221 278 58 91 562 قائمة حركة الشعب 11
15 1 443 63 51 58 731 82 326 42 34 56 قائمة االتحاد من أجل تونس 12
16 803 96 96 65 119 78 91 47 73 138 قائمة شباب مدنين 13
17 373 28 24 28 38 37 113 30 25 50 قائمة حزب املجد 14
18 68 795 9 010 10 898 4 013 8 627 7 912 9 854 3 358 5 021 10 102 قائمة حزب حركة النهضة 15
19 309 29 25 17 39 41 41 54 21 42 قائمة تونس أقوى 16
20 204 23 40 9 19 20 22 2 13 56 قائمة الشعب يريد 17
21 119 4 7 6 15 16 44 7 9 11 قائمة الربيع العربي لتحقيق السيادة\nاالقتصادية 18
22 385 86 51 31 61 33 30 14 32 47 قائمة حزب صوت شعب تونس 19
23 576 129 130 54 36 58 87 8 24 50 قائمة الوحدة الوطنية املستقلة 20
24 14 334 1 096 1 219 950 3 186 1 738 3 477 492 608 1 568 قائمة حركة نداء تونس 21
25 2 788 101 118 30 877 400 696 119 48 399 قائمة الجبهة الشعبية 22
26 273 20 24 12 23 24 20 26 106 18 قائمة التآلف الوطني 23
27 901 62 112 48 88 74 107 25 56 329 قائمة حركة وفاء 24
28 4 392 449 382 223 565 749 808 163 177 876 قائمة حزب االتحاد الوطني الحر 25
29 244 67 21 9 30 14 19 7 56 21 قائمة حزب األصالة 26
30 249 28 25 50 30 20 24 39 15 18 قائمة حركة الديمقراطيين االشتراكيين 27
31 6 305 575 1 158 217 524 1 091 1 746 283 244 467 قائمة حزب املؤتمر من أجل الجمهورية 28
32 1 736 164 143 138 120 465 238 74 205 192 قائمة حزب املبادرة الوطنية الدستورية 29
33 2 535 168 636 82 221 43 44 12 33 1 296 قائمة حماة الوطن 30
34 196 13 45 17 35 17 9 12 13 35 قائمة حركة الدستوريون األحرار 31
35 852 29 48 17 422 87 105 86 15 43 قائمة حزب التكتل 32
36 1 224 129 169 47 160 153 140 19 263 144 قائمة تيار املحبة 33
37 503 17 29 17 22 82 258 21 18 39 قائمة حزب البناء الوطني 34
38 125 007 13 621 17 260 6 902 20 782 14 614 21 250 5 378 7 707 17 496 األصوات املصرح بها لكل القائمات <NA>

Manually combining dataframes

If you’d rather not use a Flow, go page by page. .apply keeps it shorter than a for loop (a list comprehension works too):

import pandas as pd
dataframes = pdf.pages.apply(
lambda page: page.extract_table().to_df(header=None)
)
print("Found", len(dataframes), "tables")
df = pd.concat(dataframes, ignore_index=True)
df
Found 4 tables
0 1 2 3 4 5 6 7 8 9 10 11
0 الانتخاابت الترشيعية 2014 None None None None None None None None None None None
1 ورقة كشف نتائج دائرة مدنين None None None None None None None None None None None
2 مجموع األصوات\nاملصرح بها\nبالدائرة\nاالنتخابية توزيع األصوات املصرح بها حسب املعتمدية None None None None None None None None القائمات املترشحة ا\nل\nرت\nت\nي\nب
3 None مدنين\nالشمالية مدنين\nالجنوبية سيدي مخلوف جرجيس جربة ميدون جربة حومة\nالسوق جربة أجيم بني خداش بن قردان None None
4 426 63 43 56 58 49 55 33 32 37 قائمة من أجل تحقيق سلطة الشعب 1
5 3 001 419 806 197 289 217 616 59 118 280 قائمة حزب التحالف الديمقراطي 2
6 387 50 60 42 64 28 41 20 42 40 قائمة حزب العدالة والتنمية 3
7 1 193 135 87 43 170 167 327 39 181 44 قائمة الحزب الجمهوري 4
8 430 117 62 31 43 28 42 23 38 46 قائمة حزب الغد 5
9 3 066 93 127 123 1 422 322 640 68 36 235 قائمة حزب آفاق تونس 6
10 203 14 15 22 33 23 30 6 7 53 قائمة حزب االنفتاح والوفاء 7
11 192 14 13 14 20 18 21 7 7 78 قائمة اتحاد الشباب الديمقراطي التونس ي 8
12 1 617 53 59 33 127 282 883 110 25 45 قائمة حزب التيار الديمقراطي 9
13 267 23 32 21 33 25 18 15 21 79 قائمة حزب األمان 10
14 4 686 254 505 182 2 535 221 278 58 91 562 قائمة حركة الشعب 11
15 1 443 63 51 58 731 82 326 42 34 56 قائمة االتحاد من أجل تونس 12
16 803 96 96 65 119 78 91 47 73 138 قائمة شباب مدنين 13
17 373 28 24 28 38 37 113 30 25 50 قائمة حزب املجد 14
18 68 795 9 010 10 898 4 013 8 627 7 912 9 854 3 358 5 021 10 102 قائمة حزب حركة النهضة 15
19 309 29 25 17 39 41 41 54 21 42 قائمة تونس أقوى 16
20 204 23 40 9 19 20 22 2 13 56 قائمة الشعب يريد 17
21 119 4 7 6 15 16 44 7 9 11 قائمة الربيع العربي لتحقيق السيادة\nاالقتصادية 18
22 385 86 51 31 61 33 30 14 32 47 قائمة حزب صوت شعب تونس 19
23 576 129 130 54 36 58 87 8 24 50 قائمة الوحدة الوطنية املستقلة 20
24 14 334 1 096 1 219 950 3 186 1 738 3 477 492 608 1 568 قائمة حركة نداء تونس 21
25 2 788 101 118 30 877 400 696 119 48 399 قائمة الجبهة الشعبية 22
26 273 20 24 12 23 24 20 26 106 18 قائمة التآلف الوطني 23
27 901 62 112 48 88 74 107 25 56 329 قائمة حركة وفاء 24
28 4 392 449 382 223 565 749 808 163 177 876 قائمة حزب االتحاد الوطني الحر 25
29 244 67 21 9 30 14 19 7 56 21 قائمة حزب األصالة 26
30 249 28 25 50 30 20 24 39 15 18 قائمة حركة الديمقراطيين االشتراكيين 27
31 6 305 575 1 158 217 524 1 091 1 746 283 244 467 قائمة حزب املؤتمر من أجل الجمهورية 28
32 1 736 164 143 138 120 465 238 74 205 192 قائمة حزب املبادرة الوطنية الدستورية 29
33 2 535 168 636 82 221 43 44 12 33 1 296 قائمة حماة الوطن 30
34 196 13 45 17 35 17 9 12 13 35 قائمة حركة الدستوريون األحرار 31
35 852 29 48 17 422 87 105 86 15 43 قائمة حزب التكتل 32
36 1 224 129 169 47 160 153 140 19 263 144 قائمة تيار املحبة 33
37 503 17 29 17 22 82 258 21 18 39 قائمة حزب البناء الوطني 34
38 125 007 13 621 17 260 6 902 20 782 14 614 21 250 5 378 7 707 17 496 األصوات املصرح بها لكل القائمات <NA>

Take as much as possible, then clean it up later. We could spend time wrangling the spanning column headers on the first page, but it’s faster to grab everything and sort it out in pandas.

The header rows landed in the data — rows 2 and 3 hold the two levels of the spanning headers. Merge them into column names, drop the header rows, and de-space the numbers:

# Use row 3 as header, filling gaps from row 2
df.columns = df.iloc[3].fillna(df.iloc[2]).str.replace("\n", " ")
# Drop the header rows
df = df.iloc[4:].reset_index(drop=True)
# Remove spaces from numbers and convert to int
numeric_cols = df.columns[0:4]
df[numeric_cols] = df[numeric_cols].replace(r"\s+", "", regex=True).astype(int)
df
3 مجموع األصوات املصرح بها بالدائرة االنتخابية مدنين الشمالية مدنين الجنوبية سيدي مخلوف جرجيس جربة ميدون جربة حومة السوق جربة أجيم بني خداش بن قردان القائمات املترشحة ا ل رت ت ي ب
0 426 63 43 56 58 49 55 33 32 37 قائمة من أجل تحقيق سلطة الشعب 1
1 3001 419 806 197 289 217 616 59 118 280 قائمة حزب التحالف الديمقراطي 2
2 387 50 60 42 64 28 41 20 42 40 قائمة حزب العدالة والتنمية 3
3 1193 135 87 43 170 167 327 39 181 44 قائمة الحزب الجمهوري 4
4 430 117 62 31 43 28 42 23 38 46 قائمة حزب الغد 5
5 3066 93 127 123 1 422 322 640 68 36 235 قائمة حزب آفاق تونس 6
6 203 14 15 22 33 23 30 6 7 53 قائمة حزب االنفتاح والوفاء 7
7 192 14 13 14 20 18 21 7 7 78 قائمة اتحاد الشباب الديمقراطي التونس ي 8
8 1617 53 59 33 127 282 883 110 25 45 قائمة حزب التيار الديمقراطي 9
9 267 23 32 21 33 25 18 15 21 79 قائمة حزب األمان 10
10 4686 254 505 182 2 535 221 278 58 91 562 قائمة حركة الشعب 11
11 1443 63 51 58 731 82 326 42 34 56 قائمة االتحاد من أجل تونس 12
12 803 96 96 65 119 78 91 47 73 138 قائمة شباب مدنين 13
13 373 28 24 28 38 37 113 30 25 50 قائمة حزب املجد 14
14 68795 9010 10898 4013 8 627 7 912 9 854 3 358 5 021 10 102 قائمة حزب حركة النهضة 15
15 309 29 25 17 39 41 41 54 21 42 قائمة تونس أقوى 16
16 204 23 40 9 19 20 22 2 13 56 قائمة الشعب يريد 17
17 119 4 7 6 15 16 44 7 9 11 قائمة الربيع العربي لتحقيق السيادة\nاالقتصادية 18
18 385 86 51 31 61 33 30 14 32 47 قائمة حزب صوت شعب تونس 19
19 576 129 130 54 36 58 87 8 24 50 قائمة الوحدة الوطنية املستقلة 20
20 14334 1096 1219 950 3 186 1 738 3 477 492 608 1 568 قائمة حركة نداء تونس 21
21 2788 101 118 30 877 400 696 119 48 399 قائمة الجبهة الشعبية 22
22 273 20 24 12 23 24 20 26 106 18 قائمة التآلف الوطني 23
23 901 62 112 48 88 74 107 25 56 329 قائمة حركة وفاء 24
24 4392 449 382 223 565 749 808 163 177 876 قائمة حزب االتحاد الوطني الحر 25
25 244 67 21 9 30 14 19 7 56 21 قائمة حزب األصالة 26
26 249 28 25 50 30 20 24 39 15 18 قائمة حركة الديمقراطيين االشتراكيين 27
27 6305 575 1158 217 524 1 091 1 746 283 244 467 قائمة حزب املؤتمر من أجل الجمهورية 28
28 1736 164 143 138 120 465 238 74 205 192 قائمة حزب املبادرة الوطنية الدستورية 29
29 2535 168 636 82 221 43 44 12 33 1 296 قائمة حماة الوطن 30
30 196 13 45 17 35 17 9 12 13 35 قائمة حركة الدستوريون األحرار 31
31 852 29 48 17 422 87 105 86 15 43 قائمة حزب التكتل 32
32 1224 129 169 47 160 153 140 19 263 144 قائمة تيار املحبة 33
33 503 17 29 17 22 82 258 21 18 39 قائمة حزب البناء الوطني 34
34 125007 13621 17260 6902 20 782 14 614 21 250 5 378 7 707 17 496 األصوات املصرح بها لكل القائمات <NA>

This kind of cleanup is an exercise in data wrangling more than PDF work — if the row indices differ on your document, print df.head(6) first and adjust which rows become the header.