🎯 Trình duyệt đám mây tùy chỉnh, chống phát hiện được hỗ trợ bởi Chromium tự phát triển, thiết kế dành cho trình thu thập dữ liệu webtác nhân AI. 👉Dùng thử ngay
Quay lại blog

Trích xuất PDF với Python: Từ phát hiện liên kết đến bảng đã trích xuất

Isabella Garcia
Isabella Garcia

Web Data Collection Specialist

06-Aug-2026

TL;DR:

  • Việc thu thập một PDF bắt đầu từ trang HTML liên kết nó. Một trang chỉ mục trực tiếp đã tạo ra bốn URL tài liệu trước khi bất kỳ phân tích nào bắt đầu, và bước khám phá đó là phần mà hầu hết các hướng dẫn bỏ qua.
  • Một PDF phải đến dưới dạng byte. Chuyển đổi cùng một tài liệu qua một điểm cuối trả về văn bản đã tạo ra 235,403 byte so với một tệp thực có 140,815 byte, và cả pypdf lẫn pdfplumber đều không thể đọc một trang nào từ kết quả.
  • Mở một tệp không phải là đọc nó. Xây dựng một trình đọc trên những byte hỏng đó không thu được gì cả; sự cố chỉ xuất hiện khi một trang được chạm vào.
  • pdfplumber cho bạn hình học trang, pypdf cho bạn cấu trúc tài liệu. Một trang trực tiếp đã trả về 5,659 ký tự văn bản, 4 bảng được phát hiện, và 933 từ được định vị.
  • Không phải mọi bảng được phát hiện đều là dữ liệu. Bốn bảng đó có 1, 7, 2 và 10 cột, và hầu hết chúng là khung bố trí hơn là các bản ghi.
  • Bắt đầu miễn phí. Lệnh tìm kiếm liên kết chạy trên cấp độ miễn phí của Universal Scraping API.

Các cơ quan công cộng xuất bản dữ liệu hữu ích nhất của họ dưới dạng PDF. Ngân sách, hồ sơ, thống kê và kết quả kiểm tra đến dưới dạng tài liệu được thiết kế để in, và một công cụ thu thập dừng lại ở HTML không bao giờ thấy được bất kỳ điều gì trong số đó.

Hướng dẫn này bắt đầu trên một trang liên kết bốn PDF, tải xuống một cái và rút ra văn bản, bảng và vị trí từ đó — sau đó đo chính xác những gì xảy ra khi tệp đi sai đường để đến đó.

PDF Là Gì, Với Mục Đích Thu Thập

Một PDF là một thùng chứa nhị phân mô tả nơi mà các dấu hiệu đi trên một trang. Nó không có khái niệm về một đoạn văn, một tiêu đề, hoặc một bảng — chỉ có các ký tự tại các toạ độ, một mô hình mô tả trang được lập chỉ mục trong mô tả định dạng của Thư viện Quốc hội cho gia đình PDF. Loại phương tiện của nó, được đăng ký trong đặc tả loại phương tiện ứng dụng/pdf, là nhị phân chính xác vì định dạng không phải là văn bản.

Sự thật đơn lẻ đó thúc đẩy mọi thứ bên dưới. Việc trích xuất "văn bản" có nghĩa là tái tạo thứ tự đọc từ các vị trí, và việc trích xuất "một bảng" có nghĩa là suy diễn các hàng và cột từ các đường kẻ và căn chỉnh. Hai thư viện chia công việc:

  • pypdf đọc cấu trúc tài liệu — số trang, siêu dữ liệu, trạng thái mã hóa, và văn bản ở cấp độ trang.
  • pdfplumber đọc hình học trang — các ký tự có tọa độ, các đường kẻ được phát hiện, và các bảng được xây dựng từ chúng.

Cài Đặt

bash Copy
pip install pdfplumber pypdf beautifulsoup4

pdfplumber kéo vào pdfminer.six, cái mà thực hiện phân tích ở mức độ thấp. Không có gì ở đây cần một gói hệ thống hoặc một trình duyệt không đầu.

Giai Đoạn 1: Khám Phá Các Liên Kết

Các tài liệu được tham chiếu từ các trang thông thường, vì vậy bước đầu tiên là công việc HTML. Lấy trang chỉ mục và thu thập mọi .pdf href, được giải quyết thành các URL tuyệt đối:

python Copy
import urllib.parse

from bs4 import BeautifulSoup


def pdf_links(html, base_url):
    soup = BeautifulSoup(html, "html.parser")
    return sorted({
        urllib.parse.urljoin(base_url, a["href"])
        for a in soup.select('a[href$=".pdf"]')
    })

