Llame al script Python por PowerShell y pase PSObject y devuelva los datos analizados

Nov 28 2020

algunos antecedentes: actualmente estoy consultando 4Mio filas (con 50 columnas) desde un servidor MS SQL con dbatools en un PSObject (en Batch 10.000 filas cada consulta), procesando los datos con PowerShell (muchas cosas RegEx) y escribiendo de nuevo en un MariaDb con SimplySql . En promedio, obtengo aprox. 150 filas / seg. Tuve que usar muchos trucos (Net's Stringbuilder, etc.) para este rendimiento, no es tan malo en mi humilde opinión

Como nuevos requisitos quiero detectar el idioma de algunas celdas de texto y tengo que eliminar los datos personales (nombre y dirección). He encontrado algunas buenas librerías Python ( espaciosas y pycld2 ) para ese propósito. Hice pruebas con pycld2, detección bastante buena.

Código simplificado para aclarar (pista: soy un novato de 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

Esta llamada de Python funciona cada vez, pero destruye el rendimiento (12 filas / seg) debido a la llamada de bucle y la importación de pycld2 lib cada vez. Entonces, esta es una solución poco convincente :) Además, como se mencionó anteriormente, quiero usar espacio, donde algunas columnas más deben analizarse para deshacerse de los datos personales.

No estoy seguro, si tengo ganas de convertir todo el PS Parser a python: |

Creo que una mejor solución podría ser pasar todo el PSObject de PowerShell a python (antes de que comience el ciclo de PS) y devolverlo junto con PSObject, después de que se haya procesado en Python, pero no sé cómo puedo darse cuenta de esto con la función python / python.

¿Cuál sería su enfoque / sugerencias, alguna otra idea? Gracias :)

Respuestas

2 mklement0 Nov 28 2020 at 22:22

El siguiente ejemplo simplificado le muestra cómo puede pasar varias instancias [pscustomobject]( [psobject]) de PowerShell a un script de Python (pasado como una cadena a través de -cen este caso):

  • mediante el uso de JSON como formato de serialización, a través de ConvertTo-Json...

  • ... y pasar ese JSON a través de la canalización , que Python puede leer a través de stdin (entrada estándar).

Importante :

  • Codificación de caracteres :

    • PowerShell usa la codificación especificada en la $OutputEncodingvariable de preferencia al enviar datos a programas externos (como Python), que por defecto es el UTF-8 sin BOM en PowerShell [Core] v6 + , pero lamentablemente ASCII (!) En Windows PowerShell .

    • Al igual que PowerShell lo limita a enviar texto a un programa externo, también interpreta invariablemente lo que recibe como texto, es decir, en función de la codificación almacenada en [Console]::OutputEncoding; Lamentablemente, ambas ediciones de PowerShell en el momento de escribir este artículo tienen por defecto la página de códigos OEM del sistema .

    • Para enviar y recibir (sin BOM) UTF-8 en ambas ediciones de PowerShell , configure (temporalmente) $OutputEncodingy de la [Console]::OutputEncodingsiguiente manera:
      $OutputEncoding = [Console]::OutputEncoding = [System.Text.Utf8Encoding]::new($false)

  • Si desea que su secuencia de comandos de Python también genere objetos, considere nuevamente usar JSON , que en PowerShell puede analizar en objetos 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 los datos para pasar son una colección de cadenas de una sola línea , no necesita 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

Según la respuesta de @ mklement0, quiero compartir la solución completa y probada con la devolución del JSON de python a Powershell teniendo en cuenta la codificación de caracteres correcta. Ya lo probé con 100k filas en un lote, sin problemas, funcionando sin problemas y súper rápido :)

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

Pida disculpas por el desafortunado resaltado de sintaxis; El bloque de script contiene SQL, Powershell y Python .. 🙄