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 requestsfrom 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)
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
Build a flow
Section titled “Build a flow”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)
Extract the table
Section titled “Extract the table”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)dfFound 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.
Clean up the data
Section titled “Clean up the data”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 2df.columns = df.iloc[3].fillna(df.iloc[2]).str.replace("\n", " ")
# Drop the header rowsdf = df.iloc[4:].reset_index(drop=True)
# Remove spaces from numbers and convert to intnumeric_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.