urljoin quan trọng vì những href này thường là tương đối với trang web — trang đang được kiểm tra trả về /pub/irs-pdf/fw9.pdf, mà không thể lấy được riêng lẻ. set loại bỏ tài liệu liên kết nhiều hơn một lần, điều mà các trang chỉ mục thường xuyên thực hiện.

Trong một lần chạy trực tiếp trả về bốn tài liệu:

text Copy
https://www.irs.gov/pub/irs-pdf/fw9.pdf
https://www.irs.gov/pub/irs-pdf/iw9.pdf
https://www.irs.gov/pub/irs-pdf/p1281.pdf
https://www.irs.gov/pub/irs-pdf/p5027.pdf

Trang chỉ mục chính nó được truy xuất qua Universal Scraping API, cái mà trả về HTML được render dưới dạng chuỗi — đúng chính xác cho một trang, và hoàn toàn sai cho các tài liệu mà nó liên kết, như giai đoạn tiếp theo cho thấy.

Giai Đoạn 2: Tải Xuống Dưới Dạng Byte

python Copy
import urllib.request

BROWSER_UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
              "(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36")


def fetch_bytes(url):
    """Fetch a binary file. PDFs must arrive as bytes, never as text."""
    request = urllib.request.Request(url, headers={"User-Agent": BROWSER_UA})
    with urllib.request.urlopen(request, timeout=120) as response:
        return response.read()

response.read() mà không có .decode() là toàn bộ điểm chính. Tệp tải xuống có 140,815 byte bắt đầu bằng tiêu đề %PDF-, cái mà là chữ ký năm byte mà mọi tài liệu hợp lệ bắt đầu với — một khẳng định rẻ tiền xứng đáng để thực hiện trước khi phân tích.

Giai Đoạn 3: Đọc Cấu Trúc Tài Liệu

python Copy
import io

from pypdf import PdfReader

reader = PdfReader(io.BytesIO(raw))
print(len(reader.pages), reader.is_encrypted)

io.BytesIO tránh việc ghi một tệp tạm thời. Tài liệu trực tiếp báo cáo 6 trangencrypted=False. Việc mã hóa đáng để kiểm tra sớm: một tài liệu được mã hóa mở ra tốt và cung cấp văn bản trống, cái trông giống hệt như một tài liệu đơn giản không có lớp văn bản.

Giai Đoạn 4: Trích Xuất Văn Bản

python Copy
import pdfplumber

with pdfplumber.open(io.BytesIO(raw)) as pdf:
    page = pdf.pages[0]
    text = page.extract_text() or ""
    words = page.extract_words()

Trang một trả về 5,659 ký tự, mở ra trên dòng 'W-9', và 933 từ được định vị.

or "" không phải là padding phòng thủ. extract_text() trả về None khi một trang không có lớp văn bản — một hình ảnh quét, thường thấy — và None đó sẽ thất bại ở một nơi nào đó xa rời nguyên nhân của nó. Hãy coi đây như một tín hiệu rằng trang cần một công cụ khác, không coi như một chuỗi trống.
extract_words() là điều làm cho pdfplumber xứng đáng với sự phụ thuộc. Mỗi từ trở lại với x0, x1, topbottom tọa độ, vì vậy khi thứ tự đọc bị rối bởi một bố cục nhiều cột, bạn có thể chọn theo vị trí thay vì hy vọng dòng văn bản là hợp lý.

Giai đoạn 5: Trích xuất Bảng

python Copy
tables = page.extract_tables()
for index, table in enumerate(tables, 1):
    widest = max(len(row) for row in table)
    print(f"table {index}: {len(table)} rows x {widest} cols")

Trang web trực tiếp tạo ra bốn bảng:

text Copy
table 1: 4 rows x 1 cols
table 2: 2 rows x 7 cols
table 3: 1 rows x 2 cols
table 4: 2 rows x 10 cols

Một bảng một cột và một bảng một hàng không phải là bộ dữ liệu. Chúng là các vùng được đóng khung của bố cục biểu mẫu, thỏa mãn cùng một phương pháp luận về đường kẻ như một bảng thực tế. Đây là phần đáng lưu ý: extract_tables() báo cáo các cấu trúc hình chữ nhật mà nó phát hiện, và quyết định cái nào trong số đó chứa bản ghi là công việc của bạn. Lọc theo hình dạng trước khi tin tưởng vào bất kỳ điều gì — một mức tối thiểu hợp lý là hai hàng và hai cột, thắt chặt đến bất kỳ gì tài liệu của bạn thực sự sử dụng.

