使用Python+openpyxl實現(xiàn)周報數(shù)據(jù)自動化的操作過程
背景:被周報折磨的每個周一
我目前在一家電商公司負責(zé)部分業(yè)務(wù)的數(shù)據(jù)支持。我們團隊每周一上午都要出一份銷售周報,數(shù)據(jù)源是運營同事從后臺導(dǎo)出的十幾個Excel文件,每個文件對應(yīng)一個銷售渠道,比如天貓、京東、抖音等等。
我的任務(wù)就是把這些文件里的數(shù)據(jù)匯總到一張總表里,然后做一些簡單的計算,比如各渠道銷售額占比、環(huán)比增長等。聽起來不復(fù)雜,對吧?但實際做起來特別磨人。每個文件的格式雖然大體一致,但總有細微差別:有的表頭在第二行,有的在第三行;有的“銷售額”列叫“GMV”;最頭疼的是,有些文件里為了美觀,用了大量的合并單元格。
手動操作流程是這樣的:打開第一個文件,找到數(shù)據(jù)區(qū)域,復(fù)制,粘貼到總表;打開第二個文件,發(fā)現(xiàn)表頭對不上,得手動調(diào)整列順序;再打開第三個,遇到合并單元格,復(fù)制過來格式全亂了……一套流程下來,順利的話也要一個半小時,要是中途分心貼錯了行,又得從頭檢查。每個周一上午,我就像個沒有感情的“Ctrl+C/V”機器。
我忍了三個月,終于在一個數(shù)據(jù)出錯被領(lǐng)導(dǎo)點出后下定決心:必須用Python把這個過程自動化,把時間還給更有價值的分析工作。
問題分析:為什么不是pandas?
一開始,我理所當(dāng)然地想到了pandas。pd.read_excel()和df.to_excel()多簡單啊。我快速寫了個腳本:
import pandas as pd
import os
all_data = []
folder_path = './sales_reports'
for file in os.listdir(folder_path):
if file.endswith('.xlsx'):
df = pd.read_excel(os.path.join(folder_path, file))
all_data.append(df)
result = pd.concat(all_data, ignore_index=True)
result.to_excel('weekly_summary.xlsx', index=False)
跑了一下,直接報錯,而且問題一大堆:
- 表頭位置不一致:
pandas默認把第一行當(dāng)表頭,但有的文件第一行是標(biāo)題“XX渠道銷售數(shù)據(jù)”,第二行才是真正的列名。導(dǎo)致讀進來的DataFrame列名亂七八糟。 - 合并單元格災(zāi)難:
pandas會直接把合并單元格的值放在左上角單元格,其他位置變成NaN。當(dāng)我按行復(fù)制時,很多關(guān)鍵數(shù)據(jù)就丟失了。 - 格式丟失:我需要保留原文件中的數(shù)字格式(比如金額的千位分隔符)、字體顏色(標(biāo)紅的下滑數(shù)據(jù))等,
pandas完全做不到。 - 寫入新問題:用
pandas生成的Excel,有時候列寬異常,還得手動調(diào)整,并沒有完全“自動化”。
我意識到,pandas適合處理“純數(shù)據(jù)”,但對于這種需要保持原有格式、結(jié)構(gòu)不那么規(guī)范的Excel文件,有點力不從心。我需要一個能更精細操作Excel單元格的庫。
核心實現(xiàn):選擇openpyxl與設(shè)計流程
搜索之后,我鎖定了openpyxl。它是一個專門讀寫.xlsx格式的庫,可以精確到單元格級別進行讀寫,還能處理樣式。雖然不能讀老舊的.xls格式,但我們公司早就全面升級了,所以沒問題。
我的自動化腳本需要完成以下幾個核心步驟:
- 智能定位數(shù)據(jù):自動跳過表頭,找到每個文件數(shù)據(jù)開始的真實位置。
- 破解合并單元格:將合并區(qū)域的值“填充”到每一個單元格,保證數(shù)據(jù)不丟失。
- 統(tǒng)一列映射:建立渠道列名與總表標(biāo)準(zhǔn)列名的映射關(guān)系,比如把“GMV”、“銷售金額”都映射到“銷售額”。
- 帶格式寫入:將數(shù)據(jù)連同基本的格式(字體、邊框、數(shù)字格式)寫入總表。
- 簡單計算:在總表中自動計算合計、占比等。
下面我就分步拆解實現(xiàn)過程。
1. 智能定位數(shù)據(jù)起始行
這是第一個關(guān)鍵點。我觀察發(fā)現(xiàn),所有有效數(shù)據(jù)的表頭行都包含“日期”這個關(guān)鍵詞。所以,我的策略是:遍歷文件的前10行(足夠覆蓋所有情況),找到第一個包含“日期”的單元格所在的行,那一行就是表頭,下一行就是數(shù)據(jù)起始行。
from openpyxl import load_workbook
def find_data_start_row(ws, header_keyword="日期"):
"""
在工作表中查找包含指定關(guān)鍵詞的表頭行
返回數(shù)據(jù)起始行號(即表頭行號+1)
"""
# 通常數(shù)據(jù)不會超過前20行開始
for row in ws.iter_rows(min_row=1, max_row=20, max_col=10):
for cell in row:
if cell.value and header_keyword in str(cell.value):
# 找到關(guān)鍵詞,返回下一行作為數(shù)據(jù)開始
return cell.row + 1
# 如果沒找到,默認從第2行開始(假設(shè)第一行是表頭)
return 2
這里有個坑:cell.value可能是None,直接進行in判斷會報錯,所以必須先判斷cell.value是否存在,再轉(zhuǎn)換成字符串進行查找。
2. 破解合并單元格并建立列映射
這是最棘手的部分。openpyxl的worksheet.merged_cells.ranges屬性可以獲取所有合并單元格的范圍。我的思路是:先記錄下每個合并范圍的值,然后在讀取數(shù)據(jù)時,如果遇到屬于合并范圍的單元格,就使用記錄的值。
同時,我需要建立一個列名映射字典。我手動定義了一個標(biāo)準(zhǔn)列名列表,然后根據(jù)每個文件表頭行的內(nèi)容進行模糊匹配。
def get_column_mapping(ws, header_row_idx):
"""
根據(jù)表頭行,建立列索引到標(biāo)準(zhǔn)列名的映射。
例如:{1: '日期', 2: '銷售額', ...}
"""
# 標(biāo)準(zhǔn)列名(總表使用的)
standard_headers = ['日期', '渠道', '商品ID', '商品名稱', '銷售額', '訂單量']
mapping = {}
header_row = ws[header_row_idx]
for cell in header_row:
if cell.value:
header_text = str(cell.value).strip()
# 模糊匹配:如果標(biāo)準(zhǔn)列名是當(dāng)前表頭的一部分,或反之,則匹配
for std_header in standard_headers:
if std_header in header_text or header_text in std_header:
mapping[cell.column] = std_header # cell.column是列索引,如1,2,3
break # 匹配到一個就跳出內(nèi)層循環(huán)
return mapping
注意這個細節(jié):cell.column返回的是從1開始的列索引(整數(shù)),而不是字母。這比用column_index_from_string轉(zhuǎn)換更方便。
處理合并單元格的讀取邏輯,我把它整合到了遍歷數(shù)據(jù)的函數(shù)里:
def read_sheet_data(ws, data_start_row, col_mapping, merged_cell_values):
"""
從指定行開始讀取數(shù)據(jù),應(yīng)用列映射,并處理合并單元格值。
返回一個列表,每個元素是一行數(shù)據(jù)的字典。
"""
data = []
# 獲取最大行和最大列(有數(shù)據(jù)的區(qū)域)
max_row = ws.max_row
max_column = ws.max_column
for row in range(data_start_row, max_row + 1):
row_data = {v: None for v in col_mapping.values()} # 用標(biāo)準(zhǔn)列名初始化字典
for col_idx, std_header in col_mapping.items():
cell = ws.cell(row=row, column=col_idx)
cell_value = cell.value
# 關(guān)鍵!處理合并單元格:如果當(dāng)前單元格在合并范圍內(nèi),則取合并區(qū)域的值
for merged_range in merged_cell_values:
if cell.coordinate in merged_range:
cell_value = merged_cell_values[merged_range]
break
row_data[std_header] = cell_value
# 如果一行數(shù)據(jù)全為空,則跳過(可能是末尾的空行)
if any(row_data.values()):
# 補充渠道信息(可以從文件名解析,這里簡化處理)
row_data['渠道'] = os.path.basename(ws.parent.filepath).split('_')[0]
data.append(row_data)
return data
3. 帶格式寫入總表
數(shù)據(jù)都讀出來并統(tǒng)一格式后,寫入就相對簡單了。我用openpyxl創(chuàng)建一個新的工作簿,先寫入標(biāo)準(zhǔn)表頭,并設(shè)置簡單的樣式(加粗、居中、背景色)。然后逐行寫入數(shù)據(jù),給數(shù)值列加上千位分隔符的數(shù)字格式。
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
def write_summary(data_list, output_path='周報匯總.xlsx'):
"""將整理好的數(shù)據(jù)列表寫入新的Excel文件"""
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.title = "銷售周報匯總"
# 定義標(biāo)準(zhǔn)表頭
headers = ['日期', '渠道', '商品ID', '商品名稱', '銷售額', '訂單量']
# 設(shè)置表頭樣式
header_font = Font(bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
alignment = Alignment(horizontal="center", vertical="center")
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
# 寫入表頭
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col_idx, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = alignment
cell.border = thin_border
# 寫入數(shù)據(jù)
row_idx = 2
for row_data in data_list:
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row_idx, column=col_idx, value=row_data.get(header))
cell.border = thin_border
# 給銷售額和訂單量列設(shè)置數(shù)字格式
if header in ['銷售額', '訂單量'] and isinstance(cell.value, (int, float)):
if header == '銷售額':
cell.number_format = '#,##0.00' # 千位分隔符,兩位小數(shù)
else:
cell.number_format = '#,##0'
row_idx += 1
# 自動調(diào)整列寬(近似)
for col in ws.columns:
max_length = 0
column_letter = get_column_letter(col[0].column)
for cell in col:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50) # 設(shè)置一個最大寬度
ws.column_dimensions[column_letter].width = adjusted_width
wb.save(output_path)
print(f"匯總完成!文件已保存至:{output_path}")
這里有個坑:openpyxl的自動調(diào)整列寬ws.column_dimensions[column_letter].bestFit并不總是可靠,我采用了計算每列最大字符長度的方法,雖然簡單但效果不錯。注意要捕獲str(cell.value)的異常,因為cell.value可能是None。
4. 串聯(lián)主流程與簡單計算
最后,我把所有功能串聯(lián)起來,并在寫入數(shù)據(jù)后,在總表末尾添加兩行:一行計算各渠道銷售總額,一行計算占比。
# 在主函數(shù)中,讀取所有文件數(shù)據(jù)后
all_data = []
for file_path in excel_files:
wb = load_workbook(file_path, data_only=True) # data_only=True只讀值,不讀公式
ws = wb.active
ws.filepath = file_path # 給ws對象加個屬性,方便后面取文件名
# 找到表頭行和數(shù)據(jù)起始行
header_row_idx = find_data_start_row(ws) - 1
data_start_row = header_row_idx + 1
# 獲取列映射
col_mapping = get_column_mapping(ws, header_row_idx)
if not col_mapping:
print(f"警告:在文件 {file_path} 中未找到匹配的表頭,跳過。")
continue
# 預(yù)處理合并單元格的值
merged_cell_values = {}
for merged_range in ws.merged_cells.ranges:
# 獲取合并區(qū)域左上角單元格的值
top_left_cell = ws[merged_range.min_row][merged_range.min_col - 1] # 注意索引轉(zhuǎn)換
merged_cell_values[merged_range] = top_left_cell.value
# 讀取數(shù)據(jù)
sheet_data = read_sheet_data(ws, data_start_row, col_mapping, merged_cell_values)
all_data.extend(sheet_data)
# 寫入?yún)R總文件
write_summary(all_data)
# 在write_summary函數(shù)內(nèi)部,數(shù)據(jù)寫入后,可以添加匯總行
# ... 數(shù)據(jù)寫入循環(huán)之后 ...
summary_row = row_idx + 1
ws.cell(row=summary_row, column=4, value="渠道總計").font = Font(bold=True)
# 使用公式計算每個渠道的銷售額總和(假設(shè)渠道名列是B,銷售額列是E)
# 這里簡化,實際可以按渠道分組計算
ws.cell(row=summary_row, column=5, value=f"=SUMIF(B2:B{row_idx}, B{summary_row}, E2:E{row_idx})")
完整代碼
"""
Excel銷售周報自動化匯總腳本
作者:實戰(zhàn)踩坑記錄
功能:自動讀取多個渠道銷售Excel,統(tǒng)一格式后匯總至一個文件。
"""
import os
from openpyxl import load_workbook, Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
def find_data_start_row(ws, header_keyword="日期"):
"""定位數(shù)據(jù)起始行"""
for row in ws.iter_rows(min_row=1, max_row=20, max_col=10):
for cell in row:
if cell.value and header_keyword in str(cell.value):
return cell.row + 1
return 2
def get_column_mapping(ws, header_row_idx):
"""建立列索引到標(biāo)準(zhǔn)列名的映射"""
standard_headers = ['日期', '渠道', '商品ID', '商品名稱', '銷售額', '訂單量']
mapping = {}
header_row = ws[header_row_idx]
for cell in header_row:
if cell.value:
header_text = str(cell.value).strip()
for std_header in standard_headers:
if std_header in header_text or header_text in std_header:
mapping[cell.column] = std_header
break
return mapping
def read_sheet_data(ws, data_start_row, col_mapping, merged_cell_values):
"""讀取單文件數(shù)據(jù),處理合并單元格"""
data = []
max_row = ws.max_row
for row in range(data_start_row, max_row + 1):
row_data = {v: None for v in col_mapping.values()}
for col_idx, std_header in col_mapping.items():
cell = ws.cell(row=row, column=col_idx)
cell_value = cell.value
# 處理合并單元格
for merged_range, merged_value in merged_cell_values.items():
if cell.coordinate in merged_range:
cell_value = merged_value
break
row_data[std_header] = cell_value
if any(row_data.values()):
# 簡單從文件名提取渠道名,可根據(jù)實際情況修改
filename = os.path.basename(ws.parent.filepath)
channel = filename.split('_')[0] if '_' in filename else filename.replace('.xlsx', '')
row_data['渠道'] = channel
data.append(row_data)
return data
def write_summary(data_list, output_path='周報匯總.xlsx'):
"""寫入?yún)R總文件并添加格式"""
wb = Workbook()
ws = wb.active
ws.title = "銷售周報匯總"
headers = ['日期', '渠道', '商品ID', '商品名稱', '銷售額', '訂單量']
# 樣式定義
header_font = Font(bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
align_center = Alignment(horizontal="center", vertical="center")
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
# 寫表頭
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col_idx, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = align_center
cell.border = thin_border
# 寫數(shù)據(jù)
row_idx = 2
for row_data in data_list:
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row_idx, column=col_idx, value=row_data.get(header))
cell.border = thin_border
if header in ['銷售額', '訂單量'] and isinstance(cell.value, (int, float)):
cell.number_format = '#,##0.00' if header == '銷售額' else '#,##0'
row_idx += 1
# 調(diào)整列寬
for col in ws.columns:
max_len = 0
col_letter = get_column_letter(col[0].column)
for cell in col:
try:
cell_len = len(str(cell.value))
if cell_len > max_len:
max_len = cell_len
except:
pass
adjusted_width = min(max_len + 2, 50)
ws.column_dimensions[col_letter].width = adjusted_width
wb.save(output_path)
print(f"? 匯總完成!文件已保存至:{output_path}")
def main():
# 配置路徑
reports_folder = './sales_reports' # 存放所有渠道Excel文件的文件夾
output_file = './周報匯總.xlsx'
if not os.path.exists(reports_folder):
print(f"錯誤:文件夾 '{reports_folder}' 不存在。")
return
all_data = []
excel_files = [os.path.join(reports_folder, f) for f in os.listdir(reports_folder)
if f.endswith(('.xlsx', '.xlsm'))]
if not excel_files:
print("未找到Excel文件。")
return
for file_path in excel_files:
print(f"正在處理:{os.path.basename(file_path)}")
try:
wb = load_workbook(file_path, data_only=True)
ws = wb.active
ws.parent.filepath = file_path # 臨時存儲路徑
header_row_idx = find_data_start_row(ws) - 1
data_start_row = header_row_idx + 1
col_mapping = get_column_mapping(ws, header_row_idx)
if not col_mapping:
print(f" ?? 跳過,未識別到表頭。")
continue
# 處理合并單元格
merged_values = {}
for merged_range in ws.merged_cells.ranges:
top_left_cell = ws[merged_range.min_row][merged_range.min_col - 1]
merged_values[merged_range] = top_left_cell.value
sheet_data = read_sheet_data(ws, data_start_row, col_mapping, merged_values)
all_data.extend(sheet_data)
print(f" ? 讀取到 {len(sheet_data)} 行數(shù)據(jù)。")
except Exception as e:
print(f" ? 處理文件時出錯:{e}")
if all_data:
write_summary(all_data, output_file)
print(f"總計處理 {len(excel_files)} 個文件,合并 {len(all_data)} 行數(shù)據(jù)。")
else:
print("未讀取到任何有效數(shù)據(jù)。")
if __name__ == "__main__":
main()
踩坑記錄
data_only=True的誤解:一開始我沒加這個參數(shù),結(jié)果有些單元格值是公式(如=SUM(A1:A10)),讀出來就是公式字符串本身,而不是計算結(jié)果。加上data_only=True后,openpyxl會讀取上次Excel保存時計算好的值。但要注意,如果文件從未被Excel計算保存過,讀出的公式值可能是None。- 合并單元格的坐標(biāo)判斷:
cell.coordinate返回的是字符串(如'A1'),而merged_range是一個CellRange對象。我最初直接用cell.coordinate == merged_range,永遠為False。正確做法是使用cell.coordinate in merged_range,CellRange對象支持in操作符來判斷坐標(biāo)是否在其范圍內(nèi)。 - 列索引與列字母的混淆:
openpyxl中,cell.column返回整數(shù)索引(1-based),cell.column_letter返回字母。在遍歷和存儲映射關(guān)系時,用整數(shù)索引更不容易出錯。但在設(shè)置列寬ws.column_dimensions[column_letter]時,又必須用字母。這個轉(zhuǎn)換需要用get_column_letter()函數(shù)。 - 中文路徑或文件名報錯:在Windows上,如果文件路徑或工作表名稱包含中文,有時會報編碼相關(guān)的錯誤。一個穩(wěn)妥的做法是使用
os.path模塊來構(gòu)建路徑,并確保Python腳本文件本身保存為UTF-8編碼。如果問題依舊,可以嘗試在文件路徑字符串前加r(原始字符串)防止轉(zhuǎn)義。
小結(jié)
這個自動化腳本把我每周一上午的“體力活”壓縮到了3分鐘以內(nèi),而且完全避免了手動操作可能帶來的錯誤。核心收獲是:處理非標(biāo)準(zhǔn)格式的Excel文件,openpyxl這種單元格級操作的庫比pandas更靈活、更強大。下一步可以考慮加入郵件自動發(fā)送功能,或者用PyInstaller打包成可執(zhí)行文件,分享給不會編程的同事使用。
以上就是使用Python+openpyxl實現(xiàn)周報數(shù)據(jù)自動化的操作過程的詳細內(nèi)容,更多關(guān)于Python openpyxl周報數(shù)據(jù)自動化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python實現(xiàn)多線程暴力破解登陸路由器功能代碼分享
這篇文章主要介紹了python實現(xiàn)多線程暴力破解登陸路由器功能代碼分享,本文直接給出實現(xiàn)代碼,需要的朋友可以參考下2015-01-01
如何使用yolov5輸出檢測到的目標(biāo)坐標(biāo)信息
YOLOv5是一系列在 COCO 數(shù)據(jù)集上預(yù)訓(xùn)練的對象檢測架構(gòu)和模型,下面這篇文章主要給大家介紹了關(guān)于如何使用yolov5輸出檢測到的目標(biāo)坐標(biāo)信息的相關(guān)資料,需要的朋友可以參考下2022-03-03
關(guān)于SSD目標(biāo)檢測模型的人臉口罩識別
這篇文章主要介紹了關(guān)于SSD目標(biāo)檢測模型的人臉口罩識別問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11
基于python+selenium的二次封裝的實現(xiàn)
這篇文章主要介紹了基于python+selenium的二次封裝的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
python歐拉角和旋轉(zhuǎn)矩陣變換的實現(xiàn)示例
在計算機圖形學(xué)中,歐拉角和旋轉(zhuǎn)矩陣是描述物體旋轉(zhuǎn)的常用方法,本文主要介紹了python歐拉角和旋轉(zhuǎn)矩陣變換的實現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下2024-03-03

