---
# System prepended metadata

title: 在PDF上擷取表格並輸出成CSV、EXCEL檔案
tags: [openData]

---

# 擷取PDF單一頁面的表格

```python=
import pdfplumber
import pandas as pd

# 打開PDF文件
with pdfplumber.open("study_list.pdf") as pdf:
    # 獲取PDF的第一頁
    first_page = pdf.pages[0]
    
    # 提取表格
    table = first_page.extract_table()

    # 將表格轉換為DataFrame
    df = pd.DataFrame(table[1:], columns=table[0])

    # 儲存成CSV文件
    df.to_csv("output.csv", index=False)

print("Table extracted and saved to output.csv")
```
* study_list.pdf
![image](https://hackmd.io/_uploads/rk3SVJStA.png)


* output.csv
![image](https://hackmd.io/_uploads/rJrwEyHtA.png)


# 從多頁的PDF擷取指定表格

```python=
import pdfplumber
import pandas as pd

# 獲得輸入的頁碼和表格索引
page_number = int(input("请輸入要提取表格的頁碼（從1開始）："))
table_index = int(input("请輸入要提取表格的索引（從1開始）："))

# 打開PDF文件
with pdfplumber.open("Test_Fake_File.pdf") as pdf:
    # 檢查頁碼是否有效
    if page_number > len(pdf.pages) or page_number < 1:
        raise ValueError("無效的頁碼")
    
    # 獲得頁碼對應的頁面
    page = pdf.pages[page_number - 1]
    
    # 提取頁面中所有表格
    tables = page.extract_tables()
    
    # 檢查表格索引是否有效
    if table_index > len(tables) or table_index < 1:
        raise ValueError("無效的表格索引")
    
    # 抓取指定的表格
    specific_table = tables[table_index -1]

    # 將表格轉換為DataFrame
    df = pd.DataFrame(specific_table[1:], columns=specific_table[0])

    # 儲存成Excel文件
    df.to_excel("output.xlsx", index=False)

print("Table extracted and saved to output.xlsx")
```

* Test_Fake_File.pdf
![image](https://hackmd.io/_uploads/B1YxIJSY0.png)
![image](https://hackmd.io/_uploads/r19zL1SF0.png)
![image](https://hackmd.io/_uploads/rylEIkrFC.png)

* output.xlsx
(擷取條件：第2頁第2個表格)
![image](https://hackmd.io/_uploads/HkAsL1BYA.png)
![image](https://hackmd.io/_uploads/H1i_U1rFR.png)

# 處理具有合併儲存格的表格

```python=
import pdfplumber
import pandas as pd
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
from openpyxl.styles import Font, Alignment

def extract_table_from_pdf(pdf_path, page_number, table_index):
    # 打開 PDF 文件
    with pdfplumber.open(pdf_path) as pdf:
        # 檢查頁碼是否有效
        if page_number > len(pdf.pages) or page_number < 1:
            raise ValueError("無效的頁碼")
        
        # 獲取指定頁
        page = pdf.pages[page_number - 1]  # 頁碼從1開始，索引從0開始
        
        # 提取頁面中的所有表格
        tables = page.extract_tables()
        
        # 檢查表格索引是否有效
        if table_index > len(tables) or table_index < 1:
            raise ValueError("無效的表格索引")
        
        # 獲取指定表格
        specific_table = tables[table_index - 1]  # 表格索引從1開始，索引從0開始

        return specific_table

def create_excel_with_merged_cells(table_data, output_path, merge_index):
    wb = Workbook()
    ws = wb.active

    # 填充表格數據
    for i, row in enumerate(table_data):
        for j, cell in enumerate(row):
            if cell is not None:
                ws.cell(row=i+1, column=j+1, value=cell)

    # 自動調整欄寬
    for col in ws.columns:
        max_length = 0
        column = col[0].column  # 取得列字母
        for cell in col:
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(cell.value)
            except:
                pass
        adjusted_width = (max_length + 2)  # 可以調整加的寬度
        ws.column_dimensions[get_column_letter(column)].width = adjusted_width

    # 將合併的儲存格加粗 & 置中
    ws.merge_cells(merge_index)
    mergecell = ws[merge_index.split(':')[0]]
    mergecell.font = Font(bold=True)  # 設定字型為粗體
    mergecell.alignment = Alignment(horizontal='center', vertical='center')  # 設定置中對齊

    # 保存 Excel 文件
    wb.save(output_path)

def main():
    # 獲取使用者輸入的頁碼和表格索引
    pdf_path = "Text_Fake_File_combine.pdf"
    page_number = int(input("請輸入要提取表格的頁碼（從1開始）："))
    table_index = int(input("請輸入要提取表格的索引（從1開始）："))
    merge_index = str(input("請輸入合併的儲存格座標（例如A1:A4、或B1:C2等格式）："))

    # 提取表格
    table_data = extract_table_from_pdf(pdf_path, page_number, table_index)

    # 保存為 Excel 文件
    output_path = "output_combine.xlsx"
    create_excel_with_merged_cells(table_data, output_path, merge_index)
    print(f"Table extracted and saved to {output_path}")

if __name__ == "__main__":
    main()
```

* Text_Fake_File_combine.pdf
![image](https://hackmd.io/_uploads/rkrDUWIK0.png)
![image](https://hackmd.io/_uploads/BktO8bLY0.png)
![image](https://hackmd.io/_uploads/SJAcFZItR.png)

## 情境1：表頭在最上方

==(擷取條件：第1頁第1個表格，表頭在第一行)==

![image](https://hackmd.io/_uploads/HynT1G8tA.png)
![image](https://hackmd.io/_uploads/BJE1jZIFR.png)

## 情境2：表頭在中間

==(擷取條件：第2頁第1個表格，表頭在第二行)==

![image](https://hackmd.io/_uploads/SyK7eGLKR.png)
![image](https://hackmd.io/_uploads/ryPtj-IKR.png)

## 情境3：表頭在奇怪的位置

==(擷取條件：第3頁第1個表格，表頭在第中間跨行，約是B1:C2)==

![image](https://hackmd.io/_uploads/HJfOefIY0.png)
![image](https://hackmd.io/_uploads/rytFlG8FA.png)

# 使用OCR辨識單一圖片內的表格內容

透過`pytesseract`套件對圖片進行光學辨識，首先要去[tesseract的GitHub](https://github.com/tesseract-ocr/tesseract?tab=readme-ov-file)下載驅動程式：
![image](https://hackmd.io/_uploads/HJoswN22A.png)
![image](https://hackmd.io/_uploads/B1-UONh30.png)
![image](https://hackmd.io/_uploads/BkA_d4h2C.png)

```python=
import pytesseract
from PIL import Image
import pandas as pd

ocrpath = "Your tesseract path."

# 如果你使用的是Windows，設置Tesseract的路徑
pytesseract.pytesseract.tesseract_cmd = ocrpath

# 打開圖片
img_path = 'table_image.png'  # 圖片文件的路徑
image = Image.open(img_path)

# 使用OCR識別圖片中的表格文本
ocr_result = pytesseract.image_to_string(image, lang='chi_tra')

# 將OCR的結果進行處理，將文本轉換為列表
lines = ocr_result.split("\n")
data = []
for line in lines:
    # 根據圖片中的排版，通過分隔符（如空格、Tab）來拆分表格行
    row = line.split()
    if row:  # 如果該行有內容
        data.append(row)

# 使用pandas將數據轉換為表格格式
df = pd.DataFrame(data)

# 將表格數據保存為Excel文件
df.to_excel("output_ocr.xlsx", index=False, header=False)

print("表格已成功轉換並保存至 'output_ocr.xlsx'")
```
* table_image.png
![table_image](https://hackmd.io/_uploads/B1dGFEn20.png)


==但是中文辨識的成效並不太好......==
![image](https://hackmd.io/_uploads/r1Z4tN22C.png)

# 處理被旋轉過的PDF表格
這邊以聯詠2023年的年報為範例(LY_2023.pdf)：
![pdf_ss_13](https://hackmd.io/_uploads/BkU-sN230.png)

首先，把被旋轉過的頁面轉回來。
```python=
import fitz  # PyMuPDF

# 打開 PDF 文件
pdf_file = "LY_2023.pdf"  # 替換成你的 PDF 檔案
output_pdf = "rotated_output_LY.pdf"  # 保存結果的文件名稱
doc = fitz.open(pdf_file)

# 要旋轉的頁面列表 (頁碼從 1 開始)
pages_to_rotate = [19, 20, 21, 22]
rotation_angle = 90  # 順時針旋轉 90 度

# 針對每個指定頁面進行旋轉
for page_num in pages_to_rotate:
    page = doc.load_page(page_num - 1)  # PDF 頁碼從 0 開始
    page.set_rotation(rotation_angle)  # 設定旋轉角度

# 保存旋轉後的 PDF 文件
doc.save(output_pdf)
doc.close()

print(f"Rotated pages saved to '{output_pdf}'")
```
* rotated_output_LY.pdf
![image](https://hackmd.io/_uploads/HyuYiVh2C.png)
:::warning
rotation_angle在少數情況下需要設定成45度，才會轉動頁面，原理目前還不清楚。
:::
然後，重新用pdfplumber讀取表格資料：
```python=
import pdfplumber
import pandas as pd

# 打開PDF文件
with pdfplumber.open("rotated_output_LY.pdf") as pdf:
    # 獲取PDF的第一頁
    first_page = pdf.pages[12]
    
    # 提取表格
    table = first_page.extract_table()

    # 將表格轉換為DataFrame
    df = pd.DataFrame(table[1:])
    print(df)

    # 儲存成CSV文件
    df.to_csv("output_GY.csv", index=False)

print("Table extracted and saved to output.csv")
```
![image](https://hackmd.io/_uploads/HyopoEnnR.png)

# 處理斜線標題表格
![pdf_ss_15](https://hackmd.io/_uploads/rJDAkB23C.png)
其實pdfplumber也可以正常讀取資料，但這邊想呈現的效果，是用箭頭指出橫向標題是"條件"，縱向標題是"姓名"：
```python=
import pdfplumber
import pandas as pd

# 打開PDF文件
with pdfplumber.open("LY_2023.pdf") as pdf:
    # 獲取PDF的第一頁
    first_page = pdf.pages[15]
    
    # 提取表格
    table = first_page.extract_table()

    # 將表格轉換為DataFrame
    df = pd.DataFrame(table[0:])

    # 取得 [0, 0] 的欄位內容
    cell_content = df.iloc[0, 0]

    # 使用 '\n' 切割內容
    title = cell_content.split('\n')

    # 覆寫 [0, 0] 的內容為新的格式 "{title[0]}→\\n{title[1]↓}"
    if len(title) > 1:
        df.iloc[0, 0] = f"{title[0]}→\n{title[1]}↓"
    else:
        print("The content does not contain a '\\n' to split.")

    print(df)

    # 儲存成CSV文件
    df.to_csv("output_slope.csv", index=False)

print("Table extracted and saved to output.csv")
```
![image](https://hackmd.io/_uploads/rk0heHn3C.png)

# 小工具：PDF頁面擷取
可以將指定頁數的PDF頁面截圖存檔。
```python=
import fitz  # PyMuPDF

# 打開 PDF 文件
pdf_file = 'LY_2023.pdf'  # 替換成你的 PDF 檔案路徑
doc = fitz.open(pdf_file)

# 需要截圖的頁面
pages_to_screenshot = [13]

# 針對每個頁面進行截圖並保存
for page_num in pages_to_screenshot:
    page = doc.load_page(page_num - 1)  # PyMuPDF 的頁面是從 0 開始計算
    pix = page.get_pixmap()  # 將頁面轉換為圖像
    image_file = f'pdf_ss_{page_num}.png'
    pix.save(image_file)  # 保存為 PNG 檔案
    print(f'Saved: {image_file}')

# 關閉 PDF 文件
doc.close()
```

:::danger
**★ 關於PDF複製內容後出現亂碼**
在複製聚陽(1477)的年報內容時出現以下亂碼，檢查後發現文件有內嵌以下字體：

![image](https://hackmd.io/_uploads/B1zqu__3R.png)
![image](https://hackmd.io/_uploads/ryXpuuu2C.png)

* DFKaiShu-SB-Estd-Bf-ETen-B5-H
* Helvetica-Bold
* PMingLiU-ETen-B5-H
* PalatinoLinotype-Bold
* PalatinoLinotype-Roman
* TimesNewRomanPS-BoldMT
* TimesNewRomanPSMT

PDF檔案會正常顯示，不過當複製貼上、以及用程式讀取PDF內容時，就會變成亂碼，下載字體後仍無法正常讀取，將PDF轉成圖片進行OCR效果不好，Google文件、文件轉換程式、修改檔案編碼等方法只能得到充滿亂碼的新文件，目前仍不知道該如何處理。

- [字型為「DFKAI-sb 標楷體」，pdf中的字型會變成破字](https://forum.aspose.com/t/aspose-pdf-html-pdf-dfkai-sb-pdf/169286)
- [How to solve no unicode mapping error from PDFBox?](https://stackoverflow.com/questions/58829597/how-to-solve-no-unicode-mapping-error-from-pdfbox)
