Fri Jun 26 2026 20:00:00 GMT-0400 (北美东部夏令时间)

Hướng dẫn nén ảnh bằng Python sử dụng Pillow, WebP và mozjpeg

Nén các định dạng ảnh JPEG, PNG và WebP trong Python bằng cách sử dụng thư viện Pillow, cwebp và mozjpeg. Hỗ trợ cài đặt chất lượng, vòng lặp kích thước mục tiêu và script batch tốc độ cao.

Hướng dẫn nén ảnh bằng Python sử dụng Pillow, WebP và mozjpeg

Last updated: June 27, 2026

Bạn có một thư mục các file JPEG kích thước camera, một bước build để gửi chúng đến người dùng, và một ngân sách page-weight liên tục bị vượt quá. Việc nén thủ công những file đó bằng trình chỉnh sửa ảnh không thể mở rộng quy mô được. Hướng dẫn này sẽ chỉ cho bạn chính xác đoạn Python cần thiết để thu nhỏ các file JPEG, PNG và WebP theo lịch trình, trong một job CI, hoặc bên trong bộ xử lý upload.

Câu trả lời nhanh: làm thế nào để nén ảnh bằng Python?

Mở ảnh bằng Pillow, sau đó lưu nó với các cờ định dạng (format flags) phù hợp. Đối với ảnh chụp, hãy lưu dưới dạng JPEG với quality=80, optimize=True, và progressive=True. Đối với các file hiện đại nhỏ nhất, hãy lưu dưới dạng WebP với quality=80, method=6. Đối với đồ họa phẳng và ảnh chụp màn hình, giữ PNG và truyền optimize=True.

from PIL import Image

def compress_jpeg(src, dst, quality=80):
    img = Image.open(src)
    if img.mode in ("RGBA", "P"):
        img = img.convert("RGB")
    img.save(dst, "JPEG", quality=quality, optimize=True, progressive=True)

Hàm bốn dòng đó đã cắt giảm 40 đến 70 phần trăm hầu hết các file JPEG từ điện thoại và DSLR. Phần còn lại của hướng dẫn này bổ sung thêm các mục tiêu kích thước, WebP, mozjpeg, và một bộ chạy hàng loạt (batch runner) để bạn có thể trỏ nó vào một thư mục và bỏ đi.

Bạn cần gì trước khi bắt đầu?

Bạn cần Python 3.9 trở lên và Pillow, bản fork được bảo trì của PIL. Pillow xử lý JPEG, PNG và WebP ngay lập tức trên mọi nền tảng phổ biến.

pip install Pillow

Để có đầu ra nhỏ nhất, bạn cũng sẽ muốn hai bộ mã hóa dòng lệnh (command-line encoders) mà Python có thể gọi: cwebp từ libwebp và cjpeg từ mozjpeg.

brew install webp mozjpeg   # macOS; Linux: apt install webp, build mozjpeg

Hãy kiểm tra mục đích của từng thành phần trước khi viết mã:

  • Pillow đọc và ghi mọi định dạng và thực hiện việc thay đổi kích thước (resizing).
  • cwebp tạo ra các file WebP nén chặt nhất và cung cấp nhiều tùy chỉnh hơn Pillow.
  • mozjpeg tái mã hóa JPEG nhỏ hơn tiêu chuẩn libjpeg mà Pillow sử dụng.
  • Một helper báo cáo để mỗi lần chạy in ra số byte bạn đã tiết kiệm được.

Đây là helper được sử dụng xuyên suốt hướng dẫn này:

import os

def report(src, dst):
    before, after = os.path.getsize(src), os.path.getsize(dst)
    saved = (1 - after / before) * 100
    print(f"{before/1024:.0f} KB -> {after/1024:.0f} KB ({saved:.0f}% smaller)")

Nếu bạn chỉ cần nén một lần và không muốn cài đặt bất cứ thứ gì, image compressor dựa trên trình duyệt cũng thực hiện công việc tương tự cho một số ít file.

Làm thế nào để nén JPEG bằng Pillow?

