ShS's Blog

Just another sysadmin's weblog

Скрипт поиска файлов-писем (почтовых сообщений) по полю “Subject” (“Тема”). Дубль два (или работа над ошибками)

Posted by shs на 2010/04/08

Как выяснилось, в предыдущем посте я «оговорил» PoSh, заявив, что он не имеет встроенных возможностей построчного чтения и прекращения обработки файла по динамически вычисляемому условию. Но Василий Гусев, аки коршун ;), бросился на защиту любимого shell’а и указал на ошибочность моих заявлений (за что ему — большое спасибо).
На чем базировались мои ошибочные предположения? Я полагал, что каждый командлет в конвейере работает независимо от другого, распараллеливая обработку информации. Например, я ошибочно полагал, что связка Get-content some_file.txt | foreach {…} будет работать следующим образом: Get-Content, независимо от того, что происходит дальше по конвейеру, будет читать файл строка за строкой и построчно отдавать результат в выходной поток в сторону комадлета (или скриптблока), который стоит дальше по конвейеру и принимает/обрабатывает данные из потока, сформированного для него комадлетом-предшественником. Поэтому, я ошибочно предположил, что прерывание работы какого-либо командлета (или скриптблока) в конвейере никак не скажется на работе командлетов, стоящих от него по конвейеру слева, а, значит, Get-content будет продолжать свою работу, выбирая данные и отправляя их в конвейер строка за строкой.  Но, оказалось, что это не так, и это можно легко проверить при помощи простой последовательности команд (опять спасибо Василию):

Get-content som_file.txt| %{write-host $_; $_}| %{start-sleep 20; Write-Host $_;Break}

На самом деле в данном примере конвейер ведет себя, как, своего рода, выполняемый в цикле набор последовательно вызываемых на исполнение функций, где результат, возвращаемый очередной функцией, является параметром вызова следующей, а команда Break прерывает выполнение всего конвейера целиком (я же ошибочно полагал, что будет остановлен только текущий скриптблок и все последующие, стоящие по конвейеру справа).

Так же  можно «реабилитировать» и оператор Switch ;) Надо только вспомнить, о том, что помимо оператора Break, прерывающего обработку, существует еще и оператор Continue, позволяющий пропустить оставшуюся часть «цикла» и прейти к новой итерации.

Переписанный (с использованием встроенных возможностей PoSh для работы с файлом) скрипт теперь выглядит так:

