## 📄 DXF to CSV or TXT 程式筆記 因為 DXF 檔很難打開,所以想先將 DXF 檔案轉為文字檔案( CVS, TXT ) 再進行閱讀 整體流程:使用 `ezdxf` ,讀取`.dxf` 檔案,並提取特定類型的實體(`LINE`、`LWPOLYLINE`、`POLYLINE`、`TEXT`、`MTEXT`、`ATTRIB`),並拆解 `INSERT`(圖塊)來獲得內部的子物件。 最終,所有提取到的幾何座標與文字將被轉換並儲存到對應的 `.csv` 檔案中。 ### 函式分解:`write_header` 在 CSV 檔案的開頭寫入標頭行。 ```python def write_header(f: TextIO): ``` | 參數 | 類型 | 說明 | | :--- | :--- | :--- | | `f` | `TextIO` | 一個已開啟並可供寫入的文字檔案物件(即 CSV 檔案)。 | | **回傳** | `None` | | | **寫入標頭** | `f.write("Entity Type,Handle,...")` | 向檔案寫入固定的 CSV 標頭字串,定義後續數據的欄位名稱。 | ----- ### 函式拆解:`process_line` 處理 `LINE` (直線) 物件。 ```python def process_line(entity: DXFEntity, f: TextIO) -> int: ``` | 參數 | 類型 | 說明 | | :--- | :--- | :--- | | `entity` | `DXFEntity` | 從 `ezdxf` 讀取到的 `LINE` 實體物件。 | | `f` | `TextIO` | 目標 CSV 檔案的寫入物件。 | | **回傳** | `int` | 成功回傳 `1`,失敗回傳 `0`,用於主函式計數。 | #### 程式解析 | 行為 | 程式碼片段 | 說明 | | :--- | :--- | :--- | | **獲取座標** | `start = entity.dxf.start`<br>`end = entity.dxf.end` | 從 `LINE` 實體中提取 3D 的「起點」和「終點」座標。 | | **格式化輸出** | `output_line = (...)`<br>`f.write(output_line)` | 將實體類型、圖層、起點 (X1,Y1,Z1) 和終點 (X2,Y2,Z2) 格式化為 CSV 的一行並寫入檔案。座標格式化到小數點後 4 位。 | ----- ### 函式拆解:`process_lwpolyline` 處理 `LWPOLYLINE` (2D 輕量級聚合線),並將其**拆分為多條獨立線段**。 ```python def process_lwpolyline(entity: DXFEntity, f: TextIO) -> int: ``` | 參數 | 類型 | 說明 | | :--- | :--- | :--- | | `entity` | `DXFEntity` | `LWPOLYLINE` 實體物件。 | | `f` | `TextIO` | 目標 CSV 檔案的寫入物件。 | | **回傳** | `int` | 成功處理的**線段數量**。 | #### 程式解析 | 行為 | 程式碼片段 | 說明 | | :--- | :--- | :--- | | **獲取 2D 頂點** | `points: list = entity.get_points(format='xy')` | `LWPOLYLINE` 是 2D 的,使用 `get_points` 獲取所有 `(x, y)` 頂點列表。 | | **獲取 Z 座標** | `elevation = entity.dxf.get('elevation', 0.0)` | 獲取該聚合線的「高程」(Elevation) 屬性,它將作為所有頂點的 Z 座標。 | | **拆分線段** | `for i in range(len(points) - 1): ... f.write(...)` | 遍歷頂點列表,將每兩個連續的點 (i 和 i+1) 組合,並配上 `elevation` 的 Z 座標,作為一條 `LWPOLY_SEGMENT` 寫入 CSV。 | | **處理封閉線段** | `if entity.closed ... f.write(...)` | 檢查聚合線是否「封閉」。如果是,則額外寫入一條從「最後一個點」回到「第一個點」的線段。 | ----- ### 函式拆解:`process_polyline` 處理 `POLYLINE` (3D 聚合線),將其**拆分為多條獨立線段**。 ```python def process_polyline(entity: DXFEntity, f: TextIO) -> int: ``` | 參數 | 類型 | 說明 | | :--- | :--- | :--- | | `entity` | `DXFEntity` | `POLYLINE` 實體物件。 | | `f` | `TextIO` | 目標 CSV 檔案的寫入物件。 | | **回傳** | `int` | 成功處理的**線段數量**。 | #### 程式解析 | 行為 | 程式碼片段 | 說明 | | :--- | :--- | :--- | | **獲取 3D 頂點** | `points_coords: list[Vec3] = [v.dxf.location for v in entity.vertices]` | `POLYLINE` 是 3D 的,它本身包含 `VERTEX` 子實體。此行遍歷所有 `entity.vertices` 並直接提取每個頂點的 3D `location` 座標。 | | **拆分線段** | `for i in range(len(points_coords) - 1): ... f.write(...)` | 遍歷 3D 頂點列表,將每兩個連續的點 (i 和 i+1) 作為一條 `POLY_SEGMENT` 寫入 CSV。 | | **處理封閉線段** | `if entity.is_closed ... f.write(...)` | 檢查聚合線是否為「封閉」。如果是,則額外寫入一條從「最後一個點」回到「第一個點」的線段。 | ----- ### 函式拆解:`process_text` 統一處理 `TEXT` (單行文字)、`MTEXT` (多行文字) 和 `ATTRIB` (圖塊屬性文字)。 ```python def process_text(entity: DXFEntity, f: TextIO) -> int: ``` | 參數 | 類型 | 說明 | | :--- | :--- | :--- | | `entity` | `DXFEntity` | `TEXT`, `MTEXT`, 或 `ATTRIB` 實體物件。 | | `f` | `TextIO` | 目標 CSV 檔案的寫入物件。 | | **回傳** | `int` | 成功回傳 `1`,失敗回傳 `0`。 | #### 程式解析 | 行為 | 程式碼片段 | 說明 | | :--- | :--- | :--- | | **判斷類型並取值** | `if entity_type == 'TEXT': ... elif entity_type == 'MTEXT': ...` | 判斷實體類型。`MTEXT` 使用 `.text` 屬性取值,而 `TEXT` 和 `ATTRIB` 使用 `.dxf.text` 屬性。 | | **清理文字內容** | `text_content = str(...).strip().replace('\n', ' ')` | 標準化文字:去除頭尾空白,並將所有換行符 `\n` 替換為空格,以確保 CSV 格式正確。 | | **格式化輸出** | `f",,," # X2, Y2, Z2 留空`<br>`f'"{text_content}"\n'` | 將文字的 `insert` 點寫入 `X1,Y1,Z1` 欄位,`X2,Y2,Z2` 欄位留空。**文字內容用雙引號 `"` 包裹**,以防止文字中的逗號破壞 CSV 結構。 | ----- ### 函式拆解:`extract_dxf_data` **單一檔案處理的核心**。負責載入 DXF 檔案、遍歷實體、處理 `INSERT` (圖塊),並呼叫相應的 `process_...` 函式。 ```python def extract_dxf_data(dxf_path: str, output_path: str): ``` | 參數 | 類型 | 說明 | | :--- | :--- | :--- | | `dxf_path` | `str` | 來源 `.dxf` 檔案的完整路徑。 | | `output_path` | `str` | 數據匯出 `.csv` 檔案的完整路徑。 | | **回傳** | `None` | | #### 程式解析 | 行為 | 程式碼片段 | 說明 | | :--- | :--- | :--- | | **載入模型空間** | `doc = ezdxf.readfile(dxf_path)`<br>`msp = doc.modelspace()` | 讀取 DXF 檔案並獲取其 Modelspace。 | | **遍歷頂層實體** | `for entity in msp:` | 迭代模型空間中的所有**頂層**實體。 | | **處理 Block** | `if entity_type == 'INSERT':` | 檢查實體是否為 `INSERT`。| | **拆解** | `for sub_entity in entity.explode():` | 呼叫 `entity.explode()` 將圖塊拆解為其組成的基本物件(如 LINE, TEXT),並自動應用圖塊的位置、旋轉和縮放轉換。 | | **處理子物件** | `if sub_entity_type == 'LINE': ... process_line(sub_entity, f)` | 遍歷拆解後的**子物件**,根據其類型(`LINE`, `LWPOLYLINE` 等)呼叫對應的 `process_...` 函式進行處理。 | | **處理一般實體** | `elif entity_type == 'LINE': ... process_line(entity, f)` | 如果頂層物件**不是** `INSERT`(只是一般的 LINE、TEXT 等),則直接呼叫相應的 `process_...` 函式。 | | **匯總統計** | `print(f"提取的總線段數: {total_segments}")` | 處理完畢後,印出該檔案的處理摘要。| ----- ### 函式拆解:`batch_process_folder` **批次處理的啟動器**。遍歷指定資料夾中的所有 `.dxf` 檔案,並為每個檔案呼叫 `extract_dxf_data`。 ```python def batch_process_folder(input_folder: str, output_folder: str): ``` | 參數 | 類型 | 說明 | | :--- | :--- | :--- | | `input_folder` | `str` | 包含所有來源 `.dxf` 檔案的資料夾路徑。 | | `output_folder` | `str` | 用於儲存所有輸出 `.csv` 檔案的資料夾路徑。 | | **回傳** | `None` | | #### 程式解析 | 行為 | 程式碼片段 | 說明 | | :--- | :--- | :--- | | **確保輸出資料夾** | `os.makedirs(output_folder, exist_ok=True)` | 確保輸出資料夾存在。 | | **篩選 DXF 檔案** | `all_files = os.listdir(input_folder)`<br>`dxf_files = [f for f in all_files if f.lower().endswith('.dxf')]` | 讀取並篩選出副檔名為 `.dxf` 的檔案。 | | **建立輸出路徑** | `base_name = os.path.splitext(filename)[0]`<br>`output_filename = f"{base_name}.csv"` | 根據輸入的 DXF 檔名(例如 `drawing.dxf`),生成對應的 CSV 輸出檔名(例如 `drawing.csv`)。 | | **呼叫核心函式** | `extract_dxf_data(full_dxf_path, full_output_csv_path)` | 在迴圈中,為每個 DXF 檔案呼叫 `extract_dxf_data` 函式,傳入完整的輸入和輸出路徑,以執行單一檔案的數據提取。 | | **批次完成** | `print(f"成功處理 {total_files_processed} / {len(dxf_files)} 個檔案。")` | 所有檔案處理完畢後,印出最終的批次處理匯總報告。 |