正如您將在即將發布的部落格文章中看到的那樣,我正處於金融知識時代。接近年底,我想看看我的數據:我繳了多少稅?我隨時待命的輪班賺了多少錢?多個 PDF 檔案並不是查看這些數據的最舒適方式,我想要一個可以在 Excel 中使用的單一 CSV 檔案。
像許多優秀的開發人員一樣,我懶得手動插入數字,所以我寫了一個腳本。如果你喜歡程式設計——和我一起冒險吧!如果您沒有心情 — 我將向您展示如何調整程式碼以符合您的薪資結構:D
This script receives a directory with payslip PDFs and returns a CSV file with the desired data |
main.py: # translate 1 pdf to 1 dict # loop over the pdf dir # save all dicts to 1 json file # translate json report to csv report
我們將首先編寫讀取 PDF 的程式碼,包括決定報告中需要哪些欄位。這是您需要調整以符合您的薪資結構的部分。一旦弄清楚,我們將迭代整個薪資目錄。
在第三步驟中,我選擇在 PDF 和 CSV 之間新增一個額外的步驟 - JSON 報告。一旦我們看到一切正常,我們將刪除該檔案的使用。
最後,我們會將 JSON 資料轉換為 CSV 檔案。然後,該 CSV 可以輕鬆轉換為 Google 試算表(只需按一下「開啟方式」)或 Excel(可在此處找到說明)。
這是一個簡單而美好的計劃,但你知道它是如何進行的 - 一路上會發現挑戰......你能猜出事情可能會變得複雜嗎?
在我們開始之前 - 重要提示:保密您的薪資單!如果您將專案上傳到 GitHub — 請確保不要分享這些個人詳細資訊!您可以使用 .gitignore 來實現此目的:
/payslips_pdf pdf_rows.txt report.json report.csv
我們開始吧?
我們將從閱讀 PDF 並列印所有行開始。這樣我們就會知道每一行出現的內容。這只需要完成一次(而報告可能會每月或每年創建一次),並且它不是報告的一部分 - 因此我們將在單獨的文件中創建它。
先建立一個新的 Python 檔案(我將其命名為 pdf_to_txt.py),然後編寫一個函數來讀取 pdf 並將結果列印到 .txt 檔案:
我們也會在主腳本中讀取PDF文件,所以將此功能移到那裡會更好。
現在我們知道了 PDF 的讀取結構 - 我們可以取得所需的值。就我而言 - 這是我感興趣的訊息:
請注意,表內有資料(每個月可能會有所不同的類別)和表外的資料。
表外資料:
付款期間 — 可以在第 19 行找到
總工資 — 這個規則很難找到規則,因為它出現在付款清單之後並且沒有標題「總工資」。
如前所述,付款和扣除可能會有所不同,並且並非每個月都相同。因此,不同月份的總工資可能會出現在不同的行中。
我確實注意到它出現在員工姓名之後 - 所以這就是我使用的。首先將其硬編碼添加,稍後我們將從外部獲取它。
Nett Pay:這個很簡單 - 它出現在第 17 行。
我將這些表外值收集到一個函數中:
付款和扣款詳情:這是最有趣的部分!我們將從剪切行數組開始,以在接下來的 for 循環中節省幾毫秒。然後,我需要區分 列表項目
和其他行。
我注意到在整個文件中,列表項目是唯一符合此規則的項目:以字母字元 開頭, 以數字字元 結尾和包含空格(最後一個條件是過濾掉my
中錯誤的行工資單,你可能不需要它)。
例如我們來看退休金項目:
main.py: # translate 1 pdf to 1 dict # loop over the pdf dir # save all dicts to 1 json file # translate json report to csv report
我不關心餘額(右邊的數字),但我關心代碼(G表示它是從稅前總工資中扣除的— 和 N 表示從淨工資中扣除- 稅後)。所以理想情況下,我們會有 json_obj["Pension (G)"]=150.00。
我們將使用空格來分隔線。最好有重複的空格——這樣我們就可以區分幾個單字之間的空間分割和幾個欄位之間的空間分割。
描述:
我們將找到第一個雙倍空格並用它分割。
代碼:
空格的數量取決於描述的長度,因此我們無法提前知道有多少個 - 這就是我也將使用 lstrip() 的原因。現在該行的其餘部分以非空格字元開頭。
並非所有清單項目都有程式碼,因此我們要檢查該行是否以程式碼或數字開頭。如果它是代碼 - 我將其包裝在 () 中(包括左括號前的空格),並將其附加到描述字串。如果沒有 - 不添加任何內容。
金額:
如果有程式碼-我們就會有更多的空間可以刪除。如果沒有,我們的行可能包含兩個金額:每月金額和餘額。
我注意到了 4 個案例:
/payslips_pdf pdf_rows.txt report.json report.csv
提取類別和代碼後,我們剩下:
PENSION G 150.00 587.49
為了涵蓋情況 2-3,我們將找到分隔金額的空格索引並剪掉尾部。它也適用於第一種情況,即沒有空格(也稱為沒有尾部)。
為了涵蓋案例4,我依賴行中具有單一金額的兩種類型類別之間的差異:第一種類型就像工資- 我們要在其中保存金額,第二種類型就像預扣稅——我們想忽略它。不同之處在於,只有扣除額會記錄表中的年度餘額 - 所以我正在檢查 -.
總而言之,這就是它的樣子:
這不是強制性步驟 - 我們可以使用 JSON 物件而不匯出值。我更喜歡看到它的樣子,至少在編碼階段是這樣。
縮放至多個 PDF 文件
原本,我以為我必須重新命名檔案(Payslip1.pdf -> Payslip01.pdf),但有一個更好的解決方案:
由於付款和扣除項目可能因工資單而異,因此本節不僅僅是直接翻譯。 CSV 是一個關係資料集,這意味著我們需要提前了解付款和扣除的所有類別,並在不存在工資單的情況下將條目保留為空。另一方面,JSON 是非關係型的,每個條目都指定其鍵。
考慮到這一點,我們的 CSV 報告的第一步是收集類別。所有類別。
收集分類:
現在,乍一看,您可能會認為使用 Set 來實現這一點 - 因為我們希望所有類別只出現一次。我已經嘗試過了。問題是套裝未列出,我發現匹配原始薪資單中出現的項目順序很重要。使用清單時,不要忘記在追加之前檢查清單中是否存在該項目:
必須
分開,但我希望薪資報告將所有付款放在右側,所有扣除額放在左側,而不是混合。
最後,我返回一個列表,因為將付款與扣除分開是沒有用的 - 分開是為了確保付款將顯示在右側,扣除將顯示在左側。
填充 CSV 表:
現在我們有了類別,我們可以開始填充 CSV 表:
您可以透過下載 VS 擴充 RainbowCSV(或任何其他 IDE 的平行版本)來使其更易於閱讀
一旦我們知道一切正常,我們就不需要寫入和讀取 JSON 檔案 - 我們可以直接使用 JSON 物件:
我們將直接使用 json_payslips,而不是使用 json_object(如 json_object = json.dumps(json_payslips)):
寫入:無需寫入report.json - 我們可以刪除此部分。
閱讀:將 json_payslips 直接傳遞給 json_to_csv() 函數:
一旦您準備好腳本 - 您就會想與您的同事和朋友分享!為了獲得良好的使用者體驗,我們將從命令列匯出員工姓名,而不是要求他們開啟程式碼。
閱讀論證
我們將從快樂路徑開始 - 假設使用者輸入員工姓名 - 並新增使用它的程式碼:
在 pdf_to_dict() 中,我們將從參數中讀取它:employee_name = sys.argv[1],而不是硬編碼 EMPLOYEE_NAME = "IFAT NEUMANN"。不要忘記導入 sys!
現在我們會考慮其他場景:
未提供員工姓名
如果使用者沒有輸入任何員工姓名怎麼辦?我們希望盡快抓住它,並通知他們!
因此,我們將在 main 函數的第一行新增一個檢查。現在,直覺是在那裡初始化employee_name變數 - 但這會導致函數屬性冒泡,直到它到達使用該變數的函數 - 我發現它不是一個非常乾淨的方法。
最後,我將嘗試存取此欄位 - 並捕獲它是否不存在:
main.py: # translate 1 pdf to 1 dict # loop over the pdf dir # save all dicts to 1 json file # translate json report to csv report
請注意,新增異常意味著 print_warning() 函數移至 main.py。否則,你會得到一個錯誤:
不帶引號的員工姓名
您可以跳過引號要求並循環參數,收集用戶名的所有部分 - 但我發現這種方法增加了不必要的複雜性。
員工的名字沒有出現在薪資單上
如果我們沒有薪資單上顯示的員工姓名,我們將無法找到總薪資。
最後,我們在主函數中捕獲錯誤:
以下是您可用於處理薪資單的完整代碼:
https://cupofcode.blog/ |
以上是週末編碼:將 PDF 薪資單轉換為單一 CSV 報告的詳細內容。更多資訊請關注PHP中文網其他相關文章!