Ba cờ quan trọng đối với JPEG là quality, optimize, và progressive. quality kiểm soát mức độ chi tiết mà bộ mã hóa loại bỏ. optimize=True chạy một lần truyền thứ hai để xây dựng bảng Huffman nhỏ hơn. progressive=True sắp xếp lại file sao cho nó vẽ từ trên xuống dưới ở độ phân giải thấp trước, điều này cảm thấy nhanh hơn trên các kết nối chậm và thường làm giảm kích thước file thêm một chút nữa.

Laptop on a wooden desk showing source code open in an editor

Hãy chọn giá trị chất lượng dựa trên nơi hình ảnh được sử dụng, chứ không phải một con số toàn cầu duy nhất:

Quality value Typical use What to expect
90-95 Hero images, print proofs Near-original, noticeably larger files
75-85 Most web photos The safe default, large savings
60-70 Thumbnails and previews Visible softening on fine detail
Below 50 Avoid for photos Blocky artifacts around edges

Một lỗi thường gặp khiến hầu hết mọi người mắc phải: JPEG không có kênh alpha (alpha channel). Nếu bạn mở một ảnh chụp màn hình PNG có độ trong suốt và lưu nó trực tiếp sang JPEG, Pillow sẽ báo cannot write mode RGBA as JPEG. Lệnh convert("RGB") trong hàm trước đó đã làm phẳng độ trong suốt lên một nền màu đặc, đó là lý do tại sao nó chạy trước mọi lần lưu JPEG.

Nén PNG và chuyển đổi sang WebP

PNG là định dạng không mất dữ liệu (lossless), vì vậy bạn không thể đánh đổi chất lượng lấy kích thước như khi dùng JPEG. Điều bạn có thể làm là chạy bộ tối ưu hóa của Pillow và, đối với bất cứ thứ gì thực sự là ảnh chụp, hãy thay đổi hoàn toàn định dạng.

def compress_png(src, dst):
    Image.open(src).save(dst, "PNG", optimize=True, compress_level=9)

def to_webp(src, dst, quality=80):
    img = Image.open(src)
    img.save(dst, "WEBP", quality=quality, method=6)

method=6 yêu cầu bộ mã hóa WebP dành nhiều thời gian hơn để tìm kiếm một file nhỏ hơn. Nó chậm hơn cho mỗi ảnh nhưng đáng giá trong một bước build. Đối với một bức ảnh ban đầu là PNG 2 MB, WebP ở chất lượng 80 thường nằm dưới 200 KB mà trông giống hệt khi xem bình thường. Các phép đo của Google tự thân đã đặt WebP nhỏ hơn khoảng 25 đến 35 phần trăm so với JPEG tương đương, theo WebP documentation.

Sử dụng bảng này để chọn định dạng thay vì đoán:

Source content Best format Why
Photograph WebP or JPEG Smooth gradients compress well lossy
Logo, icon, flat UI PNG or WebP lossless Sharp edges stay crisp
Screenshot with text PNG or lossless WebP Avoids fuzzy text from lossy passes
Image needing transparency WebP or PNG JPEG cannot store an alpha channel

Nếu bạn vẫn đang phân vân giữa các định dạng cho toàn bộ trang web, bài phân tích trong WebP vs JPEG vs PNG bao gồm hỗ trợ trình duyệt và sự đánh đổi chi tiết hơn.

Đạt kích thước file mục tiêu trong một vòng lặp

"Làm sao để dưới 200 KB" là yêu cầu phổ biến từ các công cụ email, marketplace, và giới hạn upload của CMS. Bạn không thể chọn một con số chất lượng nào mà đạt được mục tiêu byte ngay lần đầu tiên, bởi vì cùng một chất lượng sẽ tạo ra kích thước khác nhau đáng kể cho một bức ảnh phức tạp so với một hình phẳng. Hãy lặp giảm từ chất lượng cao cho đến khi bạn nằm dưới mức giới hạn:

def compress_to_target(src, dst, target_kb=200, q=90, floor=35):
    img = Image.open(src).convert("RGB")
    while q >= floor:
        img.save(dst, "JPEG", quality=q, optimize=True, progressive=True)
        if os.path.getsize(dst) <= target_kb * 1024:
            return q
        q -= 5
    return q

