戰(zhàn):從手動拉表到一鍵生成Excel報表全流程)
1. 從手動拉表半小時到一鍵跑完全部我為什么要做這個Python自動化事情的起因很樸素。我所在的部門每天上午都要從公司幾個不同的系統(tǒng)里導(dǎo)出數(shù)據(jù)然后手工清洗、合并、算指標(biāo)最后整理成一張Excel報表發(fā)給領(lǐng)導(dǎo)。數(shù)據(jù)源包括Oracle數(shù)據(jù)庫、一個內(nèi)部管理系統(tǒng)網(wǎng)頁、還有兩三個Excel模板里的歷史數(shù)據(jù)。一開始數(shù)據(jù)量小每天二十分鐘能搞定但隨著業(yè)務(wù)量漲上來單是登錄兩三個系統(tǒng)、逐一點(diǎn)擊導(dǎo)出、再復(fù)制粘貼到總表里就要花掉將近四十分鐘而且經(jīng)常因?yàn)槁┑裟骋恍谢蛘吖經(jīng)]拉對而被返工。我當(dāng)時的想法很直接能不能讓每天早上的這四十分鐘變得不需要人盯著于是就有了這次Python自動化開發(fā)的小項(xiàng)目。背景交代清楚之后我先把目標(biāo)定下來每天早上8點(diǎn)自動登錄內(nèi)部系統(tǒng)、從Oracle查數(shù)據(jù)、拉取網(wǎng)頁報表、匯總生成Excel、發(fā)到企業(yè)IM群。整個項(xiàng)目從設(shè)計到落地用了大概五天之后運(yùn)行了兩個月穩(wěn)定性和收益都超出了預(yù)期。這篇文章就把完整過程和關(guān)鍵代碼整理出來適合想用Python解決日常工作重復(fù)勞動、但還沒系統(tǒng)上手的讀者參考。先說結(jié)論P(yáng)ython做這類內(nèi)部自動化最大的優(yōu)勢不是語法簡單而是它的庫生態(tài)幾乎覆蓋了所有連系統(tǒng)、讀數(shù)據(jù)、寫報表、發(fā)通知的環(huán)節(jié)。requests管接口調(diào)用、pandas管數(shù)據(jù)處理、openpyxl管Excel寫入、apscheduler管定時調(diào)度每個環(huán)節(jié)都有成熟的輪子。真正花時間的不是寫代碼而是梳理業(yè)務(wù)流程和排查環(huán)境問題。2. 需求盤點(diǎn)與技術(shù)選型先搞清楚自動化到底要自動什么2.1 流程拆解把手動操作翻譯成代碼步驟很多人一上來就寫代碼這是最容易翻車的方式。自動化的本質(zhì)是把人的操作流程翻譯成計算機(jī)能執(zhí)行的步驟前提就是先把流程拆細(xì)。我把我這邊的場景拆成了五步訪問內(nèi)部管理系統(tǒng)A輸入賬號密碼拿到登錄態(tài)。登錄后跳轉(zhuǎn)到報表頁面根據(jù)當(dāng)天的日期參數(shù)下載數(shù)據(jù)文件。連接Oracle數(shù)據(jù)庫執(zhí)行一段已經(jīng)寫好的查詢SQL取出前一天的數(shù)據(jù)。把下載的文件和數(shù)據(jù)庫查詢結(jié)果在pandas里清洗、合并計算同比、環(huán)比和幾個指標(biāo)。把最終結(jié)果寫入Excel模板通過企業(yè)IM的機(jī)器人接口發(fā)送到指定群同時把文件歸檔到共享目錄。每拆一步就要確認(rèn)一個關(guān)鍵問題這步操作有沒有不受人工干預(yù)的入口比如系統(tǒng)A有沒有APIOracle能不能遠(yuǎn)程直連企業(yè)IM有沒有機(jī)器人Webhook。確認(rèn)完這些自動化方案才談得上可行。2.2 技術(shù)棧選擇哪些庫負(fù)責(zé)哪件事根據(jù)上面的步驟我做了選型步驟工具理由登錄網(wǎng)頁系統(tǒng)requests 手動維護(hù)Cookie內(nèi)部系統(tǒng)沒有開放API登錄協(xié)議簡單用requests模擬表單登錄即可下載報表文件requests.get session保持會話保持登錄態(tài)按參數(shù)請求文件地址查詢Oracle數(shù)據(jù)庫cx_Oracle現(xiàn)在叫oracledbPython連接Oracle最成熟的方案數(shù)據(jù)清洗合并pandas numpyDataFrame處理表格數(shù)據(jù)幾乎是標(biāo)配寫入Excelopenpyxl pandas.ExcelWriter寫入格式可控能沿用原模板樣式定時調(diào)度apscheduler Windows計劃任務(wù)/cron腳本本身支持手動觸發(fā)調(diào)度交給系統(tǒng)級定時器更穩(wěn)定消息通知requests調(diào)用Webhook企微/釘釘/飛書都有機(jī)器人接口本質(zhì)就是POST一個JSON這里有一個很重要的問題為什么不用Selenium因?yàn)槲以u估了一下目標(biāo)系統(tǒng)是純表單登錄沒有復(fù)雜的JS驗(yàn)證碼直接用requests模擬登錄更輕量、更穩(wěn)定。Selenium要額外啟動瀏覽器進(jìn)程在服務(wù)器上沒有圖形界面的時候還得裝虛擬顯示器成本高。只有當(dāng)系統(tǒng)有復(fù)雜前端渲染或點(diǎn)擊事件的時候才值得用Selenium。選型要跟著穩(wěn)定性和維護(hù)成本走不是為了酷炫。2.3 Python環(huán)境準(zhǔn)備別小看這一步項(xiàng)目跑在公司W(wǎng)indows服務(wù)器上我直接裝了Python 3.10版本。這里要給新手提醒一個容易踩的坑不要直接裝最新版Python先確認(rèn)你依賴的庫是否支持該版本。我當(dāng)時吃過一個教訓(xùn)前置版本裝的是Python 3.12結(jié)果有個內(nèi)部依賴包還沒適配只好回退。通用的做法是裝3.10或3.11這種次新穩(wěn)定版。環(huán)境方面我用的是虛擬環(huán)境venv每裝一個庫都在虛擬環(huán)境里裝而不是全局環(huán)境。原因很簡單服務(wù)器上可能還有別的項(xiàng)目不同的項(xiàng)目依賴不同版本的pandas、requests全局裝的話遲早沖突。動手之前先建立好virtualenv后面部署到另一臺機(jī)器時只要導(dǎo)出requirements.txt目標(biāo)機(jī)器一條pip install -r requirements.txt就能復(fù)現(xiàn)。另外要提一下pip換源這件事。公司內(nèi)網(wǎng)訪問PyPI官方源經(jīng)常超時我用的是國內(nèi)鏡像源比如清華源或者阿里源。命令很簡單pip config set global.index-url 指向鏡像地址就行。這個問題不解決很多新人在安裝numpy庫或安裝sklearn庫這種依賴包體積大的場景下光等下載就等到懷疑人生。3. 核心代碼實(shí)現(xiàn)登錄、查數(shù)、清洗、落表全流程3.1 登錄態(tài)保持與報表下載requests模擬登錄登錄內(nèi)部系統(tǒng)這一步我先用瀏覽器的開發(fā)者工具抓了一下登錄請求發(fā)現(xiàn)就是POST到/login接口參數(shù)是username和password成功后返回一個sessionid的Cookie。于是代碼就有了底。注意企業(yè)內(nèi)部系統(tǒng)往往有登錄失敗次數(shù)限制所以代碼里務(wù)必加上失敗重試和異常通知不然賬號被鎖定了很麻煩。import requests login_url http://內(nèi)部系統(tǒng)地址/login report_url http://內(nèi)部系統(tǒng)地址/report/export session requests.Session() login_data { username: your_name, password: your_password } resp session.post(login_url, datalogin_data, timeout10) if resp.status_code ! 200 or 登錄失敗 in resp.text: raise RuntimeError(登錄失敗請檢查賬號或網(wǎng)絡(luò)) params { date: 2025-03-18, type: daily_report } file_resp session.get(report_url, paramsparams, timeout60) if file_resp.status_code 200: with open(report.xlsx, wb) as f: f.write(file_resp.content)這段代碼的關(guān)鍵在于session對象的復(fù)用。用requests的SessionCookie會被自動保存下來下載文件時不需要重新登錄。文件保存用二進(jìn)制寫模式b因?yàn)镋xcel文件本質(zhì)是二進(jìn)制格式這一步錯了會導(dǎo)致文件打不開。3.2 Oracle查詢鏈接、游標(biāo)、DataFrameOracle連接部分我用的是cx_Oracle庫。首先公司提供的連接字符串通常是這樣的格式host:port/service_name。代碼里用cx_Oracle.connect創(chuàng)建連接然后通過pandas.read_sql直接把查詢結(jié)果讀成DataFrame。這里有個很好的實(shí)踐SQL語句不要直接寫在業(yè)務(wù)代碼里單獨(dú)放在一個sql文件或配置表里這樣業(yè)務(wù)人員調(diào)整口徑時不用動代碼。import cx_Oracle import pandas as pd cx_Oracle.init_oracle_client(lib_dirrD:\instantclient_21_x64) conn cx_Oracle.connect( userusername, passwordpassword, dsn192.168.1.10:1521/ORCLPDB1 ) sql SELECT dept, SUM(amount) AS total_amount FROM sales_data WHERE stat_date TO_DATE(:dt, YYYY-MM-DD) GROUP BY dept df_sales pd.read_sql(sql, conconn, params{dt: 2025-03-18}) conn.close()這里要解釋一個容易踩的坑cx_Oracle需要Oracle客戶端庫Instant Client才能工作。如果你在服務(wù)器上直接pip install cx_Oracle然后連數(shù)據(jù)庫大概率會報DPI-1047錯誤。解決辦法有兩個一是把instantclient解壓出來用init_oracle_client指定到解壓目錄二是裝oracledb的thin模式不需要任何本地客戶端庫。我后來換成了oracledb thin模式部署時省了非常多事。import oracledb import pandas as pd conn oracledb.connect(userusername, passwordpassword, dsn192.168.1.10:1521/ORCLPDB1) df_sales pd.read_sql(sql, conconn) conn.close()pandas.read_sql這個接口對數(shù)據(jù)庫類型的適配做得很好不管是從Oracle還是MySQL查詢拿到的都是DataFrame后面的處理流程完全不需要關(guān)心數(shù)據(jù)來源。這也是我選擇pandas做中間層的原因統(tǒng)一了后續(xù)所有操作的數(shù)據(jù)結(jié)構(gòu)。3.3 pandas數(shù)據(jù)清洗合并、去重、計算指標(biāo)數(shù)據(jù)拿到之后就是最常見的pandas操作。我一般是這么處理的第一步檢查空值和重復(fù)值第二步統(tǒng)一字段名稱和格式第三步合并兩張表做計算。import pandas as pd import numpy as np # 兩張表一張是網(wǎng)頁下載的明細(xì)表一張是Oracle查詢的匯總表 df_detail pd.read_excel(report.xlsx, sheet_name明細(xì)) df_sales pd.read_sql(sql, conconn) # 統(tǒng)一日期字段為datetime類型 df_detail[日期] pd.to_datetime(df_detail[日期]) df_sales[stat_date] pd.to_datetime(df_sales[stat_date]) # 按部門合并 df_merged pd.merge(df_detail, df_sales, left_on部門, right_ondept, howleft) # 計算環(huán)比本期/上期-1 df_merged[環(huán)比] df_merged[本期金額] / df_merged[上期金額] - 1 df_merged[同比] df_merged[本期金額] / df_merged[去年同期金額] - 1 # 處理除數(shù)為0的情況 df_merged[[環(huán)比, 同比]] df_merged[[環(huán)比, 同比]].replace([np.inf, -np.inf], np.nan) df_merged df_merged.fillna(0)這里有一個關(guān)于數(shù)據(jù)可靠性的重要習(xí)慣每次合并之后打印一下數(shù)據(jù)量信息和幾個關(guān)鍵統(tǒng)計量。比如打印len(df_merged)和df_merged.isnull().sum()確認(rèn)沒有出現(xiàn)大批量空值。如果某天上游數(shù)據(jù)延遲或者下載到的文件是空的這一步就能直接暴露問題而不是等到報表發(fā)出去之后被人發(fā)現(xiàn)錯誤。我還用了一個技巧把校驗(yàn)規(guī)則寫進(jìn)代碼。例如數(shù)據(jù)行數(shù)必須大于0、合計數(shù)不能為0、環(huán)比絕對值不能大于10倍如果校驗(yàn)不通過就中止發(fā)送并給人發(fā)告警。這個校驗(yàn)機(jī)制極大提高了自動化跑批的可信度否則第一次代碼跑通很簡單長期穩(wěn)定運(yùn)行很難。3.4 Excel寫入沿用模板保留樣式寫到Excel這一步我用的是openpyxl引擎。pandas默認(rèn)的ExcelWriter引擎在直接df.to_excel時是沒辦法保留一個現(xiàn)成模板里的表頭樣式和公式的。我的做法是先復(fù)制一份模板文件再用openpyxl.load_workbook加載它把指定單元格區(qū)域?qū)懭霐?shù)據(jù)。from openpyxl import load_workbook from openpyxl.utils.dataframe import dataframe_to_rows template_path daily_report_template.xlsx output_path daily_report_20250318.xlsx # 復(fù)制模板文件 import shutil shutil.copyfile(template_path, output_path) wb load_workbook(output_path) ws wb[每日報表] # 從第5行開始寫入數(shù)據(jù)保留前4行的標(biāo)題和說明 for r_idx, row in enumerate(dataframe_to_rows(df_merged, indexFalse, headerFalse), start5): for c_idx, value in enumerate(row, start1): ws.cell(rowr_idx, columnc_idx, valuevalue) # 對金額列設(shè)置數(shù)字格式 for row in ws.iter_rows(min_row5, max_row5 len(df_merged), min_col2, max_col4): for cell in row: cell.number_format #,##0.00 wb.save(output_path)這里就有個細(xì)節(jié)如果直接用to_excel會把原有模板的樣式都抹掉而我們公司的報表是有固定格式和公式的不能丟。openpyxl在處理既有工作簿時不會重排其他內(nèi)容可以精準(zhǔn)寫入指定位置。如果報表還要繼續(xù)保留模板里的求和公式只要在模板里預(yù)留好公式行openpyxl就會保留。3.5 發(fā)到企業(yè)IM一個POST請求搞定企業(yè)微信、釘釘、飛書都有機(jī)器人Webhook本質(zhì)就是往URL post一個JSON。在自動化項(xiàng)目里這個環(huán)節(jié)同時承擔(dān)著兩個職責(zé)發(fā)送報表文件和發(fā)送異常告警。import requests import json webhook_url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key你的key def send_report(file_path): # 先上傳文件拿到media_id with open(file_path, rb) as f: upload_resp requests.post( https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key你的keytypefile, files{media: f}, timeout30 ) media_id upload_resp.json().get(media_id) payload { msgtype: file, file: {media_id: media_id} } requests.post(webhook_url, datajson.dumps(payload), timeout10) def send_alert(message): payload { msgtype: text, text: {content: f自動化報表任務(wù)異常{message}} } requests.post(webhook_url, datajson.dumps(payload), timeout10)雖然代碼不復(fù)雜但這一步是整個系統(tǒng)無人值守的關(guān)鍵。沒有通知環(huán)節(jié)腳本半夜掛了沒人知道第二天早上一看報表沒發(fā)等于自動化失去了意義。我后來還加了try/except任何異常都先發(fā)告警然后拋出確保寧可讓人看到錯誤也不能讓錯誤被靜默吞掉。4. 讓腳本每天自動跑APScheduler與系統(tǒng)定時任務(wù)的選擇4.1 為什么不用Python內(nèi)部sleep循環(huán)寫常駐進(jìn)程很多人想到定時任務(wù)第一反應(yīng)是while True: time.sleep(60)然后檢查時間。這個方案有兩個問題一是進(jìn)程常駐如果內(nèi)存泄漏或網(wǎng)絡(luò)異常跑幾天就掛了二是進(jìn)程崩潰沒人知道沒發(fā)報表也不知道。我一開始也考慮過APScheduler掛常駐進(jìn)程但后來還是放棄了。最靠譜的方案是把主流程寫成一個可以被一次調(diào)用的腳本調(diào)度交給操作系統(tǒng)。Windows用計劃任務(wù)、Linux用crontab每天8點(diǎn)觸發(fā)一次python腳本。Max這樣的好處是腳本本身無狀態(tài)、跑完即退出出問題只需要重跑一次。這不依賴Python進(jìn)程的穩(wěn)定性也方便監(jiān)測。當(dāng)然如果你需要在腳本內(nèi)部跑多個子任務(wù)APScheduler也是個好選擇。它支持cron表達(dá)式代碼寫起來很方便。我在這套系統(tǒng)里用的是系統(tǒng)定時任務(wù)調(diào)度單腳本方式簡單、易排查、不易出現(xiàn)僵尸進(jìn)程。def main(): try: run_daily_report() send_report(daily_report_20250318.xlsx) except Exception as e: send_alert(str(e)) raise if __name__ __main__: main()4.2 Windows計劃任務(wù)的配置要點(diǎn)Windows服務(wù)器上配置計劃任務(wù)我給三條實(shí)戰(zhàn)經(jīng)驗(yàn)一定要把使用最高權(quán)限運(yùn)行打開避免Python腳本因?yàn)闄?quán)限不足無法讀寫共享目錄。程序填python解釋器的絕對路徑參數(shù)填腳本絕對路徑工作目錄也填腳本所在目錄。因?yàn)槟_本中如果有相對路徑引用配置文件工作目錄不對會直接報FileNotFoundError。計劃任務(wù)觸發(fā)器設(shè)置每天時間填8:00但如果運(yùn)行時間較長建議再加一個如果任務(wù)運(yùn)行時間超過30分鐘強(qiáng)制停止的選項(xiàng)避免異常卡死。題外話如果你是Linux服務(wù)器cron表達(dá)式一行搞定0 8 * * * cd /opt/python_project /usr/bin/python3 run_daily.py /var/log/auto_task.log 21。重定向日志很重要排查問題全靠它。5. 我踩過的幾個坑環(huán)境、編碼、路徑、三方庫兼容性問題5.1 Python環(huán)境層面的坑寫代碼用了兩天排查環(huán)境問題用了一天半。我遇到最大的坑就是前面提到的cx_Oracle在Windows服務(wù)器上缺O(jiān)racle Instant Client。另一個坑是pandas和numpy版本不匹配導(dǎo)致DataFrame某些操作報奇怪的內(nèi)部錯誤。這類問題的通用排查思路是先看報錯堆棧到底指向哪個庫然后把那個庫單獨(dú)升級或降級到兼容版本。還需要提醒的是不要在系統(tǒng)自帶Python里瞎裝庫。Windows上有個App execution alias命令行里敲python可能會打開微軟商店而不是真正的Python環(huán)境。解決辦法是在系統(tǒng)設(shè)置里關(guān)掉應(yīng)用執(zhí)行別名或者安裝Python時選擇Add Python to PATH。5.2 數(shù)據(jù)上的坑數(shù)據(jù)自動化最核心的坑是你以為數(shù)據(jù)沒問題它偏偏給你出幺蛾子。我遇到過網(wǎng)頁下載的Excel文件帶了前導(dǎo)空格導(dǎo)致merge對不上遇到過Oracle查詢出來有id為NULL的臟數(shù)據(jù)遇到過下載下來的文件實(shí)際是HTML錯誤頁而不是Excel文件。所以我用了一個通用檢查函數(shù)替代人工目視檢查def check_file_valid(file_path): with open(file_path, rb) as f: head f.read(4) return head bPK\x03\x04 # xlsx文件的zip頭xlsx文件本質(zhì)上是一個zip包文件頭應(yīng)該是PK如果檢查到文件頭不是這個可以直接判定下載失敗。這種文件內(nèi)容校驗(yàn)的思路看著簡單卻省了我好幾次把壞文件發(fā)給領(lǐng)導(dǎo)的尷尬。另一個數(shù)據(jù)坑是Excel里日期格式不統(tǒng)一有的單元格是日期類型有的存成了文本。處理的辦法是統(tǒng)一用pd.to_datetime并加上format參數(shù)必要時用errorscoerce把非法值變成NaT然后再統(tǒng)一清洗。5.3 路徑與目錄結(jié)構(gòu)代碼里寫死路徑是自動化腳本的大忌。我第一版直接在腳本里寫了D:\my_script\data\report.xlsx后來腳本被挪到另一個目錄全都炸了。后來我改成腳本自動識別目錄import os import sys BASE_DIR os.path.dirname(os.path.abspath(__file__)) DATA_DIR os.path.join(BASE_DIR, data) CONFIG_PATH os.path.join(BASE_DIR, config.yaml)用os.path.abspath(file)獲取腳本真正所在目錄再基于這個目錄拼裝所有路徑。同時把登錄賬號、數(shù)據(jù)庫連接串、Webhook地址這些敏感信息和環(huán)境相關(guān)的配置都放到config.yaml或者環(huán)境變量里腳本本身不摻雜具體環(huán)境配置。這樣項(xiàng)目從開發(fā)機(jī)遷移到服務(wù)器只需要改配置文件代碼一行不動。6. 進(jìn)階擴(kuò)展從自動報表到數(shù)據(jù)服務(wù)的一點(diǎn)延伸項(xiàng)目穩(wěn)定跑了兩個禮拜之后我開始琢磨擴(kuò)展。既然數(shù)據(jù)每天都能自動取那能不能讓業(yè)務(wù)人員自己按條件查詢指標(biāo)于是我在同一個項(xiàng)目里加了兩個額外的能力第一把同類的查詢邏輯封裝成一個函數(shù)對外提供簡單的命令行參數(shù)接口。比如python query.py --date 2025-03-18 --dept 銷售部它能直接查詢指定日期的指標(biāo)并輸出表格這樣非技術(shù)人員通過一個簡單的批處理文件就能自助查數(shù)。第二加入了一點(diǎn)協(xié)程和并發(fā)處理的思路。比如每天要下載多個省份的文件用requests一個個下載比較慢我就用concurrent.futures.ThreadPoolExecutor開了4個線程并發(fā)下載整體時間幾乎縮短到原來的四分之一。不過這里也要提醒一句并發(fā)下載要注意目標(biāo)服務(wù)器的承受能力公司內(nèi)部系統(tǒng)通常沒問題如果是對外網(wǎng)站就要注意控制頻率別把別人服務(wù)器搞崩了主要也是避免給自己惹麻煩。第三很多熱詞里提到了python爬蟲和量化交易策略代碼雖然和我的場景不完全一樣但底層邏輯是通的。只要是定期從某個數(shù)據(jù)源獲取數(shù)據(jù)→清洗→按規(guī)則執(zhí)行動作都可以套用這同一套骨架數(shù)據(jù)獲取層、數(shù)據(jù)處理層、任務(wù)調(diào)度層、通知層。很多人覺得爬蟲或者量化交易很高端拆開看核心還是requests拿數(shù)據(jù)、pandas算信號、定時任務(wù)觸發(fā)。我對這套自動化架構(gòu)的體會是不要為了自動化而自動化自動化是為了把人的精力釋放到更有價值的判斷工作上去。手拉報表四十分鐘浪費(fèi)的其實(shí)是每天早上最清醒的時間。而把流程交給腳本之后人只需要在收到異常告警時介入其余時間該干嘛干嘛這才是工具應(yīng)該有的樣子。項(xiàng)目再往后走我打算把Excel報表往更輕量的方向演一下比如直接在網(wǎng)頁上做一個看板數(shù)據(jù)還是由這套定時任務(wù)推送到數(shù)據(jù)庫表再由前端展示。不過那是另一個項(xiàng)目了當(dāng)前這套Python自動化已經(jīng)把我從每天的重復(fù)勞動里徹底解放出來了。