Panggil skrip Python per PowerShell & teruskan PSObject dan kembalikan data yang diurai

Nov 28 2020

beberapa latar belakang: saat ini saya menanyakan 4Mio baris (dengan 50 kolom) dari server MS SQL dengan dbatools ke dalam PSObject (dalam Batch 10.000 baris setiap permintaan), memproses data dengan PowerShell (banyak hal RegEx) dan menulis kembali menjadi MariaDb dengan SimplySql . Rata-rata saya mendapatkan kira-kira. 150 baris / detik. Harus menggunakan banyak trik (Net's Stringbuilder dll.) Untuk kinerja ini, itu tidak terlalu buruk

Sebagai persyaratan baru, saya ingin mendeteksi bahasa beberapa sel teks dan saya harus menghapus data pribadi (nama & alamat). Saya menemukan beberapa libs python yang bagus ( spacy dan pycld2 ) untuk tujuan itu. Saya melakukan tes dengan pycld2 - deteksi yang cukup bagus.

Kode yang disederhanakan untuk klarifikasi (petunjuk: Saya adalah 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

Panggilan python ini setiap kali berfungsi, tetapi itu merusak kinerja (12 baris / detik) karena panggilan-loop dan mengimpor pycld2 lib setiap kali. Jadi, ini adalah solusi yang payah :) Selain itu, seperti yang disebutkan di atas - saya ingin menggunakan spacy - di mana beberapa kolom lagi harus diurai untuk membuang data pribadi.

Saya tidak yakin, apakah saya ingin mengubah seluruh PS Parser ke python: |

Saya percaya, solusi yang lebih baik mungkin meneruskan seluruh PSObject dari PowerShell ke python (sebelum loop PS dimulai) dan mengembalikannya serta PSObject - setelah diproses dengan python - tetapi saya tidak tahu, bagaimana saya bisa sadari ini dengan fungsi python / python.

Apa pendekatan / saran Anda, jika ada ide lain? Terima kasih :)

Jawaban

2 mklement0 Nov 28 2020 at 22:22

Contoh sederhana berikut menunjukkan kepada Anda bagaimana Anda dapat mengirimkan beberapa [pscustomobject]( [psobject]) contoh dari PowerShell ke skrip Python (diteruskan sebagai string melalui -cdalam kasus ini):

  • dengan menggunakan JSON sebagai format serialisasi, melalui ConvertTo-Json...

  • ... dan meneruskan JSON tersebut melalui pipeline , yang dapat dibaca Python melalui stdin (input standar).

Penting :

  • Pengkodean karakter :

    • PowerShell menggunakan pengkodean yang ditentukan dalam $OutputEncodingvariabel preferensi saat mengirim data ke program eksternal (seperti Python), yang secara default ditetapkan ke BOM-less UTF-8 di PowerShell [Core] v6 + , tetapi sayangnya ke ASCII (!) Di Windows PowerShell .

    • Sama seperti PowerShell yang membatasi Anda untuk mengirim teks ke program eksternal, PowerShell juga selalu menafsirkan apa yang diterimanya sebagai teks, yaitu berdasarkan pengkodean yang disimpan di [Console]::OutputEncoding; sayangnya, kedua edisi PowerShell pada tulisan ini default ke halaman kode OEM sistem .

    • Untuk mengirim dan menerima (tanpa BOM) UTF-8 di kedua edisi PowerShell , (sementara) setel $OutputEncodingdan [Console]::OutputEncodingsebagai berikut:
      $OutputEncoding = [Console]::OutputEncoding = [System.Text.Utf8Encoding]::new($false)

  • Jika Anda ingin skrip Python Anda juga menampilkan objek, pertimbangkan lagi untuk menggunakan JSON , yang pada PowerShell Anda dapat mengurai menjadi objek 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'])

'@

Jika data yang akan diteruskan adalah kumpulan string baris tunggal , Anda tidak memerlukan 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

Berdasarkan jawaban @ mklement0, saya ingin membagikan solusi lengkap dan teruji dengan mengembalikan JSON dari python ke Powershell dengan pertimbangan pengkodean karakter yang benar. Saya sudah mencobanya dengan 100rb Baris dalam satu batch - tidak ada masalah, berjalan dengan sempurna dan super cepat :)

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

Mohon maaf atas penyorotan sintaks yang tidak menguntungkan; blok skrip berisi SQL, Powershell dan Python .. 🙄