【问题标题】:Handling multiple CSVs in Powershell efficiently在 Powershell 中高效处理多个 CSV
【发布时间】:2019-02-16 13:29:53
【问题描述】:

我正在从一个 API 中检索两个 CSV,一个名为 students.csv,类似于:

StudentNo,PreferredFirstnames,PreferredSurname,UPN
111, john, smith, john@email.com
222, jane, doe, jane@email.com

一个叫rooms.csv

roomName, roomNo, students
room1, 1, {@{StudentNo=111; StudentName=john smith; StartDate=2018-01-01T00:00:00; EndDate=2018-07-06T00:00:00},....
room2, 2,{@{StudentNo=222; StudentName=jane doe; StartDate=2018-01-01T00:00:00; EndDate=2018-07-06T00:00:00},...   

rooms.csv 中的第三列是 API 提供的数组

将两者合并成的最佳方法是什么

StudentNo,PreferredFirstnames,PreferredSurname,UPN, roomName
111, john, smith, john@email.com, room1
222, jane, doe, jane@email.com, room2

我在想……

$rooms = Import-Csv rooms.csv
$students  = Import-Csv students.csv
$combined = $students | select-object StudentNo,PreferredSurname,PreferredFirstnames,UPN,
@{Name="roomName";Expression={ ForEach ($r in $rooms) {
    if ($r.Students.StudentNo.Contains($_.StudentNo) -eq "True") 
{return $r.roomName}}}} 

这行得通,但是foreach 是正确的方法,我是把事情搞混了还是有更有效的方法???

--- 原帖---

有了所有这些信息,我需要比较学生数据并更新 AzureAD,然后编译一个数据列表,包括从 AzureAD 检索到的first namelast nameupnroom 和其他数据。

我的问题是“效率”。我的代码大部分都可以工作,但需要几个小时才能运行。目前我正在循环遍历students.csv,然后每个学生循环遍历rooms.csv 以找到他们所在的房间,并且显然在这之间等待多个api调用。

为每个学生找到房间的最有效方法是什么?将 CSV 作为自定义 PSObject 导入是否与使用哈希表相当?

【问题讨论】:

  • 对活动目录进行一次调用,获取所有房间和分配到房间的用户,将其保存到数组/列表中。然后你可以在数组/列表中查找有学生的房间,而无需多次调用活动目录
  • 另外请记住,您可以在 powershell 中调用 .net 的所有功能...stackoverflow.com/questions/26123/…
  • 请您发布您的活动目录查询。然后我们可以将其更改为您的答案...
  • 你能给我们估计一个文件中有多少学生吗?数据量可能会影响策略的选择。
  • 你能提供你工作但效率不高的代码吗?无论潜在的受访者提出改进意见还是完全重写,这都会对他们有所帮助。

标签: powershell csv import-csv export-csv


【解决方案1】:

我能够让您提出的代码工作,但它需要对代码和数据进行一些调整:

  • 必须有一些额外的步骤将rooms.csvstudents 列反序列化为对象集合。它似乎是一个 ScriptBlock,其计算结果为 HashTables 数组,但仍需要对 CSV 输入进行一些更改:
    • StartDateEndDate 属性需要被引用并转换为 [DateTime]
    • 至少对于包含多个学生的房间,必须引用该值,以便 Import-Csv 不会将 , 分隔数组元素解释为附加列。
  • 使用 CSV 作为中间格式的缺点是丢失了原始属性类型;导入后,所有内容都变为[String]。有时出于效率目的需要转换回原始类型,有时为了使某些操作正常工作是绝对必要的。您可以在每次使用这些属性时进行转换,但我更喜欢在导入后立即转换一次。

随着这些变化rooms.csv 变成...

roomName, roomNo, students
room1, 1, "{@{StudentNo=111; StudentName='john smith'; StartDate=[DateTime] '2018-01-01T00:00:00'; EndDate=[DateTime] '2018-07-06T00:00:00'}}"
room2, 2, "{@{StudentNo=222; StudentName='jane doe'; StartDate=[DateTime] '2018-01-01T00:00:00'; EndDate=[DateTime] '2018-07-06T00:00:00'}}"

...脚本变成...

# Replace the [String] property "students" with an array of [HashTable] property "Students"
$rooms = Import-Csv rooms.csv `
    | Select-Object `
        -ExcludeProperty 'students' `
        -Property '*', @{
            Name = 'Students'
            Expression = {
                $studentsText = $_.students
                $studentsScriptBlock = Invoke-Expression -Command $studentsText
                $studentsArray = @(& $studentsScriptBlock)

                return $studentsArray
            }
        }
# Replace the [String] property "StudentNo" with an [Int32] property of the same name
$students = Import-Csv students.csv `
    | Select-Object `
        -ExcludeProperty 'StudentNo' `
        -Property '*', @{
            Name = 'StudentNo'
            Expression = { [Int32] $_.StudentNo }
        }
$combined = $students `
    | Select-Object -Property `
        'StudentNo', `
        'PreferredSurname', `
        'PreferredFirstnames', `
        'UPN', `
        @{
            Name = "roomName";
            Expression = {
                foreach ($r in $rooms)
                {
                    if ($r.Students.StudentNo -contains $_.StudentNo)
                    {
                        return $r.roomName
                    }
                }

                #TODO: Return text indicating room not found?
            }
        }

这可能很慢的原因是因为您正在执行线性搜索 - 实际上是其中两个 - 针对每个学生对象;首先通过房间集合 (foreach),然后通过每个房间的学生集合 (-contains)。这很快就会变成大量的迭代和相等比较,因为在当前学生未分配到的每个房间中,您都在迭代该房间的学生的整个集合,不断地迭代,直到您找到该学生的房间。

在执行线性搜索时可以进行的一个简单优化是对正在搜索的项目进行排序(在这种情况下,Students 属性将按每个学生的StudentNo 属性排序)...

# Replace the [String] property "students" with an array of [HashTable] property "Students"
$rooms = Import-Csv rooms.csv `
    | Select-Object `
        -ExcludeProperty 'students' `
        -Property '*', @{
            Name = 'Students'
            Expression = {
                $studentsText = $_.students
                $studentsScriptBlock = Invoke-Expression -Command $studentsText
                $studentsArray = @(& $studentsScriptBlock) `
                    | Sort-Object -Property @{ Expression = { $_.StudentNo } }

                return $studentsArray
            }
        }

