Find duplicate Excel rows and separate them for review
Find rows with exact matches in all four columns and collect them in a new Excel file with their original row numbers. Review each duplicate group, including its first occurrence.
Merge several PDFs into one file, extract an inclusive page range into a second file, and verify the resulting page counts and order. A synthetic set of blank pages with deliberately different dimensions makes the result checkable by hand.
Who this is forThis guide is for people who need to combine PDFs or extract selected pages without modifying the original documents.
The workflow uses three source PDFs in an explicit order: alpha.pdf, beta.pdf, then gamma.pdf. Their pages are appended to one merged PDF. A second output then extracts pages 2 through 5 from the merged PDF, using ordinary human page numbering where the first page is page 1.
Python sequences use zero-based indexes, but this script accepts START_PAGE and END_PAGE as one-based inclusive page numbers. Pages 2 through 5 therefore correspond to Python slice positions 1 through 4, implemented as merged_reader.pages[START_PAGE - 1:END_PAGE].
The PDFs below are synthetic and were created specifically for this article. Each page is blank, but every page has a different width and height. That makes page order independently checkable without requiring another PDF-generation package.
from pathlib import Path
from pypdf import PdfWriter
SOURCE_DIR = Path("outputs") / "pdf_merge_split_demo"
PDFS = {
"alpha.pdf": [(100, 200), (110, 210)],
"beta.pdf": [(120, 220)],
"gamma.pdf": [(130, 230), (140, 240), (150, 250)],
}
SOURCE_DIR.parent.mkdir(parents=True, exist_ok=True)
SOURCE_DIR.mkdir() # Stop if the synthetic source already exists.
for filename, page_sizes in PDFS.items():
writer = PdfWriter()
for width, height in page_sizes:
writer.add_blank_page(width=width, height=height)
target = SOURCE_DIR / filename
with target.open("xb") as stream:
writer.write(stream)
python create_pdf_demo.py| Source PDF | Pages | Page dimensions in order |
|---|---|---|
| alpha.pdf | 2 | 100×200, 110×210 |
| beta.pdf | 1 | 120×220 |
| gamma.pdf | 3 | 130×230, 140×240, 150×250 |
The three source files contain 6 pages in total: 2 + 1 + 3. The dimensions are expressed in PDF user-space units. Their purpose here is simply to give every synthetic page a recognizable signature.
Because the inputs are explicitly listed, the merged PDF should contain the two alpha pages first, followed by the beta page, then the three gamma pages. The merged result should therefore contain 6 pages.
| Merged page | Source | Expected dimensions |
|---|---|---|
| 1 | alpha.pdf page 1 | 100×200 |
| 2 | alpha.pdf page 2 | 110×210 |
| 3 | beta.pdf page 1 | 120×220 |
| 4 | gamma.pdf page 1 | 130×230 |
| 5 | gamma.pdf page 2 | 140×240 |
| 6 | gamma.pdf page 3 | 150×250 |
Extracting merged pages 2 through 5 should therefore produce exactly 4 pages with dimensions 110×210, 120×220, 130×230, and 140×240 in that order.
Save the following script as pdf_merge_split.py. It validates every input file, rejects encrypted PDFs for this simple example, merges all pages, reopens the merged output to verify it, then creates the requested page-range file.
from pathlib import Path
from pypdf import PdfReader, PdfWriter
SOURCE_DIR = Path("outputs") / "pdf_merge_split_demo"
INPUTS = ("alpha.pdf", "beta.pdf", "gamma.pdf")
OUTPUT_DIR = Path("outputs") / "pdf_merge_split_result"
MERGED = OUTPUT_DIR / "merged.pdf"
EXTRACTED = OUTPUT_DIR / "pages_2_to_5.pdf"
START_PAGE = 2
END_PAGE = 5
def page_size(page) -> tuple[int, int]:
width = int(float(page.mediabox.width))
height = int(float(page.mediabox.height))
return width, height
def main() -> None:
source_root = SOURCE_DIR.resolve(strict=True)
if not source_root.is_dir():
raise ValueError("SOURCE_DIR must be a directory.")
if START_PAGE < 1 or END_PAGE < START_PAGE:
raise ValueError("Invalid page range.")
input_paths = [source_root / name for name in INPUTS]
for path in input_paths:
if not path.is_file():
raise FileNotFoundError(f"Missing input PDF: {path}")
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir() # Refuse to reuse an existing result folder.
expected_sizes = []
writer = PdfWriter()
for path in input_paths:
reader = PdfReader(path)
if reader.is_encrypted:
raise ValueError(f"Encrypted PDF is not supported here: {path.name}")
for page in reader.pages:
expected_sizes.append(page_size(page))
writer.add_page(page)
with MERGED.open("xb") as stream:
writer.write(stream)
merged_reader = PdfReader(MERGED)
if merged_reader.is_encrypted:
raise RuntimeError("Unexpected encrypted merged PDF.")
merged_sizes = [page_size(page) for page in merged_reader.pages]
if merged_sizes != expected_sizes:
raise RuntimeError("Merged page count or order does not match the inputs.")
if END_PAGE > len(merged_reader.pages):
raise ValueError(
f"Requested page {END_PAGE}, but merged PDF has only "
f"{len(merged_reader.pages)} pages."
)
selected_pages = merged_reader.pages[START_PAGE - 1:END_PAGE]
extracted_writer = PdfWriter()
expected_extract_sizes = []
for page in selected_pages:
expected_extract_sizes.append(page_size(page))
extracted_writer.add_page(page)
with EXTRACTED.open("xb") as stream:
extracted_writer.write(stream)
extracted_reader = PdfReader(EXTRACTED)
extracted_sizes = [page_size(page) for page in extracted_reader.pages]
if extracted_sizes != expected_extract_sizes:
raise RuntimeError("Extracted PDF does not match the selected pages.")
print(f"Merged {len(INPUTS)} PDFs into {len(merged_reader.pages)} pages.")
print(
f"Extracted pages {START_PAGE}-{END_PAGE}: "
f"{len(extracted_reader.pages)} pages."
)
print(f"Merged output: {MERGED.as_posix()}")
print(f"Extracted output: {EXTRACTED.as_posix()}")
if __name__ == "__main__":
main()
python pdf_merge_split.pyThe first output, merged.pdf, should contain 6 pages. The second output, pages_2_to_5.pdf, should contain 4 pages. The extraction is inclusive at both ends because the script converts the human page numbers into the corresponding Python slice.
| Output | Expected pages | Expected dimensions in order |
|---|---|---|
| merged.pdf | 6 | 100×200, 110×210, 120×220, 130×230, 140×240, 150×250 |
| pages_2_to_5.pdf | 4 | 110×210, 120×220, 130×230, 140×240 |
The expected console text below was derived manually from the synthetic inputs and code. It is not an execution log.
Merged 3 PDFs into 6 pages.
Extracted pages 2-5: 4 pages.
Merged output: outputs/pdf_merge_split_result/merged.pdf
Extracted output: outputs/pdf_merge_split_result/pages_2_to_5.pdfFor real documents, also inspect page content visually. Matching counts alone cannot reveal every possible issue, such as an unexpected input order or a source PDF whose page labels differ from its physical page positions.
| Symptom | What to check |
|---|---|
| ModuleNotFoundError: No module named pypdf | Install pypdf into the same Python environment with python -m pip install pypdf. |
| FileNotFoundError | Check SOURCE_DIR, INPUTS, and the terminal's working directory. Run the synthetic setup script first for this example. |
| FileExistsError | The result folder already exists. Review the previous outputs and choose a new folder rather than overwriting them. |
| Encrypted PDF is not supported here | Use a permitted decrypted copy or add an explicit password-handling workflow appropriate to your documents. |
| Requested page is beyond the merged PDF | Check the merged page count and the one-based START_PAGE and END_PAGE values. |
| Merged or extracted verification mismatch | Treat the generated file as unverified. Check the inputs and use a fresh output folder for the next run. |
If an exception happens after OUTPUT_DIR is created, partial files can remain. The script does not automatically delete them. Keep failed outputs separate from verified files.
This workflow verifies physical page count and page dimensions, not semantic content. It does not compare rendered pixels, extracted text, annotations, forms, bookmarks, named destinations, attachments, signatures, or accessibility structure.
PDF page labels can differ from physical page positions. A document might display roman numerals or custom labels even though pypdf still accesses its page objects by zero-based sequence position. The START_PAGE and END_PAGE values in this tutorial refer to physical sequence positions, not printed page numbers.
Some complex PDFs contain interactive forms, digital signatures, encrypted content, unusual object relationships, or features that require more deliberate handling. Merging a digitally signed PDF into a new document does not preserve the meaning of the original signature as a signature over the newly created file. For important records, inspect the relevant PDF requirements before treating a merged copy as equivalent to the original.
ChatGPT Python tool · Python 3.13.5 · pypdf 5.9.0
The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.