Bắt đầu không cần thẻ — kế hoạch miễn phí bao gồm việc thu thập phát hiện.

Điều Gì Xảy Ra Khi Một PDF Di Chuyển Dưới Dạng Văn Bản

Quy tắc đã nêu ở giai đoạn 2 xứng đáng với bằng chứng hơn là khẳng định, vì vậy cùng một tài liệu đã được lấy theo cách thứ hai — thông qua điểm cuối trả về văn bản được sử dụng cho trang chỉ mục — và kết quả được đo:

text Copy
same pdf through the text endpoint: 235403 bytes (real file is 140815)
  pypdf reads it: failed (PdfReadError)
  pdfplumber reads it: failed (PdfminerException)

Tệp đã tăng 67%. Giải mã các byte tùy ý dưới dạng văn bản và mã hóa lại chúng sẽ mở rộng bất kỳ điều gì bên ngoài phạm vi ASCII thành các chuỗi đa byte, cơ chế được mô tả trong đặc tả mã hóa UTF-8, và các byte mà chưa bao giờ là văn bản hợp lệ bị thay thế ngay lập tức. Kết quả vẫn bắt đầu với %PDF-, đó là lý do tại sao sự cố này đủ thuyết phục để lãng phí một buổi chiều.

Một chi tiết quan trọng hơn số lượng byte. Xây dựng một PdfReader trên dữ liệu đó không tạo ra gì cả — đối tượng được tạo ra, và chỉ chạm vào reader.pages là thất bại. Một kiểm tra dừng lại ở "nó có mở không" báo cáo thành công trên một tệp không thể đọc được, vì vậy hãy xác minh bằng cách đọc một trang:

python Copy
def page_count(data):
    return len(PdfReader(io.BytesIO(data)).pages)

Sử dụng API trả về văn bản cho HTML và một lấy dữ liệu trả về byte cho tài liệu. Hai cái này không thể thay thế cho nhau, và chế độ thất bại thì im lặng.

Toàn Bộ Quy Trình

python Copy
import io
import json
import logging
import os
import urllib.parse
import urllib.request

import pdfplumber
from bs4 import BeautifulSoup
from pypdf import PdfReader

logging.getLogger("pypdf").setLevel(logging.CRITICAL)
logging.getLogger("pdfminer").setLevel(logging.CRITICAL)

UNLOCKER = "https://api.scrapeless.com/api/v2/unlocker/request"
INDEX = "https://www.irs.gov/forms-pubs/about-form-w-9"
BROWSER_UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
              "(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36")


def fetch_html(url):
    """Fetch a rendered HTML page as text."""
    payload = json.dumps({
        "actor": "unlocker.webunlocker",
        "input": {"url": url, "proxy_country": "US", "js_render": False},
    }).encode()
    request = urllib.request.Request(
        UNLOCKER, data=payload,
        headers={"Content-Type": "application/json",
                 "x-api-token": os.environ["SCRAPELESS_API_KEY"]},
    )
    with urllib.request.urlopen(request, timeout=120) as response:
        return json.loads(response.read().decode())["data"]


def fetch_bytes(url):
    """Fetch a binary file. PDFs must arrive as bytes, never as text."""
    request = urllib.request.Request(url, headers={"User-Agent": BROWSER_UA})
    with urllib.request.urlopen(request, timeout=120) as response:
        return response.read()


def pdf_links(html, base_url):
    soup = BeautifulSoup(html, "html.parser")
    return sorted({
        urllib.parse.urljoin(base_url, a["href"])
        for a in soup.select('a[href$=".pdf"]')
    })