...然后当您搜索同一个集合时,如果您遇到的项目大于您正在搜索的项目,您就会知道该集合的其余部分不能可能包含您正在搜索的内容,您可以立即中止搜索...

@{
    Name = "roomName";
    Expression = {
        foreach ($r in $rooms)
        {
            # Requires $room.Students to be sorted by StudentNo
            foreach ($roomStudentNo in $r.Students.StudentNo)
            {
                if ($roomStudentNo -eq $_.StudentNo)
                {
                    # Return the matched room name and stop searching this and further rooms
                    return $r.roomName
                }
                elseif ($roomStudentNo -gt $_.StudentNo)
                {
                    # Stop searching this room
                    break
                }

                # $roomStudentNo is less than $_.StudentNo; keep searching this room
            }
        }

        #TODO: Return text indicating room not found?
    }
}

更好的是,通过排序集合,您还可以执行binary search,这比线性搜索要快*Array class 已经提供了一个BinarySearch static method,所以我们也可以用更少的代码来完成这个......

@{
    Name = "roomName";
    Expression = {
        foreach ($r in $rooms)
        {
            # Requires $room.Students to be sorted by StudentNo
            if ([Array]::BinarySearch($r.Students.StudentNo, $_.StudentNo) -ge 0)
            {
                return $r.roomName
            }
        }

        #TODO: Return text indicating room not found?
    }
}

然而,我解决这个问题的方法是使用[HashTable]StudentNo 映射到一个房间。构建[HashTable] 需要进行一些预处理,但这将在为学生检索房间时提供恒定时间的查找。

function GetRoomsByStudentNoTable()
{
    $table = @{ }

    foreach ($room in $rooms)
    {
        foreach ($student in $room.Students)
        {
            #NOTE: It is assumed each student belongs to at most one room
            $table[$student.StudentNo] = $room
        }
    }

    return $table
}

# Replace the [String] property "students" with an array of [HashTable] property "Students"
$rooms = Import-Csv rooms.csv `
    | Select-Object `
        -ExcludeProperty 'students' `
        -Property '*', @{
            Name = 'Students'
            Expression = {
                $studentsText = $_.students
                $studentsScriptBlock = Invoke-Expression -Command $studentsText
                $studentsArray = @(& $studentsScriptBlock)

                return $studentsArray
            }
        }
# Replace the [String] property "StudentNo" with an [Int32] property of the same name
$students = Import-Csv students.csv `
    | Select-Object `
        -ExcludeProperty 'StudentNo' `
        -Property '*', @{
            Name = 'StudentNo'
            Expression = { [Int32] $_.StudentNo }
        }
$roomsByStudentNo = GetRoomsByStudentNoTable
$combined = $students `
    | Select-Object -Property `
        'StudentNo', `
        'PreferredSurname', `
        'PreferredFirstnames', `
        'UPN', `
        @{
            Name = "roomName";
            Expression = {
                $room = $roomsByStudentNo[$_.StudentNo]
                if ($room -ne $null)
                {
                    return $room.roomName
                }

                #TODO: Return text indicating room not found?
            }
        }

您可以通过在导入 rooms.csv 的同时这样做来改善构建 $roomsByStudentNo 的影响...

# Replace the [String] property "students" with an array of [HashTable] property "Students"
$rooms = Import-Csv rooms.csv `
    | Select-Object `
        -ExcludeProperty 'students' `
        -Property '*', @{
            Name = 'Students'
            Expression = {
                $studentsText = $_.students
                $studentsScriptBlock = Invoke-Expression -Command $studentsText
                $studentsArray = @(& $studentsScriptBlock)

                return $studentsArray
            }
        } `
    | ForEach-Object -Begin {
        $roomsByStudentNo = @{ }
    } -Process {
        foreach ($student in $_.Students)
        {
            #NOTE: It is assumed each student belongs to at most one room
            $roomsByStudentNo[$student.StudentNo] = $_
        }

        return $_
    }

*小型数组除外

【讨论】:

  • 感谢您为此付出的努力,它让我朝着正确的方向前进,现在脚本运行不到一分钟。
  • 请记住,对于任何问题,您都可以upvote or downvote 回答,具体取决于您是否觉得他们有帮助,而对于您自己的问题,您可以accept an answer 回答您认为最能解决您的问题。跨度>
猜你喜欢
  • 2021-11-11
  • 2023-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-30
相关资源
最近更新 更多