####################################################################################################################
#
# GetMailsSubject.ps1 PowerShell shs 20100407
#
####################################################################################################################
#
#.Назначение
#	Декодирование поля Subject почтовых сообщений (файлов) в заданной папке и вложенных подпапках.
#	Опционально - удаление почтовых сообщений (файлов) согласно заданной маске для поля Subject
#
#.Параметры
#
#	.\GetMailsSubject.ps1 <Path2Dir> [<MaskOfSubj>]
#
#	Path2Dir 	- путь к папке, в которой будет происходить поиск почтовых сообщений (файлов с расширением *.msg)
#
#	MaskOfSubj	- маска-регулярное выражение. Все файлы почтовых сообщений, чье декодированое поле Subject
#				  соответствует заданой маске, будут удалены.
#
#.Описание
#	Скрипт считывает все файлы *.msg в заданной папке и подпапках и ищет в них строки,
#	из которых состоит поле Subject. Если в этих строках имеется текст, закодированный при помощи Quoted-printable
#	или Base64, то декадирует его и реконструирует текст поля Subject.
#	Скрипт должен быть запущен, по крайней мере, с одним обязательным параметром, содержащим путь к папке, в которой будет
#	происходить поисх файлов-сообщений. Второй параметр запуска - необязатьельный. Если второй параметр запуска не указан,
#	то скрипт просто выводит на экран декодированный текст поля Subject для каждого сообщения (файла), иначе скрипт будет
#	выводить на экран содержимое поля Subject, которое соответствует заданной этим параметром маске.
#
#	Все файлы, чье  декодированое содержимое поля Subject соответствует указанной маске, будут удалены!!!
#
#
#
####################################################################################################################
#Параметры вызова скрипта
#
param ($Path2Dir=$(throw "Укажите путь к папке с письмами в качестве параметра запуска"), $MaskOfSubj)
#
#===================================================================================================================
#					Функция декодирования Quoted-printable|Base64 строки
#
#  $CodePage 		- наименование текстовой кодовой страницы
#  $EncodingMethot	- флаг метода кодирования: Q|B (Q - quoted printable, B - base 64)
#  $EncodedString	- строка, подлежащая декодированию
#=
function DecodeQPB64 ($CodePage, $EncodingMethod, $EncodedString){
	$DecodedString=""
	#Декодируем Base64
	if ($EncodingMethod -eq "B") {
		$DecodedString=[System.Text.Encoding]::GetEncoding($CodePage).GetString([System.Convert]::FromBase64String($EncodedString))
	}
	#Декодируем Quoted-printable
	if ($EncodingMethod -eq "Q") {
		[regex]$regex="(?:=([0-9A-F]{2,2}))|([^=]+)"
		$match=$regex.match($EncodedString)
		#
		$DecodedString=""
		while ($match.Success) {
		$charcode=$match.Groups[1].value
		$chars=$match.Groups[2].value
		if ($charcode) {
   			$charcode=[int]("0x"+$charcode)
   			$DecodedString+= [System.Text.Encoding]::GetEncoding($CodePage).GetString($charcode)
		}
		if ($chars) {
			$DecodedString+=($chars -replace "_", " ")
		}
		$match=$match.NextMatch()
		}
	}
	$DecodedString
}
#
#=======================================================================================================================
#=
#								 Функция декодирования поля Subject
#=
function DecodeSubjString ($SubjString) {
	#Если строка содержит кодированную в Base64 или Quoted-printable подстроку,
	#то разберем эту строчку при помощи regex и, затем, декодируем.
	if ($SubjString -match "\s*(.*)(?==\?[^=])=\?(.+?)\?([BQ])\?(.+)\?=(?<=\?=)(.*)" ) {
		#
		#Реузультаты разбора строки при помощи regexp:
		#$Matches[1] - некодированные символы, которые находятся до начала кодированного текста ("prefix")
		#$Matches[2] - наименование языковой кодовой страницы, использованной  в троке
		#$Matches[3] - метод кодирования (Q - quoted printable, B - Base64)
		#$Matches[4] - закодированная строка
		#$Matches[5] - некодированные символы, которые находятся после кодированного текста ("suffix")
		#
		#Отправляем закодированную часть строки на перекодировку
		$Decoded = DecodeQPB64 $Matches[2] $Matches[3] $Matches[4]
		#Строка поля Subject может содержать, как кодированную часть, так и некодированные,
		#Соберем строку Subject воедино, вернув на место некодированные части,
		#отрезанные на этапе рабора строки при помощи regexp
		$DecodedSubjString = $Matches[1] + $Decoded + $Matches[5]
	}
	#Иначе, декодировать нечего - выводим as is
	else {
		$DecodedSubjString = $SubjString
	}
	## Возвращаем результат работы функции
	$DecodedSubjString
	#
}
##
#=================================================================================================================
# 															Начало скрипта
#Включаем (Continue) или выключаем (SilentlyContinue) вывод отладочной информации
$VerbosePreference = "Continue" #"SilentlyContinue" #Continue
#Очистим экран
cls
#Проверяем путь к папке, переданный в скрипт в виде параметра
#Если существует, то продолжаем работу
if (Test-Path "$Path2Dir") {
	#Перебираем все файлы *.msg в заданной папке и подпапках
	dir "$Path2Dir" *.msg -recurse|
	#Здесь начинается обработка очередного письма
	foreach {
		#Устанавливаем счетчик текущей строки поля Subject в 0
		$SubjectLineNum=0
		#Обнуляем текущее переменную, содержащую декодированное содержимое поля Subject для текущего письма
		$Subject=""
		#Сохраним полное имя файла в переменную (оно нам потом понадобиться)
		$FullFileName = $_.FullName
		#Передаем полное имя файла в конвейер
		$FullFileName
	}|
	foreach {
		#Парсим текущий файл при помощи оператора switch
		switch -file "$FullFileName" {

			#Если это первая строка поля Subject, то...
			{$_ -match "^Subject:.+"}
				{
					#...отбрасываем, идущее сначала строки слово Subject: с последующими пробелами
					# (выделяем данные пля Subject из строки этого поля)
					$_ -match "^Subject:\s*(.+)" | Out-Null
					#Вывод отладочной информации
					Write-Verbose $FullFileName
					#Устанавливаем счетчик текущей строки поля Subject в 1
					$SubjectLineNum=1
					#Вывод отладочной информации
					Write-Verbose $Matches[1]
					#Декодируем первую строку поля Subject
					$Subject = DecodeSubjString $Matches[1]
					#Возврат к началу оператора switch (Переходим к следующей строке файла)
					continue
				}

			#Если это вторая или любая последующая строка поля Subject, то...
			# (Примечание: вторая или любая последующая строка поля subject
			#  начинается с симовла "пробел" или "табуляция" см. rfc 822)
			{($_ -match "^\s.+") -and ($SubjectLineNum -gt 0)}
				{
					#...увеличиваем счетчик текущей строки поля Subject
					$SubjectLineNum+=1
					#Вывод отладочной информации
					Write-Verbose "SubjLineNum=$SubjectLineNum"
					#Выделяем данные пля Subject из строки этого поля
					$_ -match "^\s(.+)" | Out-Null
					#Вывод отладочной информации
					Write-Verbose $Matches[1]
					#Добавляем к иоговому результату результат декодирования текщей строки поля Subject
					$Subject += DecodeSubjString $Matches[1]
					Continue
				}

			#Если поле Subject закончилось, то прерываем обработку текущего файла
			# (Примечание: если вторая или любая последующая строка, идущая после первой строки поля Subject,
			#  не начинается с символа "пробел" или "табуляция", значит она не относится к полю Subject)
			{($_ -notmatch "^\s.+") -and ($SubjectLineNum -gt 0)}
				{
					Break
				}
		}
		#Окончательная обработка
		#Если при запуске скрипта был указан второй параметр запуска, то
		#его значение используется в качестве regexp для отбора и удаления файлов
		#Будут удалены все файлы (письма), у которых тема письма удовлетворяет заданной маске.
		if ($MaskOfSubj) {
			if ($Subject -match $MaskOfSubj) {
				#Выводим результат (декодированное поле Subject)
				Write-Host $Subject
				#Удаление файлов, соответствующих маске
				del $FullFileName -Force -WhatIf
			}
		}
		else {
			#Выводим результат (декодированное поле Subject)
			Write-Host $Subject
		}
		Write-Verbose "========="
	}
}
else {
	Write-Host "Указанная папка с файлами (`"$Path2Dir`") не найдена"
}

Вот, только стал ли он от этого лучше своего предшественника (как по «читабельности», так и по скорости работы)? ;)
Что-то я сомневаюсь…

Реклама

комментариев 5 to “Скрипт поиска файлов-писем (почтовых сообщений) по полю “Subject” (“Тема”). Дубль два (или работа над ошибками)”

  1. angel-keeper said

    Для проверки скорости работы в Posh есть cmdlet Measure-Command {…}

    • shs said

      Спасибо за подсказку.

      Провел по 4 замера для каждого из вариантов скрипта на одинакоом наборе файлов (233 файла). Немного выигрывает первый вариант (среднее время выполнения — 28,9 с, а у второго варианта среднее время выполнения — 29,7 с) В общем, разница не велика ~2-3%

      Запуск производил из PowerGUI. Позже попробую потестить работу скриптов в PowerShell.exe (если результаты будут сильно отличаться, отпишусь в комментах)

      • Xaegr said

        4 замера — не репрезентативно.
        Я предпочитаю делать так:
        $code = {dir c:\windows}
        (1..100 | foreach {measure-command $code}) | Measure-Object -Average -Property totalMilliseconds
        Причем независмо от шелла в котором тестируется — стоит отключить в скрипте любой вывод данных на экран. Слишком уж сильно он искажает результаты и замедляет процесс.

        • shs said

          >Причем независмо от шелла в котором тестируется – стоит отключить в скрипте любой вывод данных на экран. Слишком уж сильно он искажает результаты и замедляет процесс.

          Да я обратил на это внимание и перед тестированием привел скрипты к единому знаменателю (не удалив вывод сообщений вообще, но сведя его к идентичным сообщениям, выводимым на экран обоими скриптами).

          >4 замера – не репрезентативно
          Да, надо было бы побольше. Но меня очень удивило, что время работы обоих вариантов почти одинаково. Я, честно говоря, думал, что первый вариант победит с бОльшим отрывом.

  2. […] […]

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

 
%d такие блоггеры, как: