Эффективная обработка нескольких CSV в Powershell - PullRequest
0 голосов
/ 12 сентября 2018

Я получаю два CSV из API, один из которых называется 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},...   

Третий столбец в комнатах.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, а затем составления списка данных, включая first name, last name, upn, room и другие данные, полученные из AzureAD.

Моя проблема - «эффективность».У меня есть код, который в основном работает, но для его запуска требуются часы.В настоящее время я перебираю students.csv, а затем для каждого студента перебираю rooms.csv, чтобы найти комнату, в которой они находятся, и, очевидно, жду нескольких вызовов API между ними.

Что является наиболееэффективный способ найти комнату для каждого студента?Можно ли импортировать CSV-файл как пользовательский PSObject с использованием хеш-таблиц?

1 Ответ

0 голосов
/ 14 сентября 2018

Мне удалось заставить ваш предложенный код работать, но он требует некоторых настроек кода и данных:

  • Должен быть какой-то дополнительный шаг, когда вы десериализуете столбец students rooms.csv в коллекцию объектов. Похоже, это ScriptBlock, который оценивается в массив HashTable с, но некоторые изменения в CSV-входе все еще необходимы:
    • Свойства StartDate и EndDate необходимо заключать в кавычки и приводить к [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?
    }
}

Еще лучше: с отсортированной коллекцией вы также можете выполнить двоичный поиск , который быстрее линейного поиска *. Класс Array уже предоставляет BinarySearch статический метод , поэтому мы можем выполнить это и в меньшем количестве кода ...

@{
    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?
            }
        }

Вы можете улучшить попадание здания $roomsByStudentNo, выполнив это одновременно с импортом rooms.csv ...

# 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 $_
    }

* За исключением небольших массивов

...