152-技巧-Power Query 快速合併文件夾中表格之自定義函數 TableXlsxCsv

152-技巧-Power Query 快速合併文件夾中表格之自定義函數 TableXlsxCsv

附件下載地址://jiaopengzi.com/2602.html

一、背景

在我們使用 Power BI 或者 Power Pivot 做數據分析模型時,使用 Power Query 做數導入,經常會遇到如下場景:

  • 同一文件夾下多個表格的合併。
  • 同一個 Workbook 中有多個相同的欄位的 WorkSheet 。
  • Xlsx, Xls, Csv 並存。
  • 欄位的類型更改每次都要打開 Power Query 去修改。
  • 欄位增加和刪除每次都要打開 Power Query 去修改。
  • 欄位重命名後要修改類型等多處操作。
  • 合併的表格表頭有多行不需要的資訊,影響數據的讀取。
  • 有多個項目文件夾都是合併表格,要重複寫 M 語句或者重複面板操作。
  • 把文件名稱按照規則添加列到對應的合併的表格里。
  • Excel 文件篩選後,會出現數據變多的情況。

基於上述的場景,我們寫好了一個能應對的自定義函數:TableXlsxCsv ,讓我們來看一下函數的介紹。

152-1

這個函數的提示和 Power Query 內置函數的提示是一樣的,基本函數的使用和說明都清晰了。

二、TableXlsxCsv 的使用方法

1、參數介紹

Ⅰ、必填參數

1參:FolderName

data 文件夾下需要合併表的文件夾名稱,比如demo中的文件夾:01_訂單

2參:RemoveFirstRows

數據從頂部開始需要移除的行數,一般情況需要把標題及以前的數據都要移除,在名稱管理中已經管理好了對應的標題。

3參:RemoveLastRows

數據從底部開始需要移除的行數,比如底部有類似匯總行的數據。

4參:IsAddColumn

是否需要把文件名稱按照規則添加列,只能填寫 true 或者 false 兩個參數,true 表示需要添加列,false 表示不需要添加列。

注意:當 4 參 IsAddColumn 為 false,後面四個參數不用填寫,為 true 時,後面四個參數必須填寫。

Ⅱ、可選參數

5參:ColunmName

當 4 參 IsAddColumn 為 true 表示需要把文件名稱按照規則添加列,ColunmName 即為新增列的名稱,注意不要與合併表中的欄位名稱重複。

6參:ColumnType

5 參 ColunmName 添加列的數據類型,只能填寫如下類型:

type text

type number

Int64.Type

type date

type datetime

type time

7參:NameStartNumber

文件名稱字元開始的索引,比如文件名稱:訂單資訊-2022-05-16;索引是從 0 開始的,需要取年與日的話索引的開始就是 5 。

8參:Length

文件名稱在 6 參 NameStartNumber 後的長度,比如文件名稱:訂單資訊-2022-05-16;索引是從 0 開始的,需要取年與日的話索引的開始就是 5 ,Length 就是 2022-05-16 的長度 10 個字元。

2、必要配置

Ⅰ、項目文件夾

項目新建一個文件夾,名稱隨意,我們當前的案例項目文件夾:demo152

image-20220516175650501

Ⅱ、數據文件夾 data

項目文件夾建立好後,需要把項目文件和數據文件區分開,我們建立 data 文件夾來管理數據,當然這個文件夾的名稱也是隨意,根據自己的需求來即可。

Ⅲ、數據欄位的名稱管理

在 數據文件夾 data 下有一個 00_輔助表 的文件夾,這個文件名稱不能更改,是和函數綁定的名稱。

image-20220516180047658

Ⅳ、需合併表格的名稱管理

在輔助表文件夾中,有一個 Excel 文件: 01_名稱管理,這個文件就是把需要合併的文件夾中的表格的重命名、數據類型、是否顯示等都解耦出來,在這個 Excel 文件中操作即可。

image-20220516180406087

具體配置如下圖:

image-20220516182819675

1、文件夾:表示需要合併的文件夾,比如當前的:01_訂單 文件夾。

2、ID:表示需要合併表格從左開始的欄位序號,序號從1開始依次遞增,新增加的文件夾需要再次從 1 開始。

3、原始名稱:表示需要合併表格從左開始的欄位名稱。

4、統一名稱:表示在原始名稱的基礎下重命名後的欄位名稱,比如當前的配置就把所有欄位都重命名了。

5、欄位類型ID:表示表格合併後最後統一數據類型標識,具體對應參照左邊表格。

6、是否顯示:表示該欄位是否在最終上載到模型中,只能填寫:**是 ** 或者 ,填寫其它會報錯。

Ⅴ、Power Query 路徑配置

