Document Loaders — Pulling in PDFs, Websites, CSVs, and More
Posted on Wed 19 August 2026 in GenAI
Before any LangChain pipeline does anything useful, it needs data. Document loaders are how that data gets in — they fetch content from a source, parse it, and return a list of Document objects with .page_content and .metadata. The rest of the pipeline (splitting, embedding, retrieval) doesn't care where the content came from. That's the point.
The Document object
Every loader returns the same structure:
from langchain.schema import Document
doc = Document(
page_content="This is the text content",
metadata={"source": "handbook.pdf", "page": 3}
)
.page_content is what gets embedded and fed to the LLM. .metadata is free-form — source file, URL, page number, timestamps — whatever you want to carry through for filtering or citation.
PDFs
The most common use case. Two loaders worth knowing:
# PyPDF — lightweight, good for most PDFs
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("report.pdf")
docs = loader.load() # One Document per page
# PDFPlumber — better for tables and layout-heavy docs
from langchain_community.document_loaders import PDFPlumberLoader
loader = PDFPlumberLoader("report.pdf")
docs = loader.load()
PyPDFLoader is faster. PDFPlumberLoader preserves layout better and handles tables more reliably. For scanned PDFs (image-based, no text layer), neither works — you need OCR. UnstructuredPDFLoader with mode="elements" can handle these via Tesseract.
Websites
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://docs.python.org/3/library/os.html")
docs = loader.load()
This fetches static HTML and strips the tags. Works fine for documentation sites, Wikipedia, blogs. For JavaScript-rendered pages (React/Next.js SPAs), the content won't be there — use SeleniumURLLoader or PlaywrightURLLoader instead, which spin up a real browser.
Loading multiple URLs at once:
urls = [
"https://example.com/page1",
"https://example.com/page2",
]
loader = WebBaseLoader(urls)
docs = loader.load()
CSVs
from langchain_community.document_loaders import CSVLoader
loader = CSVLoader(
file_path="employees.csv",
source_column="employee_id" # used as the metadata source field
)
docs = loader.load()
Each row becomes its own Document. The source_column parameter sets which column gets used as the source identifier in metadata — useful when you want to trace an answer back to a specific record.
For large CSVs with many columns, consider specifying which columns matter:
loader = CSVLoader(
file_path="data.csv",
csv_args={"fieldnames": ["name", "description", "date"]}
)
JSON and JSONL
from langchain_community.document_loaders import JSONLoader
loader = JSONLoader(
file_path="data.json",
jq_schema=".messages[].content", # jq syntax to extract the right field
text_content=True
)
docs = loader.load()
JSONLoader uses jq syntax to navigate nested JSON. jq_schema tells it exactly which field to pull as the page content. For JSONL (one JSON object per line), set json_lines=True.
Directories (bulk loading)
from langchain_community.document_loaders import DirectoryLoader
loader = DirectoryLoader(
path="docs/",
glob="**/*.pdf", # glob pattern for file types
loader_cls=PyPDFLoader, # which loader to use per file
show_progress=True
)
docs = loader.load()
DirectoryLoader walks a folder and applies the specified loader to each matching file. Mix it with different glob patterns and loader_cls values if you have a folder with mixed file types.
Notion, Google Drive, and databases
LangChain has loaders for these too, though they need extra auth setup:
# Notion
from langchain_community.document_loaders import NotionDBLoader
loader = NotionDBLoader(
integration_token="your-token",
database_id="your-db-id",
request_timeout_sec=30
)
# Google Drive
from langchain_community.document_loaders import GoogleDriveLoader
loader = GoogleDriveLoader(
folder_id="your-folder-id",
recursive=True
)
SQL databases have their own path — SQLDatabaseLoader runs a query and turns rows into documents. Useful for pipelines that need to answer questions against live relational data.
Custom loaders
If nothing built-in fits, subclass BaseLoader:
from langchain.document_loaders.base import BaseLoader
from langchain.schema import Document
class MyAPILoader(BaseLoader):
def __init__(self, endpoint: str):
self.endpoint = endpoint
def load(self) -> list[Document]:
import requests
response = requests.get(self.endpoint).json()
return [
Document(
page_content=item["text"],
metadata={"id": item["id"], "source": self.endpoint}
)
for item in response["items"]
]
The interface is just one method: load() returning a list of Document objects. That's all the rest of the pipeline needs.
What to watch for
Encoding issues — PDFs and older text files sometimes come with encoding problems. PyPDFLoader occasionally returns garbled text from scanned or password-protected files. Check doc.page_content before embedding — garbage in means garbage retrieved.
Large files — Loading a 500-page PDF returns 500 documents at once. If you're loading many large files, stream them in batches rather than calling .load() on everything at once.
Metadata hygiene — The metadata your loaders attach is what you'll use later for filtering. Set it deliberately. A chunk with metadata={"source": "file.pdf"} is much harder to filter than one with metadata={"source": "file.pdf", "section": "terms", "date": "2026-01"}.