攝影或3C

Python pandas 表格清理教學:空字串不是 pd.NA,notna() 會判斷為 True , dropna() 也刪不掉

## 1. 這篇在解決什麼問題?

這篇主要在解釋 `is_meaningful_table()` 裡面的判斷邏輯:

def is_meaningful_table(table_df: pd.DataFrame) -> bool:
    # Filter out empty tables and tiny fragments detected by PyMuPDF.
    # After normalize_table_df(), blank cells are NA, so notna() counts real content cells.
    if table_df.empty:
        return False

    # table_df.notna() gives a True/False grid; first sum counts non-NA cells per column,
    # second sum adds all columns into one total content-cell count.
    non_empty_cells = int(table_df.notna().sum().sum())
    return non_empty_cells >= 3

它的責任不是清理表格,
而是判斷清理後的表格是否值得輸出。

判斷規則很簡單:

如果 table_df 是空表格回傳 False
如果真正有內容的 cell 數量少於 3回傳 False
否則回傳 True

不過 `is_meaningful_table()` 能正確判斷,
有一個前提:傳進來的 `table_df` 必須已經先被清理過。

這個前置清理工作由 `normalize_table_df()` 負責。
它會把 PyMuPDF 抓到的表格整理成
比較適合 pandas 判斷的 DataFrame:

去掉文字前後空白
把空字串 "" 轉成 pd.NA
刪掉全空 row / column

在 `my_pdf2text_08.py` 裡,PyMuPDF 抓到表格後,
會先轉成 pandas `DataFrame`,
再交給 `normalize_table_df()` 清理:

table_df = normalize_table_df(pd.DataFrame(extracted_table))

接著才用 `is_meaningful_table()`
判斷這張表格是否值得輸出:

if not is_meaningful_table(table_df):
    print(f"[INFO] Page {page_number}: skip low-information table fragment #{table_index}")
    continue

目標是過濾掉 PyMuPDF 偵測到的
空表格、殘缺表格、低資訊表格碎片。

核心問題是:

dropna() 不會刪掉空字串 ""
notna() 也會把空字串 "" 當成 True

所以如果 PDF 表格裡有很多 `””` 或 `”   “`,
直接用 `dropna()` / `notna()` 會誤判。

## 2. 先建立示範 DataFrame

在 Jupyter / Interactive Window 裡先執行:

import pandas as pd

raw_df = pd.DataFrame([
    ["Part No", "Value", ""],
    ["R1", "10k", ""],
    ["", "", ""],
    ["C1", "0.1uF", ""],
    ["   ", "   ", ""],
])

raw_df

這張表看起來有很多空白格,
但那些空白格其實是空字串或空白字串:

""
"   "

它們不是 pandas 的缺值。

## 3. 為什麼 raw_df.notna() 全部都是 True?

執行:

“`python

raw_df.notna()

“`

你會看到全部 cell 都是 `True`。

原因是 pandas 認為下面這些是「有值」:

pd.notna("")
pd.notna("   ")

結果都是:True

因為 `””` 和 `”   “` 都是字串物件,不是缺值。

pandas 常見的缺值才是:

pd.NA
None
float("nan")

所以如果直接算:

raw_df.notna().sum()

會得到每一欄都是 5,
因為每個 cell 都被當成非缺值。

再算:

raw_df.notna().sum().sum()

會得到:

但這不是我們想要的「真正有內容的 cell 數」。

## 4. dropna() 也不會刪掉空字串

直接試:

raw_df.dropna(axis=0, how="all")

你會發現看起來空白的 row 沒有被刪掉。

原因一樣:

dropna() 只處理 NA / NaN
dropna() 不會把 "" 當成 NA

所以這一列:

["", "", ""]

在 pandas 眼中不是全 NA,而是三個空字串。

這一列:

["   ", "   ", ""]

也不是全 NA,而是三個字串。

## 5. 正確清理步驟一:先 strip 字串

先複製一份:

normalized_df = raw_df.copy()

然後把字串前後空白拿掉:

normalized_df = normalized_df.map(
    lambda value: value.strip() if isinstance(value, str) else value
)

normalized_df

這一步會把:

“`python

”   “

“`

變成:

“`python

“”

“`

也就是先把「只有空白的字串」統一整理成空字串。

## 6. 正確清理步驟二:把空字串轉成 pd.NA

接著執行:

normalized_df = normalized_df.replace("", pd.NA)

normalized_df

這一步才是真正關鍵。

因為做完之後,原本的空字串會變成
pandas 認得的缺值(“” -> <NA>):

