Chiama lo script Python per PowerShell e passa PSObject e restituisci i dati analizzati

Nov 28 2020

un po 'di background: attualmente sto interrogando le righe 4Mio (con 50 colonne) da un server MS SQL con dbatools in un PSObject (in batch 10.000 righe ogni query), elaborando i dati con PowerShell (molte cose RegEx) e riscrivendo in un MariaDb con SimplySql . In media ottengo ca. 150 righe / sec. Ho dovuto usare molti trucchi (Net's Stringbuilder ecc.) Per questa performance, non è poi così male imho

Come nuovi requisiti voglio rilevare la lingua di alcune celle di testo e devo rimuovere i dati personali (nome e indirizzo). Ho trovato alcune buone librerie di Python ( spacy e pycld2 ) per quello scopo. Ho fatto dei test con pycld2 - rilevamento abbastanza buono.

Codice semplificato per chiarimenti (suggerimento: sono un noob python):

#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

Questa chiamata a python funziona ogni volta, ma distrugge le prestazioni (12 righe / sec) a causa della chiamata del ciclo e dell'importazione di pycld2 lib ogni volta. Quindi, questa è una soluzione scadente :) Inoltre, come accennato in precedenza, voglio usare spacy, dove alcune colonne in più devono essere analizzate per eliminare i dati personali.

Non sono sicuro, se ho l'umore di convertire l'intero PS Parser in Python: |

Credo, una soluzione migliore potrebbe essere quella di passare l'intero PSObject da PowerShell a python (prima che il ciclo PS inizi) e restituirlo insieme a PSObject - dopo che è stato elaborato in python - ma non so, come posso realizzalo con la funzione python / python.

Quale sarebbe il tuo approccio / suggerimenti, altre idee? Grazie :)

Risposte

2 mklement0 Nov 28 2020 at 22:22

Il seguente esempio semplificato mostra come passare più istanze [pscustomobject]( [psobject]) da PowerShell a uno script Python (passato come stringa tramite -cin questo caso):

  • da utilizzare JSON come formato di serializzazione, via ConvertTo-Json...

  • ... e passando quel JSON tramite la pipeline , che Python può leggere tramite stdin (standard input).

Importante :

  • Codifica dei caratteri :

    • PowerShell utilizza la codifica specificata nella $OutputEncodingvariabile di preferenza quando invia dati a programmi esterni (come Python), che è lodevolmente predefinito UTF-8 senza BOM in PowerShell [Core] v6 + , ma purtroppo ASCII (!) In Windows PowerShell .

    • Proprio come PowerShell ti limita a inviare testo a un programma esterno, anch'esso interpreta invariabilmente ciò che riceve come testo, ovvero in base alla codifica memorizzata in [Console]::OutputEncoding; purtroppo, entrambe le edizioni di PowerShell al momento della stesura di questa scrittura hanno come impostazione predefinita la code page OEM del sistema .

    • Per inviare e ricevere UTF-8 (senza BOM) in entrambe le edizioni di PowerShell , impostare (temporaneamente) $OutputEncodinge [Console]::OutputEncodingcome segue:
      $OutputEncoding = [Console]::OutputEncoding = [System.Text.Utf8Encoding]::new($false)

  • Se vuoi che il tuo script Python produca anche oggetti, considera di nuovo l'utilizzo di JSON , che su PowerShell puoi analizzare in oggetti con 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'])

'@

Se i dati da passare sono una raccolta di stringhe a riga singola , non è necessario 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

Sulla base della risposta di @ mklement0, voglio condividere la soluzione completata e testata con la restituzione del JSON da Python a Powershell tenendo in considerazione la corretta codifica dei caratteri. L'ho già provato con 100k righe su un batch - nessun problema, funziona perfettamente e superveloce :)

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

Si prega di scusarsi per la sfortunata evidenziazione della sintassi; Il blocco di script contiene SQL, Powershell e Python .. 🙄