def main():
    html = fetch_html(INDEX)
    links = pdf_links(html, INDEX)
    print(f"index page: {len(html)} chars")
    print(f"pdf links discovered: {len(links)}")
    for link in links:
        print(f"  {link}")

    target = next(link for link in links if link.endswith("fw9.pdf"))
    raw = fetch_bytes(target)
    print(f"downloaded: {len(raw)} bytes, header {raw[:5].decode('latin-1')!r}")

    reader = PdfReader(io.BytesIO(raw))
    print(f"pypdf: {len(reader.pages)} pages, encrypted={reader.is_encrypted}")

    with pdfplumber.open(io.BytesIO(raw)) as pdf:
        page = pdf.pages[0]
        text = page.extract_text() or ""
        print(f"pdfplumber page 1: {len(text)} chars of text")
        print(f"  first line: {text.splitlines()[0][:60]!r}")

        tables = page.extract_tables()
        print(f"tables on page 1: {len(tables)}")
        for index, table in enumerate(tables, 1):
            widest = max(len(row) for row in table)
            print(f"  table {index}: {len(table)} rows x {widest} cols")

        print(f"positioned words on page 1: {len(page.extract_words())}")

    # A PDF is binary. Prove what happens if it travels as text.
    as_text = fetch_html(target).encode("utf-8")
    print(f"same pdf through the text endpoint: {len(as_text)} bytes "
          f"(real file is {len(raw)})")

    # Open AND read a page — a lenient constructor is not proof the file is usable.
    def read_with_pypdf(data):
        return len(PdfReader(io.BytesIO(data)).pages)

    def read_with_pdfplumber(data):
        with pdfplumber.open(io.BytesIO(data)) as opened:
            return len(opened.pages)

    for name, read in (("pypdf", read_with_pypdf), ("pdfplumber", read_with_pdfplumber)):
        try:
            print(f"  {name} reads it: {read(as_text)} pages")
        except Exception as exc:
            print(f"  {name} reads it: failed ({type(exc).__name__})")


if __name__ == "__main__":
    main()

Đầu ra của nó:

text Copy
index page: 99970 chars
pdf links discovered: 4
  https://www.irs.gov/pub/irs-pdf/fw9.pdf
  https://www.irs.gov/pub/irs-pdf/iw9.pdf
  https://www.irs.gov/pub/irs-pdf/p1281.pdf
  https://www.irs.gov/pub/irs-pdf/p5027.pdf
downloaded: 140815 bytes, header '%PDF-'
pypdf: 6 pages, encrypted=False
pdfplumber page 1: 5659 chars of text
  first line: 'W-9'
tables on page 1: 4
  table 1: 4 rows x 1 cols
  table 2: 2 rows x 7 cols
  table 3: 1 rows x 2 cols
  table 4: 2 rows x 10 cols
positioned words on page 1: 933
same pdf through the text endpoint: 235403 bytes (real file is 140815)
  pypdf reads it: failed (PdfReadError)
  pdfplumber reads it: failed (PdfminerException)

Hai dòng logging ở trên cùng là đáng giữ lại. Cả hai thư viện đều phát đi cảnh báo phục hồi trên đầu vào bị hỏng, và trên tài liệu bị hỏng đó, tiếng ồn chôn vùi dòng duy nhất điều quan trọng.

Khắc Phục Sự Cố

extract_text() trả về None hoặc một chuỗi trống. Trang không có lớp văn bản, điều này gần như luôn có nghĩa là nó là một hình ảnh quét. Không có số lượng cấu hình trình phân tích nào có thể phục hồi văn bản chưa bao giờ được mã hóa; công việc đó cần nhận dạng ký tự quang học, đây là một quy trình khác với các đặc điểm độ chính xác khác nhau.

Văn bản xuất hiện xen kẽ giữa các cột. Dòng văn bản theo thứ tự mà các ký hiệu được viết, không phải thứ tự đọc. Sử dụng extract_words() và nhóm theo tọa độ top, hoặc cắt trang với page.crop((x0, top, x1, bottom)) và trích xuất từng cột một cách riêng biệt.

extract_tables() không tìm thấy gì trên một trang rõ ràng có bảng. Chiến lược mặc định tìm kiếm các đường kẻ. Một bảng chỉ được tách bởi khoảng trắng đơn cần table_settings={"vertical_strategy": "text", "horizontal_strategy": "text"}, cái này suy luận các cột từ sự căn chỉnh.

Các ô chứa None. Các ô được hợp nhất và rỗng trở lại dưới dạng None thay vì "". Chuẩn hóa trước khi viết ở bất cứ đâu, và coi một hàng chủ yếu là None như một vật thể phát hiện thay vì một bản ghi.

Mọi tài liệu đều phân tích nhưng các số thì sai. Kiểm tra xem tệp có bị lấy dưới dạng byte hay không. Một tệp bị hỏng trong quá trình truyền vẫn có thể carry %PDF- header và vẫn tạo ra một đối tượng đọc, vì vậy hãy so sánh chiều dài tải xuống với Content-Length mà máy chủ đã báo cáo — trường được định nghĩa trong đặc tả nghĩa HTTP.

Kết Luận

Phần PDF của việc trích xuất PDF là phần dễ. Hai thư viện bao phủ nó: pypdf cho cấu trúc, pdfplumber cho bất kỳ điều gì phụ thuộc vào vị trí của mọi thứ trên trang.

