Data extraction and transformation with GPT models
Use GPT models to extract structured data from unstructured text and PDFs — covering NER, invoice parsing, document vision, and long-document chunking.
Use this file to discover all available pages before exploring further.
Vast amounts of enterprise data lives in formats that are difficult to work with programmatically: PDFs, scanned documents, free-form text fields, email threads, and hand-written notes. Traditional approaches — regular expressions, rule-based parsers, or OCR — break down when document layouts vary, languages mix, or the data relationships require reasoning to interpret. GPT models handle this naturally. By combining the model’s language understanding with Structured Outputs, you can reliably extract typed, validated data from almost any text source and feed it directly into downstream systems.
Extracting structured data with Structured Outputs
Structured Outputs guarantee that the model’s response conforms to a schema you define. You describe the shape of the data you want using a Pydantic model (Python) or a JSON Schema, pass it as the response_format, and the API returns a parsed object — no post-processing regex needed.The example below extracts invoice fields from a block of unstructured text:
from openai import OpenAIfrom pydantic import BaseModelclient = OpenAI()class Invoice(BaseModel): invoice_number: str vendor_name: str total_amount: float due_date: str line_items: list[str]completion = client.beta.chat.completions.parse( model="gpt-4o", messages=[ {"role": "system", "content": "Extract invoice data from the provided text."}, {"role": "user", "content": "Invoice #1234 from Acme Corp. Due March 15. Total: $450.00. Items: Widget A x2, Widget B x1."} ], response_format=Invoice)invoice = completion.choices[0].message.parsedprint(invoice.invoice_number) # "1234"print(invoice.total_amount) # 450.0print(invoice.line_items) # ["Widget A x2", "Widget B x1"]
client.beta.chat.completions.parse is a convenience wrapper that validates the response against your Pydantic model. If the model produces output that doesn’t match your schema, a ValidationError is raised rather than silently returning malformed data.
The system prompt matters. A focused instruction like "Extract invoice data from the provided text" gives the model a clear task. Avoid vague prompts like "Read this and help me" — specificity improves accuracy, especially when field names could be ambiguous.
Named Entity Recognition (NER) classifies spans of text into semantic categories: people, organizations, locations, dates, monetary amounts, and so on. GPT models perform NER without any fine-tuning — you describe the entity types you care about in the prompt, and the model finds and labels them.
from openai import OpenAIfrom pydantic import BaseModelclient = OpenAI()class Entity(BaseModel): text: str label: str # e.g. "PERSON", "ORG", "DATE", "MONEY" start_char: int end_char: intclass NERResult(BaseModel): entities: list[Entity]text = ( "On January 9, 2007, Steve Jobs announced the iPhone at Macworld " "in San Francisco. Apple's stock rose 8% that day.")completion = client.beta.chat.completions.parse( model="gpt-4o", messages=[ { "role": "system", "content": ( "You are a named entity recognition system. Extract all named entities " "from the text. Label each entity as one of: PERSON, ORG, LOCATION, DATE, MONEY, PRODUCT. " "Include the character offsets (start_char, end_char) for each entity." ), }, {"role": "user", "content": text}, ], response_format=NERResult,)for entity in completion.choices[0].message.parsed.entities: print(f"{entity.text!r:30s} {entity.label}")
NER is a foundation for text enrichment workflows: you can link extracted entities to a knowledge base (Wikipedia, an internal product catalog, a CRM) to produce annotated documents with structured metadata alongside the original prose.
PDFs and scanned documents present two distinct challenges: getting the text out, and making sense of it. GPT-4o’s vision capabilities let you skip the OCR step entirely by encoding document pages as base64 images and sending them directly.
This approach handles multilingual documents, mixed layouts, and tables without any format-specific parsing logic. GPT-4o adapts to the document’s structure rather than requiring you to specify it in advance.
Set dpi=150 when converting PDF pages to images. Lower resolutions can make small text illegible to the model; higher resolutions increase token consumption without a proportional accuracy gain.
GPT models have a finite context window, so documents longer than roughly 100,000 tokens need to be split into chunks and processed in passes. The pattern below uses tiktoken to split on token boundaries while preserving sentence integrity.
import tiktokenfrom openai import OpenAIclient = OpenAI()enc = tiktoken.encoding_for_model("gpt-4o")def chunk_text(text: str, max_tokens: int = 4000) -> list[str]: tokens = enc.encode(text) chunks = [] for i in range(0, len(tokens), max_tokens): chunk_tokens = tokens[i : i + max_tokens] chunks.append(enc.decode(chunk_tokens)) return chunksdef extract_from_chunk(chunk: str, question: str) -> str: response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "system", "content": ( "Extract the answer to the question from the provided text. " "If the text does not contain a relevant answer, respond with 'NOT FOUND'." ), }, {"role": "user", "content": f"Text:\n{chunk}\n\nQuestion: {question}"}, ], ) return response.choices[0].message.contentdef extract_from_document(document: str, question: str) -> list[str]: chunks = chunk_text(document) results = [extract_from_chunk(chunk, question) for chunk in chunks] return [r for r in results if r.strip() != "NOT FOUND"]
After collecting per-chunk answers, run a final synthesis pass:
def synthesize_answers(answers: list[str], question: str) -> str: combined = "\n---\n".join(answers) response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "system", "content": "Synthesize the following partial answers into a single, coherent response.", }, {"role": "user", "content": f"Question: {question}\n\nPartial answers:\n{combined}"}, ], ) return response.choices[0].message.content
Chunking on token boundaries can split sentences mid-clause. If extraction accuracy is critical, add a small overlap (e.g. 200 tokens) between chunks so boundary sentences appear in both the preceding and following chunk.
Best when the output schema is known in advance and you need validated, typed data. Eliminates post-processing and surfaces schema violations immediately.
Vision input (image/PDF)
Best for scanned documents, PDFs with complex layouts, or multilingual content where OCR would struggle. No preprocessing pipeline required.
NER with function calling
Best when you need entity spans with character offsets — for downstream annotation, linking to a knowledge base, or feeding into a search index.
Chunked extraction
Best for long documents that exceed the context window. Combine with a synthesis pass to produce a unified result from distributed extractions.