現在再看:

normalized_df.notna()

只有真正有文字的 cell 會是 `True`,空格會變成 `False`。

## 7. 正確清理步驟三:刪掉全空 row / column

補充一個容易混淆的點:
“” 和 pd.NA 輸出到 Excel 後,看起來通常都是空白格。
但它們在 pandas 裡的語意不同:

所以把 `””` 轉成 `pd.NA` 不是為了 Excel 顯示,
而是為了讓 pandas 後面的判斷正確。

刪掉整列都是 NA 的 row:

normalized_df = normalized_df.dropna(axis=0, how="all")
# axis=0 == axis="index"
normalized_df

刪掉整欄都是 NA 的 column:

normalized_df = normalized_df.dropna(axis=1, how="all")
# axis=1 == axis="columns"

normalized_df

最後重設 index:

normalized_df = normalized_df.reset_index(drop=True)

normalized_df

這就是 `my_pdf2text_08.py` 裡的:

def normalize_table_df(table_df: pd.DataFrame) -> pd.DataFrame:
    normalized_df = table_df.copy()
    normalized_df = normalized_df.map(
        lambda value: value.strip() if isinstance(value, str) else value
    )
    normalized_df = normalized_df.replace("", pd.NA)
    normalized_df = normalized_df.dropna(axis=0, how="all")
    normalized_df = normalized_df.dropna(axis=1, how="all")
    return normalized_df.reset_index(drop=True)

## 8. 驗證 notna().sum().sum()

清理後執行:

normalized_df.notna()

再看每欄有幾個非 NA cell:

normalized_df.notna().sum()

這裡的第一個 `.sum()` 是沿著 column 統計:
每一欄各自有幾個 True
再執行:

normalized_df.notna().sum().sum()

第二個 `.sum()` 是把每一欄的數量再加總:
整張表格總共有幾個真正有內容的 cell
所以:

int(normalized_df.notna().sum().sum())

意思是:
把整張表格中非 NA 的 cell 數量算成一個 int。

## 9. 驗證 is_meaningful_table()

目前程式用這個函式判斷表格是否值得輸出:

def is_meaningful_table(table_df: pd.DataFrame) -> bool:
    if table_df.empty:
        return False

    non_empty_cells = int(table_df.notna().sum().sum())
    return non_empty_cells >= 3

可以在 Jupyter 裡加上 `print()` 觀察:

def is_meaningful_table_demo(table_df: pd.DataFrame) -> bool:
    if table_df.empty:
        print("empty table")
        return False

    non_empty_cells = int(table_df.notna().sum().sum())
    print("non_empty_cells =", non_empty_cells)
    return non_empty_cells >= 3


is_meaningful_table_demo(normalized_df)

如果清理後還有至少 3 個有內容的 cell,就會回傳:True

如果只有 1、2 個 cell 有內容,
就當成 PyMuPDF 偵測到的低資訊碎片,跳過不輸出。

## 10. 一次跑完整流程

import pandas as pd

raw_df = pd.DataFrame([
    ["Part No", "Value", ""],
    ["R1", "10k", ""],
    ["", "", ""],
    ["C1", "0.1uF", ""],
    ["   ", "   ", ""],
])


def normalize_table_df_demo(table_df: pd.DataFrame) -> pd.DataFrame:
    normalized_df = table_df.copy()
    normalized_df = normalized_df.map(
        lambda value: value.strip() if isinstance(value, str) else value
    )
    normalized_df = normalized_df.replace("", pd.NA)
    normalized_df = normalized_df.dropna(axis=0, how="all")
    normalized_df = normalized_df.dropna(axis=1, how="all")
    return normalized_df.reset_index(drop=True)


def is_meaningful_table_demo(table_df: pd.DataFrame) -> bool:
    if table_df.empty:
        return False

    non_empty_cells = int(table_df.notna().sum().sum())
    print("non_empty_cells =", non_empty_cells)
    return non_empty_cells >= 3


clean_df = normalize_table_df_demo(raw_df)

display(raw_df)
display(raw_df.notna())
display(clean_df)
display(clean_df.notna())

is_meaningful_table_demo(clean_df)

## 11. 結論

這段流程的重點是:
PyMuPDF 抽出的表格空格常常是 “”,不是 NA。

dropna() 不會處理 “”。

notna() 會把 “” 當 True。

所以要先 strip,再 replace(“”, pd.NA)。

之後 dropna() 和 notna().sum().sum() 才會得到合理結果。

推薦hahow線上學習python: https://igrape.net/30afN

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