Các phần có thể sai nằm ở cả hai bên. Tìm tài liệu là công việc HTML, và giữ chúng nguyên vẹn là câu hỏi vận chuyển với chế độ thất bại êm ả — một tệp đến 67% lớn hơn, vẫn bắt đầu với %PDF-, vẫn tạo ra một trình đọc, và không thể đọc được. Khẳng định trên số lượng byte và đọc một trang trước khi tin vào bất kỳ điều gì.
Bạn đã sẵn sàng để chỉ vào tài liệu của riêng bạn? Tạo một tài khoản miễn phí Scrapeless, xuất khóa của bạn và thực hiện giai đoạn khám phá trên một trang bạn đã thu thập. Giới hạn kế hoạch có trên trang giá cả. Nếu bạn chỉ cần văn bản thuần từ các định dạng tài liệu hỗn hợp thay vì hình học trang, hướng dẫn tài liệu đến Markdown sẽ hướng dẫn một con đường nhẹ nhàng hơn.

Câu hỏi thường gặp

H: Tôi nên sử dụng pdfplumber hay pypdf?

Sử dụng pypdf khi bạn cần các sự thật cấp tài liệu — số trang, siêu dữ liệu, trạng thái mã hóa, hợp nhất hoặc chia tách tệp — và pdfplumber khi bạn cần biết vị trí của các thứ trên trang, điều này bao gồm việc trích xuất bảng và bất kỳ bố cục nhiều cột nào. Chúng cùng tồn tại một cách hạnh phúc; quy trình trong bài viết này sử dụng cả hai, và pdfplumber là phụ thuộc nặng hơn vì nó mang một động cơ bố cục đầy đủ.

H: Tại sao việc tải xuống PDF của tôi thành công nhưng không thể phân tích?

Thường thì nó đã được giải mã dưới dạng văn bản ở đâu đó trong quá trình truyền. Lấy dữ liệu với response.read() và không .decode(), sau đó kiểm tra hai điều: rằng năm byte đầu tiên là %PDF-, và rằng độ dài khớp với những gì máy chủ đã công bố. Một vòng đi qua văn bản làm phình to tệp — 140,815 byte đã trở thành 235,403 trong phép đo này — trong khi vẫn giữ nguyên tiêu đề.

H: Tôi có thể trích xuất bảng từ một PDF quét không?

Không với các thư viện này. Một bản quét là một hình ảnh, và cả hai công cụ đọc các lớp văn bản và vector mà tài liệu mang theo. extract_text() trả về None là dấu hiệu. Khôi phục nội dung đó yêu cầu OCR, và đầu ra nên được coi là một ước lượng cần được xác thực hơn là dữ liệu đã được trích xuất.

H: Làm thế nào tôi tìm các liên kết PDF khi chúng không phải là neo thuần?

Mở rộng bộ chọn trước khi gọi đến trình duyệt: nhiều trang liên kết tài liệu thông qua một đường dẫn chuyển hướng mà không có hậu tố .pdf, vì vậy hãy so khớp theo mẫu URL hoặc trên văn bản liên kết thay vì đuôi. Nếu các liên kết thực sự được viết bằng JavaScript phía khách, trang chỉ mục cần được kết xuất — nhưng kiểm tra HTML ban đầu trước, vì các URL thường đã có ở đó.

H: extract_tables() có đủ đáng tin cậy cho sản xuất không?

Đối với các tài liệu với bảng có kẻ ô và bố cục ổn định, có, miễn là bạn xác thực hình dạng của những gì trở lại. Trang trực tiếp ở đây đã trả về bốn bảng trong đó hầu hết là các vùng bố cục, vì vậy bộ lọc cho số hàng và cột tối thiểu là không tùy chọn. Khi các bảng của tài liệu chỉ được định nghĩa bởi khoảng trắng, chuyển sang chiến lược dựa trên văn bản và kiểm tra lại kết quả với một trang mà bạn đã đọc.

H: Tôi nên xử lý các tài liệu rất lớn như thế nào?

Lặp lại pdf.pages thay vì tạo ra mọi thứ, và đóng tài liệu với trình quản lý ngữ cảnh như các ví dụ đã làm. Nếu bạn chỉ cần một phần của một tệp, page.crop() giới hạn công việc trong một khu vực, và pypdf có thể chia một tài liệu lớn thành những tài liệu nhỏ hơn trước khi công việc hình học tốn kém bắt đầu.

Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến ​​cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.

Bài viết phổ biến nhất

Danh mục