Đối số floor ngăn vòng lặp giảm chất lượng xuống vùng bị nhòe (smear territory). Nếu một ảnh không thể đạt được mục tiêu trên mức sàn, hàm vẫn trả về để bạn có thể ghi lại và thay đổi kích thước ảnh thay vì nén mạnh hơn. Đối với các ngân sách byte cố định như 100 KB hoặc 50 KB, vòng lặp tương tự hoạt động; hướng dẫn chi tiết trong compress images without losing quality giải thích khi nào việc thay đổi kích thước tốt hơn là giảm chất lượng.

Làm thế nào để nén JPEG nhỏ hơn bằng mozjpeg và cwebp?

Pillow lưu các file JPEG với bộ mã hóa libjpeg tiêu chuẩn. mozjpeg là một encoder drop-in từ Mozilla tạo ra các file nhỏ hơn với cùng chất lượng hình ảnh, thường nhỏ hơn thêm 5 đến 15 phần trăm. Vì Pillow không đóng gói nó, hãy gọi binary cjpeg thông qua subprocess. cjpeg của mozjpeg đọc PPM đáng tin cậy, vì vậy hãy làm phẳng ảnh bằng Pillow trước:

import subprocess

def mozjpeg(src, dst, quality=80):
    Image.open(src).convert("RGB").save("_tmp.ppm")
    subprocess.run(["cjpeg", "-quality", str(quality), "-progressive",
                    "-optimize", "-outfile", dst, "_tmp.ppm"], check=True)
    os.remove("_tmp.ppm")

Đối với WebP, gọi cwebp trực tiếp cung cấp cho bạn các nút điều chỉnh mà Pillow không phơi bày, như -m 6 để nỗ lực tối đa và -sharp_yuv để cạnh màu sạch hơn:

def cwebp(src, dst, quality=82):
    subprocess.run(["cwebp", "-q", str(quality), "-m", "6",
                    "-sharp_yuv", src, "-o", dst], check=True)

Đây là cách các tùy chọn được sắp xếp để bạn có thể quyết định cài đặt cái gì:

Tool Best for Trade-off
Pillow General scripting, every format Not the smallest JPEG
mozjpeg Smallest progressive JPEG Extra binary, PPM round-trip
cwebp (libwebp) Tightest WebP control Separate binary to install
pillow-simd Faster resizing on x86 servers Harder to build and pin

Dự án mozjpeg tài liệu hóa bộ mã hóa và các bài kiểm tra hiệu suất của nó trong mozjpeg repository, và danh sách đầy đủ các tùy chọn lưu của Pillow nằm trong Pillow image file formats reference.

Làm thế nào để nén hàng loạt một thư mục nhanh chóng?

Một ảnh đơn lẻ thì dễ. Nhiệm vụ thực sự là một thư mục chứa hàng trăm ảnh. Thay đổi kích thước bất cứ thứ gì rộng hơn nhu cầu layout của bạn, sau đó nén, và chạy công việc trên các luồng (threads) để một thư mục 500 ảnh hoàn thành trong vài giây thay vì vài phút.

Developer at a multi-monitor workstation running code while wearing headphones

from pathlib import Path
from concurrent.futures import ThreadPoolExecutor

def batch(in_dir, out_dir, quality=80, max_width=1920):
    out = Path(out_dir); out.mkdir(parents=True, exist_ok=True)
    files = [p for p in Path(in_dir).iterdir()
             if p.suffix.lower() in {".jpg", ".jpeg", ".png"}]
    def work(p):
        img = Image.open(p)
        if img.width > max_width:
            h = round(img.height * max_width / img.width)
            img = img.resize((max_width, h), Image.LANCZOS)
        img.convert("RGB").save(out / f"{p.stem}.jpg", "JPEG",
                                quality=quality, optimize=True, progressive=True)
    with ThreadPoolExecutor() as pool:
        list(pool.map(work, files))
    return len(files)

Một vài lưu ý thực tế khi chạy cái này trong môi trường sản xuất:

  1. Thay đổi kích thước trước khi nén. Việc phục vụ một ảnh 6000 px cho cột 1200 px lãng phí byte mà không cài đặt chất lượng nào có thể khắc phục được.
  2. Pillow giải phóng GIL trong quá trình mã hóa, vì vậy luồng (threads) mang lại tốc độ thực tế ở đây; bạn không cần multiprocessing đối với hầu hết các thư mục.
  3. Viết ra một thư mục đầu ra riêng biệt để một lần chạy lỗi không bao giờ ghi đè lên bản gốc của bạn.
  4. Sử dụng Image.LANCZOS cho việc giảm kích thước; nó giữ cạnh sắc nét hơn bộ lọc mặc định.
  5. Ghi nhật ký mọi file bằng helper report và giữ lại bản gốc cho đến khi bạn đã xem xét mẫu thử.

Nếu công việc của bạn là định kỳ thay vì một lần, các mẫu trong batch image processing bao gồm hàng đợi (queues), thử lại (retries), và quy ước đặt tên. Đối với chạy không cần code trên một zip file, batch toolimage converter xử lý thay đổi định dạng mà không cần script.

Bạn nên chọn phương pháp nào?

Hãy khớp phương pháp với công việc thay vì luôn tìm đến công cụ nặng nhất:

  • Ảnh web trong bước build: Pillow WebP ở chất lượng 80, hoặc cwebp với -m 6 khi bạn muốn vài phần trăm cuối cùng.
  • Ngân sách byte nghiêm ngặt: vòng lặp compress_to_target, chuyển sang thay đổi kích thước khi chạm mức sàn.
  • Các pipeline JPEG cũ không thể thay đổi: tái mã hóa đầu ra bằng mozjpeg để tiết kiệm miễn phí.
  • Ảnh chụp màn hình và sơ đồ: giữ PNG với optimize=True, hoặc WebP lossless nếu trình duyệt của bạn hỗ trợ nó.

Để hiểu lý thuyết đằng sau tại sao các cài đặt này hoạt động, image compression guide giải thích về lossy so với lossless và cách bảng lượng tử hóa (quantization tables) ảnh hưởng đến kết quả.

Các lỗi thường gặp và cách khắc phục

Một số ngoại lệ chiếm phần lớn các lần chạy thất bại:

  • cannot write mode RGBA as JPEG — gọi img.convert("RGB") trước khi lưu sang JPEG.
  • Image.DecompressionBombError — Pillow bảo vệ chống lại các ảnh khổng lồ; chỉ nâng giới hạn cho những file bạn tin tưởng với Image.MAX_IMAGE_PIXELS = 200_000_000.
  • OSError: broken data stream — file nguồn bị cắt cụt; đặt ImageFile.LOAD_TRUNCATED_IMAGES = True để cứu nó, sau đó tải lại khi có thể.
  • Output lớn hơn input — bạn đã lưu lại một JPEG đã nén; hãy nén từ bản gốc, chứ không phải từ lần xuất trước đó.
  • Màu sắc trông bị rửa trôi — nguồn CMYK được lưu mà không chuyển đổi; gọi convert("RGB") trước.
from PIL import Image, ImageFile
Image.MAX_IMAGE_PIXELS = 200_000_000
ImageFile.LOAD_TRUNCATED_IMAGES = True

Detailed close-up image of a python resting on perforated surface.

Bài học chính

Pillow xử lý 90 phần trăm việc nén ảnh trong Python với ba cờ JPEG và một lệnh lưu WebP. Hãy tìm đến mozjpeg và cwebp khi bạn cần các file nhỏ nhất có thể, bọc mọi thứ trong vòng lặp kích thước mục tiêu khi ngân sách byte cố định, và chạy một thư mục qua hàm batch luồng khi khối lượng tăng lên. Giữ lại bản gốc của bạn, thay đổi kích thước trước khi nén, và in ra mức tiết kiệm trên mỗi lần chạy để bạn luôn biết pipeline thực sự đang làm gì.

Sử dụng các công cụ miễn phí trong khi bạn theo dõi hướng dẫn.

Ảnh bìa cho PNG sang WebP: Hướng dẫn Chuyển đổi và Thu nhỏ Ảnh PNG

Tue Mar 17 2026 20:00:00 GMT-0400 (北美东部夏令时间)

PNG sang WebP: Hướng dẫn Chuyển đổi và Thu nhỏ Ảnh PNG

Chuyển đổi PNG sang WebP để tối ưu hóa kích thước tệp web. Tìm hiểu khi nào nên dùng WebP không mất dữ liệu (lossless), khi nào dùng kiểu nén có tổn thất (lossy), cùng các lệnh cwebp và Pillow với fallback PNG.