打開項目文件把路徑參數:Path 配置好,注意這個參數就是 前面 data 文件夾的路徑。參數 Path 名稱不能更改,,注意大小寫。

image-20220516182819675

Ⅵ、配置自定義函數 TableXlsxCsv

在 Path 下面 新建一個空白查詢,點擊高級編輯器,把 TableXlsxCsv 的 M 程式碼複製到編輯框裡面保存,重命名查詢為 TableXlsxCsv即可。

152-7

TableXlsxCsv 的 M 程式碼:

let
  fx0 = (
    FolderName as text, 
    RemoveFirstRows as number, 
    RemoveLastRows as number, 
    IsAddColumn as logical, 
    optional ColunmName as text, 
    optional ColumnType as text, 
    optional NameStartNumber as number, 
    optional Length as number
  ) as table =>
    let
      Types0 = List.Buffer({type text, type number, Int64.Type, type date, type datetime, type time}), 
      Types1 = List.Buffer({"文本", "小數", "整數", "日期", "日期時間", "時間"}), 
      ColumnNameTable0 = Table.Buffer( Excel.Workbook(File.Contents(Path & "\00_輔助表\01_名稱管理.xlsx"), true, true){[Item = "ColumnNameTable", Kind = "Table"]}[Data]), 
      ColumnNameTable0Fileds = List.Buffer(Table.ColumnNames(ColumnNameTable0)), 
      TypeListNewjpz = List.Buffer(List.Zip({ColumnNameTable0Fileds, List.Transform({0, 2, 0, 0, 2, 0}, each Types0{_})})), 
      ColumnNameTable1 = Table.Buffer(Table.TransformColumnTypes(ColumnNameTable0, TypeListNewjpz)), 
      ColumnNameTable2 = Table.Buffer(Table.SelectRows(ColumnNameTable1, each ([文件夾] = FolderName))), 
      ColumnCount = Table.RowCount(ColumnNameTable2), 
      ColumnNameTableX = Table.Buffer(Table.SelectRows(ColumnNameTable2, each [是否顯示] = "是")), 
      ColumnNameListOld = List.Buffer(List.Transform(ColumnNameTableX[ID], each "Column" & Text.From(_))), 
      ColumnNameListNew = List.Buffer(List.Zip({ColumnNameListOld, ColumnNameTableX[統一名稱]})), 
      TypeListNew = List.Buffer(List.Zip({ColumnNameTableX[統一名稱], List.Transform(ColumnNameTableX[欄位類型ID], each Types0{_})})), 
      FileList0 = Table.Buffer(Folder.Files(Path & "\" & FolderName)), 
      BinaryList0 = List.Buffer(FileList0[Content]), 
      List0 = List.Buffer({0 .. List.Count(BinaryList0) - 1}), 
      Extension0 = List.Buffer(FileList0[Extension]), 
      NameList0 = List.Buffer(FileList0[Name]), 
      NameList1 = List.Transform(NameList0, each Text.Middle(_, NameStartNumber, Length)), 
      TableList0 = List.Buffer(
        List.Transform(
          List0, 
          (n) =>
            let
              wb = Table.SelectColumns(
                if Text.Contains(Extension0{n}, "Csv", Comparer.OrdinalIgnoreCase) then
                  Table.RemoveLastN(
                    Table.RemoveFirstN(
                      Csv.Document(BinaryList0{n}, [Delimiter = ",", Columns = ColumnCount]), 
                      RemoveFirstRows
                    ), 
                    RemoveLastRows
                  )
                else
                  Table.Combine(
                    List.Transform(
                      Table.SelectRows(
                        Excel.Workbook(BinaryList0{n}, null, true), 
                        each not Text.Contains([Name], "Filter", Comparer.OrdinalIgnoreCase)
                      )[Data], 
                      each Table.RemoveLastN(Table.RemoveFirstN(_, RemoveFirstRows), RemoveLastRows)
                    )
                  ), 
                ColumnNameListOld
              )
            in
              if IsAddColumn then Table.AddColumn(wb, ColunmName, each NameList1{n}) else wb
        )
      ), 
      Rename0 = Table.Buffer(Table.RenameColumns(Table.Combine(TableList0), ColumnNameListNew)), 
      Result = 
        if IsAddColumn then
          Table.TransformColumnTypes(
            Rename0, 
            TypeListNew & {{ColunmName, Types0{List.PositionOf(Types1, ColumnType)}}}
          )
        else
          Table.TransformColumnTypes(Rename0, TypeListNew)
    in
      Result, 
  TypeMeta0 = type function (
    FolderName as (
      type text
        meta [
          Documentation.FieldCaption = "1參:FolderName, Path 路徑下需要合併表的文件夾名稱.", 
          Documentation.SampleValues = {"訂單資訊"}, 
          Documentation.AllowedValues = List.RemoveItems(
            List.Transform(
              List.Distinct(Folder.Files(Path)[Folder Path]), 
              each Replacer.ReplaceText(Replacer.ReplaceText(_, Path, ""), "\", "")
            ), 
            {"00_輔助表"}
          )
        ]
    ), 
    RemoveFirstRows as (
      type number
        meta [
          Documentation.FieldCaption  = "2參:RemoveFirstRows,從頂部開始移除的行數.", 
          Documentation.SampleValues  = {1}, 
          Documentation.AllowedValues = {0 .. 50}
        ]
    ), 
    RemoveLastRows as (
      type number
        meta [
          Documentation.FieldCaption  = "3參:RemoveLastRows,從底部開始移除的行數.", 
          Documentation.SampleValues  = {0}, 
          Documentation.AllowedValues = {0 .. 50}
        ]
    ), 
    IsAddColumn as (
      type logical
        meta [
          Documentation.FieldCaption  = "4參:IsAddColumn,是否需要把文件名稱按照規則添加列.", 
          Documentation.SampleValues  = {false}, 
          Documentation.AllowedValues = {false, true}
        ]
    ), 
    optional ColunmName as (
      type text
        meta [
          Documentation.FieldCaption = "5參:ColunmName,添加列的名稱.", 
          Documentation.SampleValues = {"NewName"}
        ]
    ), 
    optional ColumnType as (
      type text
        meta [
          Documentation.FieldCaption  = "6參:ColumnType,添加列的類型.", 
          Documentation.SampleValues  = {"文本"}, 
          Documentation.AllowedValues = {"文本", "小數", "整數", "日期", "日期時間", "時間"}
        ]
    ), 
    optional NameStartNumber as (
      type number
        meta [
          Documentation.FieldCaption  = "7參:NameStartNumber,文件名稱字元開始的索引.", 
          Documentation.SampleValues  = {0}, 
          Documentation.AllowedValues = {1 .. 50}
        ]
    ), 
    optional Length as (
      type number
        meta [
          Documentation.FieldCaption  = "8參:Length,文件名稱在 NameStartNumber 後的長度.", 
          Documentation.SampleValues  = {1}, 
          Documentation.AllowedValues = {1 .. 50}
        ]
    )
  ) as table
    meta [
      Documentation.Name = "TableXlsxCsv", 
      Documentation.LongDescription
        = "返回同一文件夾下相同欄位表格合併後的表格,兼容 XLSX, XLS, CSV 三種常見的格式, 同時兼容一個 WorkBook 多個相同欄位的 WorkSheet 的 Excel 文件.【注意】:當參數 IsAddColumn 為 true 時,後四個參數 ColunmName, TypeNumber, NameStartNumber, Length 必填; 【00_輔助表】文件夾下的名稱管理 Excel 文件需要在使用 TableXlsxCsv 提前配置好.", 
      Documentation.WhoAskedTheRightQuestion = "www.jiaopengzi.com", 
      Documentation.Author = "焦棚子", 
      Documentation.Examples = {
        [
          Description = "1、從文件夾【訂單資訊】中合併表格,且不用文件名稱添加列.", 
          Code = "TableXlsxCsv( " & """訂單資訊""" & ", 1, 0, false )", 
          Result = "當參數 IsAddColumn 為 false 時,後四個參數可以不寫;從文件夾【訂單資訊】中每個表格從頂部移除 1 行後合併表,且不用文件名稱添加列."
        ], 
        [
          Description = "2、從文件夾【訂單資訊】中合併表格,使用文件名稱按照規則添加列.", 
          Code = "TableXlsxCsv( " & """訂單資訊""" & ", 1, 3, true," & """數據日期""" & ", "&"""日期"""&", 12, 10 )", 
          Result
            = "當參數 IsAddColumn 為 true 時,後四個參數必填;從文件夾【訂單資訊】中每個表格從頂部移除 1 行並且從底部移除 3 行後合併表,添加列的名稱:數據日期,數據類型為:日期格式,按照文件名稱從 12 個字元開始取 10 個字元."
        ]
      }
    ], 
  fx1 = Value.ReplaceType(fx0, TypeMeta0)
in
  fx1

M 程式碼複製到高級編輯器後保存後,必要配置就算完畢了。

3、TableXlsxCsv 調用

Ⅰ、數據源

首先來看一下數據源,在函數說明中介紹到,我們的自定義函數 TableXlsxCsv ,可以兼容 Xlsx、Xls、Csv 三種常見的本地文件,我們直接通過一個函數就可以解決了,不用去考慮表格是否篩選,表頭有多行,同一個 Workbook 有多個相同的 WorkSheet等問題了。

在 data 文件夾下新建文件夾來管理同一欄位表格,案例中文件夾是:01_訂單,在前面配置名稱管理時也可以看。

image-20220516185501943

細心的朋友發現了,我們這個文件夾下是有三種數據類型。

我再看下具體的每個文件下的內容:

訂單資訊-2022-03-15 文件類型是 Xlsx,裡面有 2 個Sheet 分別對應 15 號的數據和 16 號的數據,為了方便演示 15 號的 Sheet 中有 5 行數據, 16 號的 Sheet 中有 6 行數據。

訂單資訊-2022-03-17 文件類型是 Xls,裡面只有 1 個Sheet 為了方便演示 17 號的文件中有 7 行數據。

訂單資訊-2022-03-15 文件類型是 Csv,為了方便演示 18 號的文件中有 8 行數據。

image-20220516190525039

image-20220516191055054

Ⅱ、函數調用

A、不添加列

按照圖示輸入資訊後點擊調用函數,即可得到新的查詢,更改查詢名稱為自己需要的名稱即可。

我們看到查詢語句非常簡單了,而且可以復用。

let
    源 = TableXlsxCsv("01_訂單", 1, 0, false )
in
    源

image-20220517123836889

B、添加列

按照圖示輸入資訊後點擊調用函數,即可得到新的查詢,更改查詢名稱為自己需要的名稱即可。

注意:在需要添加列的情況下,後四個參數都是必填。

M 語句如下:

let
    源 = TableXlsxCsv("01_訂單", 1, 0, true, "數據日期", type date, 5, 10)
in
    源

image-20220517145703383

C、結果對比
  • 結合前面的數據源,可以看到 Xlsx, Xls, Csv 在同一個文件夾中是可以通過一個我們寫的自定義函數合併的。雖然我們做了三個中格式兼容,但從效率來講我們更希望看到的是 Csv 格式。導出的平面文件 PQ 讀取的效率高低順序: Csv > Xlsx > Xls ,盡量不使用Xls, Xls 這種格式讀取是比較慢。
  • 看到下單日期,3 月 15 號和 3 月 16 號的數據在同一個 Workbook 的不同的 Sheet 裡面,也是合併到了一起。
  • 篩選的零時表也是剔除掉的。
  • 每個欄位的名稱按照輔助表裡名稱管理的 Excel 配置的來重命名的。
  • 每個欄位的格式按照輔助表裡名稱管理的 Excel 配置的數據類型來設置的。
  • 每個欄位的顯示按照輔助表裡名稱管理的 Excel 配置欄位是否顯示來顯示的,如自動編號不顯示,其它都顯示出來了。
  • 下圖第一張是沒有後四個參數的,也就沒有添加列,第二張圖片有後四個參數,在數據後添加了一列。需要注意的我的文件名稱的格式:訂單資訊-2022-03-15 這個數據名稱也是有將就的,年是四位數佔位,月是和日都必須是兩位數佔位,比如這裡的 3 月,就是 03 ;這裡的規則其實使用的函數 Text.Middle 這個規則可以自己去修改。
  • 最後的添加列,是對文件名稱使用規則,而不是 Sheet 名稱,比如這裡的訂單資訊-2022-03-15 ,其中有兩個 Sheet,最後添加列也是 3 月 15 號。
  • 第二張第 4 參數為 2,每個表在合併前都移除了最後兩行,可以和上述數據源作對比。

image-20220517125533111

image-20220517130041497

三、總結

1、我們自定義的這個函數,其實就是封裝了 Excel.Workbook 和 Csv.Document 兩個主要的函數。

2、當然如果自己寫一個這樣的函數的成本是很高的,我們寫好的函數可以直接拿來用即可。

3、有了這函數以後,我們在做項目的時候就提高數據導入的效率,同時一張 Excel 來管理我們的數據欄位。

4、有朋友可能會說直接使用資料庫不更方便嘛,使用資料庫也是完全可行的,但是更多的業務場景給的就是 Excel 、Csv 等文件,使用這個 PQ 自定義函數會更方便快捷。

5、這個函數的復用還體現在,我們的項目會多張,這樣就有多個文件夾,我們通過名稱管理的 Excel 文件,把需要的表格名稱管理起來,同時建立好對應的文件夾,這樣我們就可以直接快速的合併文件夾裡面的內容了。比如如這樣:

image-20220517103523213

6、最後需要說明的一點,使用自定義函數合併的表格一定要自己校驗。

7、附件中有詳細的分解步驟,有興趣的可以研究下。

image-20220517150210540

by 焦棚子