Appeler le script Python par PowerShell et passer PSObject et renvoyer les données analysées

Nov 28 2020

un peu de contexte: actuellement, j'interroge 4Mio lignes (avec 50 colonnes) à partir d'un serveur MS SQL avec dbatools dans un PSObject (par lots 10.000 lignes par requête), je traite les données avec PowerShell (beaucoup de choses RegEx) et j'écris dans un MariaDb avec SimplySql . En moyenne, je reçois env. 150 lignes / sec. J'ai dû utiliser beaucoup d'astuces (Net's Stringbuilder, etc.) pour cette performance, ce n'est pas si mal à mon avis

En tant que nouvelles exigences, je souhaite détecter la langue de certaines cellules de texte et je dois supprimer les données personnelles (nom et adresse). J'ai trouvé quelques bons libs python ( de planants et pycld2 ) à cette fin. J'ai fait des tests avec pycld2 - assez bonne détection.

Code simplifié pour clarification (indice: je suis un python noob):

#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

Cet appel python fonctionne à chaque fois, mais il détruit les performances (12 lignes / s) en raison de l'appel en boucle et de l'importation de la bibliothèque pycld2 à chaque fois. Donc, c'est une solution boiteuse :) De plus, comme mentionné ci-dessus - je veux utiliser spacy - où quelques colonnes supplémentaires doivent être analysées pour se débarrasser des données personnelles.

Je ne sais pas si j'ai envie de convertir tout le PS Parser en python: |

Je pense qu'une meilleure solution pourrait être de passer tout le PSObject de PowerShell à python (avant le démarrage de la boucle PS) et de le renvoyer ainsi que PSObject - après qu'il a été traité en python - mais je ne sais pas comment je peux réalisez cela avec la fonction python / python.

Quelle serait votre approche / suggestions, d'autres idées? Merci :)

Réponses

2 mklement0 Nov 28 2020 at 22:22

L'exemple simplifié suivant vous montre comment vous pouvez transmettre plusieurs instances [pscustomobject]( [psobject]) de PowerShell à un script Python (passé sous forme de chaîne via -cdans ce cas):

  • en utilisant JSON comme format de sérialisation, via ConvertTo-Json...

  • ... et en passant ce JSON via le pipeline , que Python peut lire via stdin (entrée standard).

Important :

  • Codage des caractères :

    • PowerShell utilise le codage spécifié dans la $OutputEncodingvariable de préférence lors de l'envoi de données à des programmes externes (tels que Python), qui est louable par défaut à UTF-8 sans nomenclature dans PowerShell [Core] v6 + , mais malheureusement à ASCII (!) Dans Windows PowerShell .

    • Tout comme PowerShell vous limite à envoyer du texte à un programme externe, il interprète également invariablement ce qu'il reçoit comme du texte, à savoir en fonction de l'encodage stocké dans [Console]::OutputEncoding; Malheureusement, les deux éditions de PowerShell au moment de l'écriture par défaut sur la page de codes OEM du système .

    • Pour envoyer et recevoir (sans nomenclature) UTF-8 dans les deux éditions PowerShell , définissez (temporairement) $OutputEncodinget [Console]::OutputEncodingcomme suit:
      $OutputEncoding = [Console]::OutputEncoding = [System.Text.Utf8Encoding]::new($false)

  • Si vous souhaitez que votre script Python génère également des objets, envisagez à nouveau d'utiliser JSON , que vous pouvez analyser sur PowerShell en objets 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'])

'@

Si les données à transmettre sont une collection de chaînes sur une seule ligne , vous n'avez pas besoin de 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') + ']')

'@

TefoD Nov 30 2020 at 03:48

Sur la base de la réponse de @ mklement0, je souhaite partager la solution terminée et testée avec le retour du JSON de python à Powershell en tenant compte du codage de caractères correct. Je l'ai déjà essayé avec 100k lignes sur un lot - aucun problème, fonctionne parfaitement et ultra-rapide :)

#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'

Veuillez vous excuser de la mise en évidence de la syntaxe malheureuse; Le bloc de script contient SQL, Powershell et Python. 🙄