Wywołaj skrypt Pythona na PowerShell i przekazując PSObject i zwróć przeanalizowane dane
trochę tła: obecnie odpytuję 4Mio wierszy (z 50 kolumnami) z serwera MS SQL z dbatools do PSObject (w partii 10.000 wierszy na każde zapytanie), przetwarzam dane za pomocą PowerShell (dużo rzeczy RegEx) i piszę z powrotem do MariaDb z SimplySql . Średnio otrzymuję ok. 150 rzędów / sek. Musiałem użyć wielu sztuczek (Stringbuilder sieci itp.) Do tego wykonania, nie jest to takie złe imho
Jako nowe wymagania chcę wykryć język niektórych komórek tekstowych i muszę usunąć dane osobowe (imię i nazwisko oraz adres). W tym celu znalazłem kilka dobrych bibliotek Pythona ( spacy i pycld2 ). Wykonałem testy z pycld2 - całkiem dobra detekcja.
Uproszczony kod dla wyjaśnienia (wskazówka: jestem noobem Pythona):
#get data from MS SQL
$data = Invoke-DbaQuery -SqlInstance $Connection -Query $Query -As PSObject -QueryTimeout 1800 for ($i=0;$i -lt $data.length;$i++){ #do a lot of other stuff here #... #finally make lang detection if ($LangDetect.IsPresent){
$strLang = $tCaseDescription -replace "([^\p{L}\p{N}_\.\s]|`t|`n|`r)+",""
$arg = "import pycld2 as cld2; isReliable, textBytesFound, details = cld2.detect('" + $strLang + "', isPlainText = True, bestEffort = True);print(details[0][1])"
$tCaseLang = & $Env:Programfiles\Python39\python.exe -c $arg } else { $tCaseLang = ''
}
}
#write to MariaDB
Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $Query
To wywołanie Pythona za każdym razem działa, ale niszczy wydajność (12 wierszy / s) z powodu wywołania pętli i importowania pycld2 lib za każdym razem. A więc to kiepskie rozwiązanie :) Ponadto, jak wspomniałem wyżej - chcę użyć spacy - gdzie trzeba przeanalizować kilka kolumn, aby pozbyć się danych osobowych.
Nie jestem pewien, czy mam ochotę przekonwertować cały PS Parser do pythona: |
Myślę, że lepszym rozwiązaniem mogłoby być przekazanie całego PSObject z PowerShell do Pythona (przed uruchomieniem pętli PS) i zwrócenie go wraz z PSObject - po przetworzeniu w pythonie - ale nie wiem, jak mogę zrealizuj to za pomocą funkcji python / python.
Jakie byłoby Twoje podejście / sugestie, jakieś inne pomysły? Dzięki :)
Odpowiedzi
Poniższy uproszczony przykład pokazuje, jak można przekazać wiele [pscustomobject]( [psobject]) wystąpień z programu PowerShell do skryptu w języku Python ( -cw tym przypadku przekazywany jako ciąg znaków za pośrednictwem ):
o użyciu JSON jako format serializacji, poprzez ConvertTo-Json...
... i przekazanie tego JSON przez potok , który Python może odczytać przez stdin (standardowe wejście).
Ważne :
Kodowanie znaków :
PowerShell używa kodowania określonego w
$OutputEncodingzmiennej preferencji podczas wysyłania danych do programów zewnętrznych (takich jak Python), które domyślnie przyjmują wartość UTF-8 bez BOM w PowerShell [Core] v6 + , ale niestety do ASCII (!) W programie Windows PowerShell .Podobnie jak PowerShell ogranicza cię do wysyłania tekstu do zewnętrznego programu, tak samo niezmiennie interpretuje to, co otrzymuje jako tekst, a mianowicie na podstawie kodowania zapisanego w
[Console]::OutputEncoding; Niestety, obie wersje programu PowerShell w chwili pisania tego tekstu są domyślnie ustawione na stronie kodowej OEM systemu .Aby zarówno wysyłać, jak i odbierać (bez BOM) UTF-8 w obu wersjach programu PowerShell , ustaw (tymczasowo)
$OutputEncodingi[Console]::OutputEncodingw następujący sposób:
$OutputEncoding = [Console]::OutputEncoding = [System.Text.Utf8Encoding]::new($false)
Jeśli chcesz, aby Twój skrypt Pythona również wyświetlał obiekty, ponownie rozważ użycie JSON , który w PowerShell możesz przeanalizować do obiektów za pomocą ConvertFrom-Json.
# Sample input objects.
$data = [pscustomobject] @{ one = 1; two = 2 }, [pscustomobject] @{ one = 10; two = 20 } # Convert to JSON and pipe to Python. ConvertTo-Json $data | python -c @'
import sys, json
# Parse the JSON passed via stdin into a list of dictionaries.
dicts = json.load(sys.stdin)
# Sample processing: print the 'one' entry of each dict.
for dict in dicts:
print(dict['one'])
'@
Jeśli dane do przekazania to zbiór ciągów jednowierszowych , nie potrzebujesz JSON:
$data = 'foo', 'bar', 'baz' $data | python -c @'
import sys
# Sample processing: print each stdin input line enclosed in [...]
for line in sys.stdin:
print('[' + line.rstrip('\r\n') + ']')
'@
Opierając się na odpowiedzi @ mklement0, chcę podzielić się ukończonym i przetestowanym rozwiązaniem z zwracaniem JSON z Pythona do Powershell z uwzględnieniem prawidłowego kodowania znaków. Wypróbowałem już z 100 tys. Wierszy na jednej partii - żadnych problemów, działa bezbłędnie i superszybko :)
#get data from MS SQL
$query = -join@( 'SELECT `Id`, `CaseSubject`, `CaseDescription`, `AccountCountry`, `CaseLang` ' 'FROM `db`.`table_global` ' 'ORDER BY `Id` DESC, `Id` ASC ' 'LIMIT 10000;' ) $data = Invoke-DbaQuery -SqlInstance $Connection -Query $Query -As PSObject -QueryTimeout 1800
$arg = @' import pycld2 as cld2 import simplejson as json import sys, re, logging def main(): #toggle the logging level to stderr # https://stackoverflow.com/a/6579522/14226613 -> https://docs.python.org/3/library/logging.html#logging.debug logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logging.info('->Encoding Python: ' + str(sys.stdin.encoding)) # consideration of correct character encoding -> https://stackoverflow.com/a/30107752/14226613 # Parse the JSON passed via stdin into a list of dictionaries -> https://stackoverflow.com/a/65051178/14226613 cases = json.load(sys.stdin, 'utf-8') # Sample processing: print the 'one' entry of each dict. # https://regex101.com/r/bymIQS/1 regex = re.compile(r'(?=[^\w\s]).|[\r\n]|\'|\"|\\') # hash table with Country vs Language for 'boosting' the language detection, if pycld2 is not sure lang_country = {'Albania' : 'ALBANIAN', 'Algeria' : 'ARABIC', 'Argentina' : 'SPANISH', 'Armenia' : 'ARMENIAN', 'Austria' : 'GERMAN', 'Azerbaijan' : 'AZERBAIJANI', 'Bangladesh' : 'BENGALI', 'Belgium' : 'DUTCH', 'Benin' : 'FRENCH', 'Bolivia, Plurinational State of' : 'SPANISH', 'Bosnia and Herzegovina' : 'BOSNIAN', 'Brazil' : 'PORTUGUESE', 'Bulgaria' : 'BULGARIAN', 'Chile' : 'SPANISH', 'China' : 'Chinese', 'Colombia' : 'SPANISH', 'Costa Rica' : 'SPANISH', 'Croatia' : 'CROATIAN', 'Czech Republic' : 'CZECH', 'Denmark' : 'DANISH', 'Ecuador' : 'SPANISH', 'Egypt' : 'ARABIC', 'El Salvador' : 'SPANISH', 'Finland' : 'FINNISH', 'France' : 'FRENCH', 'Germany' : 'GERMAN', 'Greece' : 'GREEK', 'Greenland' : 'GREENLANDIC', 'Hungary' : 'HUNGARIAN', 'Iceland' : 'ICELANDIC', 'India' : 'HINDI', 'Iran' : 'PERSIAN', 'Iraq' : 'ARABIC', 'Ireland' : 'ENGLISH', 'Israel' : 'HEBREW', 'Italy' : 'ITALIAN', 'Japan' : 'Japanese', 'Kosovo' : 'ALBANIAN', 'Kuwait' : 'ARABIC', 'Mexico' : 'SPANISH', 'Monaco' : 'FRENCH', 'Morocco' : 'ARABIC', 'Netherlands' : 'DUTCH', 'New Zealand' : 'ENGLISH', 'Norway' : 'NORWEGIAN', 'Panama' : 'SPANISH', 'Paraguay' : 'SPANISH', 'Peru' : 'SPANISH', 'Poland' : 'POLISH', 'Portugal' : 'PORTUGUESE', 'Qatar' : 'ARABIC', 'Romania' : 'ROMANIAN', 'Russia' : 'RUSSIAN', 'San Marino' : 'ITALIAN', 'Saudi Arabia' : 'ARABIC', 'Serbia' : 'SERBIAN', 'Slovakia' : 'SLOVAK', 'Slovenia' : 'SLOVENIAN', 'South Africa' : 'AFRIKAANS', 'South Korea' : 'Korean', 'Spain' : 'SPANISH', 'Sweden' : 'SWEDISH', 'Switzerland' : 'GERMAN', 'Thailand' : 'THAI', 'Tunisia' : 'ARABIC', 'Turkey' : 'TURKISH', 'Ukraine' : 'UKRAINIAN', 'United Arab Emirates' : 'ARABIC', 'United Kingdom' : 'ENGLISH', 'United States' : 'ENGLISH', 'Uruguay' : 'SPANISH', 'Uzbekistan' : 'UZBEK', 'Venezuela' : 'SPANISH'} for case in cases: #concatenate two fiels and clean them a bitfield, so that we not get any faults due line brakes etc. tCaseDescription = regex.sub('', (case['CaseSubject'] + ' ' + case['CaseDescription'])) tCaseAccCountry = case['AccountCountry'] if tCaseAccCountry in lang_country: language = lang_country[tCaseAccCountry] isReliable, textBytesFound, details = cld2.detect(tCaseDescription, isPlainText = True, bestEffort = True, hintLanguage = language) else: isReliable, textBytesFound, details = cld2.detect(tCaseDescription, isPlainText = True, bestEffort = True) #Take Value case['CaseLang'] = details[0][0] #logging.info('->Python processing CaseID: ' + str(case['Id']) + ' / Detected Language: ' + str(case['CaseLang'])) #encode to JSON retVal = json.dumps(cases, 'utf-8') return retVal if __name__ == '__main__': retVal = main() sys.stdout.write(str(retVal)) '@ $dataJson = ConvertTo-Json $data $data = ($dataJson | python -X utf8 -c $arg) | ConvertFrom-Json
foreach($case in $data) {
$tCaseSubject = $case.CaseSubject -replace "\\", "\\" -replace "'", "\'"
$tCaseDescription = $case.CaseDescription -replace "\\", "\\" -replace "'", "\'"
$tCaseLang = $case.CaseLang.substring(0,1).toupper() + $case.CaseLang.substring(1).tolower() $tCaseId = $case.Id $qUpdate = -join @(
"UPDATE db.table_global SET CaseSubject=`'$tCaseSubject`', " "CaseDescription=`'$tCaseDescription`', "
"CaseLang=`'$tCaseLang`' " "WHERE Id=$tCaseId;"
)
try{
$result = Invoke-SqlUpdate -ConnectionName 'maria' -Query $qUpdate
} catch {
Write-Host -Foreground Red -Background Black ("result: " + $result + ' / No. ' + $i)
#break
}
}
Close-SqlConnection -ConnectionName 'maria'
Przepraszamy za niefortunne podkreślenie składni; blok skryptu zawiera SQL, Powershell i Python .. 🙄