NewConnect Claude, Gemini, ChatGPT, and other AI agents to API Ninjas via our MCP server

How to Convert PDF to CSV

Image to Text APIUpdated August 28, 2026
Create a free API key and it will be pre-filled into every code sample on this page. No credit card required.
To convert a PDF to CSV, extract its tables with pdfplumber and write the rows out with Python's csv module — about ten lines for a digital PDF. Scanned PDFs take an OCR step through the Image to Text API first. This guide walks the whole path: extraction, cleanup, Excel export, and the scanned case.

Prerequisites

You need:

  • Python 3.8 or newer.
  • pdfplumber for digital PDFs; add pandas for the Excel step and pypdfium2 + requests for scanned tables.
  • For the OCR route only: a free API Ninjas API key — issued instantly, no credit card; existing accounts find it on the profile page.
shell
pip install pdfplumber pandas pypdfium2 requests

Step 1: Extract the tables

A PDF has no native concept of a table — just characters positioned on a page, sometimes with ruling lines. pdfplumber reconstructs the grid from those positions, which is why it works on digital PDFs and returns nothing for scans (those are handled in Step 4). The core is short:

pdf_to_csv.py
import csv import pdfplumber rows = [] with pdfplumber.open('report.pdf') as pdf: for page in pdf.pages: for table in page.extract_tables(): rows.extend(table) with open('report.csv', 'w', newline='') as f: csv.writer(f).writerows(rows) print(f'Wrote {len(rows)} rows to report.csv')

Step 2: Clean the rows

Real-world tables arrive with None cells, stray whitespace, and empty spacer rows. Normalize while collecting so the CSV comes out clean:

clean.py
import csv import pdfplumber def pdf_tables_to_csv(pdf_path: str, csv_path: str) -> int: rows = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: for table in page.extract_tables(): for row in table: cells = [(cell or '').strip() for cell in row] if any(cells): # drop fully empty rows rows.append(cells) with open(csv_path, 'w', newline='', encoding='utf-8') as f: csv.writer(f).writerows(rows) return len(rows) count = pdf_tables_to_csv('report.pdf', 'report.csv') print(f'{count} rows written')

Two small habits prevent the classic CSV bugs: open the output with newline='' (stops blank lines on Windows), and keep numbers as text for now — currency symbols and thousands separators break naive parsing, so convert deliberately later.

Step 3: Use pandas — or go straight to Excel

If the data is headed for analysis, a DataFrame gets you sorting, math, and one-line export to either format:

pandas_export.py
import pandas as pd import pdfplumber with pdfplumber.open('report.pdf') as pdf: tables = [t for page in pdf.pages for t in page.extract_tables()] # First row of the first table is the header df = pd.DataFrame(tables[0][1:], columns=tables[0][0]) df.to_csv('report.csv', index=False) # Or straight to Excel df.to_excel('report.xlsx', index=False)

Step 4: Handle scanned tables (OCR)

A scanned table has no characters to extract, so the pipeline becomes: render the page, OCR it, rebuild rows from coordinates. Every word the Image to Text API returns carries a bounding box, and words that share a y position belong to the same row:

scanned_table.py
import io import os import pypdfium2 as pdfium import requests session = requests.Session() session.headers['X-Api-Key'] = os.environ.get('API_NINJAS_KEY', 'YOUR_API_KEY') def ocr_words(image) -> list: buffer = io.BytesIO() image.save(buffer, format='PNG') buffer.seek(0) response = session.post( 'https://api.api-ninjas.com/v1/imagetotext', files={'image': ('page.png', buffer, 'image/png')}, timeout=30, ) response.raise_for_status() return response.json() def words_to_rows(words: list, tolerance: int = 14) -> list: """Group OCR words into table rows by bounding-box y position.""" rows = [] for word in sorted(words, key=lambda w: (w['bounding_box']['y1'], w['bounding_box']['x1'])): y = word['bounding_box']['y1'] if rows and abs(rows[-1][0] - y) <= tolerance: rows[-1][1].append(word) else: rows.append((y, [word])) return [ [w['text'] for w in sorted(ws, key=lambda w: w['bounding_box']['x1'])] for _, ws in rows ] pdf = pdfium.PdfDocument('scanned_report.pdf') image = pdf[0].render(scale=300 / 72).to_pil() for row in words_to_rows(ocr_words(image)): print(row)

Here's what the API sends back for a simple table — headers, then a data row:

api.api-ninjas.com
POST/v1/imagetotext
200 OKapplication/json
[ { "text": "Qty", "bounding_box": { "x1": 102, "y1": 340, "x2": 148, "y2": 366 } }, { "text": "Item", "bounding_box": { "x1": 210, "y1": 340, "x2": 270, "y2": 366 } }, { "text": "Price", "bounding_box": { "x1": 480, "y1": 340, "x2": 546, "y2": 366 } }, { "text": "2", "bounding_box": { "x1": 110, "y1": 384, "x2": 128, "y2": 410 } }, { "text": "Widget", "bounding_box": { "x1": 210, "y1": 384, "x2": 296, "y2": 410 } }, { "text": "19.99", "bounding_box": { "x1": 478, "y1": 384, "x2": 548, "y2": 410 } } ]

Feed the reconstructed rows into the same csv.writer as Step 2. For source images rather than PDFs — photographed receipts, screenshots — skip the render and POST the image directly; details in How to OCR a PDF.

Step 5: Handle multi-page tables

Long tables repeat their header on every page. Since extraction runs page by page in order, the fix is one comparison — keep the first header, skip its duplicates:

multipage.py
rows = [] header = None with pdfplumber.open('report.pdf') as pdf: for page in pdf.pages: for table in page.extract_tables(): for row in table: cells = [(cell or '').strip() for cell in row] if not any(cells): continue if header is None: header = cells # first row anywhere = header rows.append(cells) elif cells != header: # skip repeated headers rows.append(cells)

A final sanity check worth automating: every row should have the same cell count as the header — log the ones that don't for manual review instead of letting them shift columns silently.

Where to go from here

If you need prose rather than tables, that's How to Convert a PDF to Text; for the deep dive on scanned documents, How to OCR a PDF. The complete script — cleanup and repeated-header handling included — is below.

pdf_to_csv.py

The complete script from this guide: table extraction, row cleanup, and repeated-header handling for multi-page tables.

Frequently asked questions

How do I convert a PDF to CSV for free?

For digital PDFs, the open-source pdfplumber library extracts tables and Python’s built-in csv module writes them — completely free. For scanned PDFs, the Image to Text API has a free tier and returns word coordinates you can group into rows.

Why does extract_tables() return nothing?

Either the PDF is scanned (no text layer — use the OCR route), or the table has no ruling lines to detect. For borderless tables, pass text-based strategies: extract_tables({"vertical_strategy": "text", "horizontal_strategy": "text"}).

Can I convert a PDF to Excel instead of CSV?

Yes — load the rows into a pandas DataFrame and call df.to_excel("report.xlsx"). CSV is more portable; Excel preserves multiple sheets and formatting.

How accurate is converting a scanned table with OCR?

Clean scans at 300 DPI reconstruct reliably — the bounding boxes preserve column order. Faint scans or dense grids deserve a spot-check: compare a few totals against the original document.

How should I handle numbers in the extracted CSV?

Keep them as text at the CSV stage and parse later — currency symbols and thousands separators corrupt naive float parsing. pandas’ to_numeric with errors="coerce" is a good second pass.