Выгрузка тендеров с официального портала госзакупок

Введение

Расскажу о проекте который считаю интересным и который может быть полезен для многих компаний желающих найти новые заказы в лице гос.компаний. Как известно такие компании обязаны размещать информацию о закупке товаров/услуг на сайте госзакупок. Речь пойдёт о сайте госзакупок Российской Федерации http://zakupki.gov.ru.
Задача которая стояла передо мной звучала так: выгружать список тендеров с учётом фильтров.
Фильтры это:

  • дата/диапазон дат
  • регион
  • вхождение слов/словосочетаний в названии или описании тендера/лотов
  • номера кодов товаров/услуг
  • сумма тендера (сумма лотов)

Дальше предполагалось что список будет загружаться в базу данных компании, но сейчас я остановлюсь на сохранении списка в XML файле что бы пример был изолирован.
Реализация будет на Delphi XE6 хотя была версия и на C# и Delphi 7. Никаких сторонних библиотек не используется поэтому это не очень важно.

Собрав начальную информацию (погуглив по поводу портала госзакупок) я понял что оптимальным вариантом будет выгружать данные с FTP портала. Там хранятся все материалы до текущего дня (! т.е за текущий там нет) в полном объёме, сами тендеры в XML файлах заархивированные в zip.
Потом мне попалась хорошая статья написанная простым понятным языком на хабре где рассматриваются и другие варианты но они не выдерживают критики. Очень советую почитать для полной ясности. Так же там приводятся адреса/пароли к FTP.

Всё что нам надо храниться на FTP. Все тендеры сохранены в форматах соответствующим трём федеральным законам: 44-ФЗ, 223-ФЗ, 94-ФЗ и сохранены по разным директориям.
Последний (наверное) устаревающий формат и в нём мало тендеров. Изначально программа обрабатывала его формат но потом посчитали что не нужен и я отключил. В исходниках остались закомментированные участки. Дальше будет речь только о первых двух

Для 223-ФЗ:

  • FTP: ftp.zakupki.gov.ru Логин/Пароль:fz223free/fz223free
  • Директория: /out/published/<регион>/*Notice*/daily
  • где <регион> - это название региона, *Notice* - это все директории в названии которых встречается Notice

дальше будут файлы с такими названиями purchaseNoticeEP_Adygeya_Resp_20150407_000000_20150407_235959_daily_001.xml.zip. В названии содержится дата - именно по ней будет отбирать тендеры за нужный период.

Для 44-ФЗ:

  • FTP: ftp.zakupki.gov.ru Логин/Пароль:free/free
  • Директория: /fcs_regions/<регион>/notifications/currMonth
  • где <регион> - название региона

дальше будут файлы с такими названиями notification_Moskva_2015063000_2015070100_001.xml.zip. В названии так же содержится дата - по ней будет отбирать тендеры за нужный период.

Если посмотреть на название регионов то некоторые отличаются в написании, например:
Адыгея Респ: Adygeja_Resp и Adygeya_Resp.
Для этого я составил список с названием регионов и возможным их написанием. Этот список вынес в настройки в файл filter.xml. Так же в этот файл собрал все остальные требуемые фильтры.
"code" это код по классификатору «ОКПД», в данном случае это 72 - "Продукты программные и услуги, связанные с использованием вычислительной техники и информационных технологий"
Атрибут "e" это 1-вкл,0-выкл.

filter.xml

<?xml version="1.0" encoding="utf-8"?>
<filter>
	<regions>
		<region e="1" n="Адыгея Респ" t1="Adygeja_Resp" t2="Adygeya_Resp" t3=""/>
		<region e="1" n="Алтай Респ" t1="Altaj_Resp" t2="Altay_Resp" t3=""/>
		<region e="1" n="Алтайский край" t1="Altajskij_kraj" t2="Altayskii__krai" t3=""/>
		<region e="1" n="Амурская обл" t1="Amurskaja_obl" t2="Amurskaya_obl" t3=""/>
		<region e="1" n="Архангельская обл" t1="Arkhangelskaja_obl" t2="Arhangelskaya_obl" t3=""/>
		<region e="1" n="Астраханская обл" t1="Astrakhanskaja_obl" t2="Astrahanskaya_obl" t3=""/>
		<region e="1" n="Байконур г" t1="Bajkonur_g" t2="Baikonur_g" t3=""/>
		<region e="1" n="Башкортостан Респ" t1="Bashkortostan_Resp" t2="" t3=""/>
		<region e="1" n="Белгородская обл" t1="Belgorodskaja_obl" t2="Belgorodskaya_obl" t3=""/>
		<region e="1" n="Брянская обл" t1="Brjanskaja_obl" t2="Brianskaya_obl" t3=""/>
		<region e="1" n="Бурятия Респ" t1="Burjatija_Resp" t2="Buryatiya_Resp" t3=""/>
		<region e="1" n="Чеченская Респ" t1="Chechenskaja_Resp" t2="Chechenskaya_Resp" t3=""/>
		<region e="1" n="Челябинская обл" t1="Cheljabinskaja_obl" t2="Cheliabinskaya_obl" t3=""/>
		<region e="1" n="Чукотский АО" t1="Chukotskij_AO" t2="Chukotskii_AO" t3=""/>
		<region e="1" n="Чувашская Республика - Чувашия" t1="Chuvashskaja_Respublika_-_Chuvashija" t2="Chuvashskaja_Resp" t3="Chuvashskaya_Respublika"/>
		<region e="1" n="Дагестан Респ" t1="Dagestan_Resp" t2="" t3=""/>
		<region e="1" n="Еврейская Аобл" t1="Evrejskaja_Aobl" t2="Evreiskaya_Aobl" t3=""/>
		<region e="1" n="Ингушетия Респ" t1="Ingushetija_Resp" t2="Ingushetiya_Resp" t3=""/>
		<region e="1" n="Иркутская обл" t1="Irkutskaja_obl" t2="Irkutskaya_obl" t3=""/>
		<region e="1" n="Иркутская обл - Усть-Ордынский Бурятский округ" t1="Irkutskaja_obl_Ust-Ordynskij_Burjatskij_okrug" t2="Irkutskaya_obl_Ust-Ordynskii_Buriatskii_okrug" t3=""/>
		<region e="1" n="Ивановская обл" t1="Ivanovskaja_obl" t2="Ivanowskaya_obl" t3=""/>
		<region e="1" n="Ямало-Ненецкий АО" t1="Jamalo-Neneckij_AO" t2="Jamalo-Nenetckii_AO" t3=""/>
		<region e="1" n="Ярославская обл" t1="Jaroslavskaja_obl" t2="Jaroslavskaya_obl" t3=""/>
		<region e="1" n="Кабардино-Балкарская Респ" t1="Kabardino-Balkarskaja_Resp" t2="Kabardino-Balkarskaya_Resp" t3=""/>
		<region e="1" n="Калининградская обл" t1="Kaliningradskaja_obl" t2="Kaliningradskaya_obl" t3=""/>
		<region e="1" n="Калмыкия Респ" t1="Kalmykija_Resp" t2="Kalmykiya_Resp" t3=""/>
		<region e="1" n="Калужская обл" t1="Kaluzhskaja_obl" t2="Kaluzhskaya_obl" t3=""/>
		<region e="1" n="Камчатский край" t1="Kamchatskij_kraj" t2="Kamchatskii_krai" t3=""/>
		<region e="1" n="Карачаево-Черкесская Респ" t1="Karachaevo-Cherkesskaja_Resp" t2="Karachaevo-Cherkesskaya_Resp" t3=""/>
		<region e="1" n="Карелия Респ" t1="Karelija_Resp" t2="Kareliya_Resp" t3=""/>
		<region e="1" n="Кемеровская обл" t1="Kemerovskaja_obl" t2="Kemerowskaya_obl" t3=""/>
		<region e="1" n="Хабаровский край" t1="Khabarovskij_kraj" t2="Habarovskii_krai" t3=""/>
		<region e="1" n="Хакасия Респ" t1="Khakasija_Resp" t2="Hakasiia_Resp" t3=""/>
		<region e="1" n="Ханты-Мансийский Автономный округ - Югра АО" t1="Khanty-Mansijskij_Avtonomnyj_okrug_-_Jugra_AO" t2="Khanty-Mansijskij_AO-Jugra_AO" t3="Hanty-Mansiiskii_AO_Iugra_AO"/>
		<region e="1" n="Кировская обл" t1="Kirovskaja_obl" t2="Kirowskaya_obl" t3=""/>
		<region e="1" n="Коми Респ" t1="Komi_Resp" t2="" t3=""/>
		<region e="1" n="Костромская обл" t1="Kostromskaja_obl" t2="Kostromskaya_obl" t3=""/>
		<region e="1" n="Краснодарский край" t1="Krasnodarskij_kraj" t2="Krasnodarskii_krai" t3=""/>
		<region e="1" n="Красноярский край" t1="Krasnojarskij_kraj" t2="Krasnoyarskii_krai" t3=""/>
		<region e="1" n="Крым Респ" t1="Krim_Resp" t2="" t3=""/>
		<region e="1" n="Курганская обл" t1="Kurganskaja_obl" t2="Kurganskaya_obl" t3=""/>
		<region e="1" n="Курская обл" t1="Kurskaja_obl" t2="Kurskaya_obl" t3=""/>
		<region e="1" n="Ленинградская обл" t1="Leningradskaja_obl" t2="Leningradskaya_obl" t3=""/>
		<region e="1" n="Липецкая обл" t1="Lipeckaja_obl" t2="Lipetckaya_obl" t3=""/>
		<region e="1" n="Магаданская обл" t1="Magadanskaja_obl" t2="Magadanskaya_obl" t3=""/>
		<region e="1" n="Марий Эл Респ" t1="Marij_El_Resp" t2="Marii_El_Resp" t3=""/>
		<region e="1" n="Мордовия Респ" t1="Mordovija_Resp" t2="Mordoviya_Resp" t3=""/>
		<region e="1" n="Московская обл" t1="Moskovskaja_obl" t2="Moskovskaya_obl" t3=""/>
		<region e="1" n="Москва" t1="Moskva" t2="" t3=""/>
		<region e="1" n="Мурманская обл" t1="Murmanskaja_obl" t2="Murmanskaya_obl" t3=""/>
		<region e="1" n="Ненецкий АО" t1="Neneckij_AO" t2="Nenetckii_AO" t3=""/>
		<region e="1" n="Нижегородская обл" t1="Nizhegorodskaja_obl" t2="Nizhegorodskaya_obl" t3=""/>
		<region e="1" n="Новгородская обл" t1="Novgorodskaja_obl" t2="Novgorodskaya_obl" t3=""/>
		<region e="1" n="Новосибирская обл" t1="Novosibirskaja_obl" t2="Novosibirskaya_obl" t3=""/>
		<region e="1" n="Омская обл" t1="Omskaja_obl" t2="Omskaya_obl" t3=""/>
		<region e="1" n="Оренбургская обл" t1="Orenburgskaja_obl" t2="Orenburgskaya_obl" t3=""/>
		<region e="1" n="Орловская обл" t1="Orlovskaja_obl" t2="Orlovskaya_obl" t3=""/>
		<region e="1" n="Пензенская обл" t1="Penzenskaja_obl" t2="Penzenskaya_obl" t3=""/>
		<region e="1" n="Пермский край" t1="Permskij_kraj" t2="Permskii_krai" t3=""/>
		<region e="1" n="Приморский край" t1="Primorskij_kraj" t2="Primorskii_krai" t3=""/>
		<region e="1" n="Псковская обл" t1="Pskovskaja_obl" t2="Pskovskaya_obl" t3=""/>
		<region e="1" n="Ростовская обл" t1="Rjazanskaja_obl" t2="Rostovskaya_obl" t3=""/>
		<region e="1" n="Рязанская обл" t1="Rostovskaja_obl" t2="Ryazanskaya_obl" t3=""/>
		<region e="1" n="Саха /Якутия/ Респ" t1="Sakha_Jakutija_Resp" t2="Saha_Jakutiya_Resp" t3=""/>
		<region e="1" n="Сахалинская обл" t1="Sakhalinskaja_obl" t2="Sahalinskaya_obl" t3=""/>
		<region e="1" n="Самарская обл" t1="Samarskaja_obl" t2="Samarskaya_obl" t3=""/>
		<region e="1" n="Санкт-Петербург" t1="Sankt-Peterburg" t2="" t3=""/>
		<region e="1" n="Саратовская обл" t1="Saratovskaja_obl" t2="Saratovskaya_obl" t3=""/>
		<region e="1" n="Севастополь г" t1="Sevastopol_g" t2="" t3=""/>
		<region e="1" n="Северная Осетия - Алания Респ" t1="Severnaja_Osetija_-_Alanija_Resp" t2="Severnaja_Osetija-Alanija_Resp" t3="Severnaia_Osetiya_Alaniia_Resp"/>
		<region e="1" n="Смоленская обл" t1="Smolenskaja_obl" t2="Smolenskaya_obl" t3=""/>
		<region e="1" n="Ставропольский край" t1="Stavropolskij_kraj" t2="Stavropolskii_krai" t3=""/>
		<region e="1" n="Свердловская обл" t1="Sverdlovskaja_obl" t2="Sverdlovskaya_obl" t3=""/>
		<region e="1" n="Тамбовская обл" t1="Tambovskaja_obl" t2="Tambovskaya_obl" t3=""/>
		<region e="1" n="Татарстан Респ" t1="Tatarstan_Resp" t2="" t3=""/>
		<region e="1" n="Тюменская обл" t1="Tjumenskaja_obl" t2="Tiumenskaya_obl" t3=""/>
		<region e="1" n="Томская обл" t1="Tomskaja_obl" t2="Tomskaya_obl" t3=""/>
		<region e="1" n="Тульская обл" t1="Tulskaja_obl" t2="Tulskaya_obl" t3=""/>
		<region e="1" n="Тверская обл" t1="Tverskaja_obl" t2="Tverskaya_obl" t3=""/>
		<region e="1" n="Тыва Респ" t1="Tyva_Resp" t2="" t3=""/>
		<region e="1" n="Удмуртская Респ" t1="Udmurtskaja_Resp" t2="Udmurtskaya_Resp" t3=""/>
		<region e="1" n="Ульяновская обл" t1="Uljanovskaja_obl" t2="Ulianovskaya_obl" t3=""/>
		<region e="1" n="Владимирская обл" t1="Vladimirskaja_obl" t2="Vladimirskaya_obl" t3=""/>
		<region e="1" n="Волгоградская обл" t1="Volgogradskaja_obl" t2="Volgogradskaya_obl" t3=""/>
		<region e="1" n="Вологодская обл" t1="Vologodskaja_obl" t2="Vologodskaya_obl" t3=""/>
		<region e="1" n="Воронежская обл" t1="Voronezhskaja_obl" t2="Voronezhskaya_obl" t3=""/>
		<region e="1" n="Забайкальский край" t1="Zabajkalskij_kraj" t2="Zabaikalskii_krai" t3=""/>
		<region e="1" n="Забайкальский край - Агинский Бурятский АО" t1="Zabajkalskij_kraj_Aginskij_Burjatskij_okrug" t2="Zabaikalskii_krai_Aginskii_Buriatskii_okrug" t3=""/>
		<region e="1" n="Undefined" t1="undefined" t2="fcs_undefined" t3=""/>
	</regions>
	<keywords>
		<keyword e="1">вычислит</keyword>
		<keyword e="1">коммутатор</keyword>
		<keyword e="1">коммутацион</keyword>
		<keyword e="1">комплектующ</keyword>
		<keyword e="1">компьютер</keyword>
		<keyword e="1">маршрутиза</keyword>
		<keyword e="1">программн</keyword>
		<keyword e="1">разработка</keyword>
		<keyword e="1">сервер</keyword>
		<keyword e="1">сетев</keyword>
	</keywords>
	<codes>
		<code e="1">72</code>
	</codes>
	<summa>
		<min e="1">100000</min>
		<max e="1">10000000</max>
	</summa>
</filter>

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

Т.к предполагается что программа будет запускаться автоматически то я сделала с и без интерфейса в зависимости от передаваемого параметра. Интерфейс нужен только для отладки поэтому он минимален. Если интересен запуск без интерфейса описание параметров есть в файле ReadMe.txt в исходниках. Рассмотрим процедуру скачивания zip файлов с FTP (создание директорий и прочее опускаю) для ФЗ223, для ФЗ44 аналогично
uTenderList.pas

  IdFTP := TIdFTP.Create(Application);
  try
    if Assigned(output) then output.Add('223fz');
    IdFTP.Host := 'ftp.zakupki.gov.ru';
    IdFTP.Username := 'fz223free';
    IdFTP.Password := IdFTP.Username;
    IdFTP.ConnectTimeout := 5000;
    IdFTP.Connect();
    IdFTP.Passive := true;
    if IdFTP.Connected then
    begin
      IdFTP.ChangeDir('/out/published');
      lst_region.Clear;
      IdFTP.List(lst_region,'*', false);

      for cnt_oblast := 1 to RegionList.Count do
      begin
        if not TRegion(RegionList.List[cnt_oblast-1]).check then
          Continue;

        with TRegion(RegionList.List[cnt_oblast-1]) do
          Region := FindRegionName(t1, t2, t3);
        if Length(Region) < 6 then
          Continue;

        IdFTP.ChangeDir('/out/published/'+Region);

        lst_dir.Clear;
        IdFTP.List(lst_dir,'*Notice*', false);
        for cnt_dir := 1 to lst_dir.Count do
        begin
          Dir := Trim(lst_dir[cnt_dir-1]);
          if Length(Dir) < 6 then
            Continue;

          IdFTP.ChangeDir('/out/published/'+Region+'/'+Dir+'/daily');

          dt := DateFrom;
          while CompareDate(dt, DateTo) = -1 do
          begin
            mask := Dir+'_'+Region+'_'+FormatDateTime('yyyymmdd', dt)+'_000000_'+FormatDateTime('yyyymmdd', dt)+'_235959_daily_*.zip';

            lst_file.Clear;
            IdFTP.List(lst_file, mask, false);

            if lst_file.Count > 0 then
            begin
              dir_to_save := IncludeTrailingPathDelimiter(constDestFolder)+FormatDateTime('yyyymmdd', dt)+'\'+fz_array[0]+'\';
              for cnt_file := 1 to lst_file.Count do
              begin
                if IdFTP.Size(lst_file[cnt_file-1]) < 1000 then
                  Continue;

                if not DirectoryExists(dir_to_save) then
                  ForceDirectories(dir_to_save);

                if Assigned(output) then output.Add(lst_file[cnt_file-1]);
                IdFTP.Get(lst_file[cnt_file-1], dir_to_save+lst_file[cnt_file-1]);
              end;
            end;

            dt := dt+1;
          end;
        end;
      end;
      IdFTP.Disconnect;
    end;

строка 1049: RegionList - это просто список регионов из filter.xml. Свойство check это атрибут "e" или галка в интерфейсе.
строка 1053: функция FindRegionName ищет директорию в списке директорий lst_region (полученных с FTP в строке 1045) подставляя все возможные написания региона (t1, t2, t3 - из filter.xml)
Дальше находятся все директории содержащие Notice, для них выполняется переход в директорию daily, по маске выбираются все файлы удовлетворяющие диапазону дат, проверяется размер файла (файлы на указанную дату ОБЯЗАНЫ быть, если нет тендеров то архив пустой), если тендеров несколько они упаковываются в один файл (максимальное кол-во не помню, при превышении создаются файлы с постфиксом _001, _002.. и т.д) и загружаются в папку \out по датам.

Распаковка (кнопка UnZip) сделана средствами Windows через COM-объекст Shell.Application. Приведу вызов и саму процедуру распаковки. Вызывается одна для всех zip-файлов во всех папках \out\YYYYMMDD - так сделано что бы разделить процесс на независимые части.

..
  ShellUnzip(<путь_имя_zip_архива>, <выходная_директория>, '*noti*.xml') 
..
..
procedure ShellUnzip(ZipFile, TargetFolder: string; filter: string = '');
const
  SHCONTCH_NOPROGRESSBOX = 4;
  SHCONTCH_AUTORENAME = 8;
  SHCONTCH_RESPONDYESTOALL = 16;
  SHCONTF_INCLUDEHIDDEN = 128;
  SHCONTF_FOLDERS = 32;
  SHCONTF_NONFOLDERS = 64;
var
  shellobj: variant;
  SrcFldr, destfldr: variant;
  shellfldritems: variant;
  ZipFileV, TargetFolderV: Variant;
begin
  shellobj := CreateOleObject('Shell.Application');

  ZipFileV := ZipFile;
  TargetFolderV := TargetFolder;

  SrcFldr := shellobj.NameSpace(ZipFileV);
  destfldr := shellobj.NameSpace(TargetFolderV);

  shellfldritems := SrcFldr.Items;
  if (filter <> '') then
    shellfldritems.Filter(SHCONTF_INCLUDEHIDDEN or SHCONTF_NONFOLDERS or SHCONTF_FOLDERS,filter);

  destfldr.CopyHere(shellfldritems, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;

На выходе, после распаковки, получится куча XML файлов каджый из которых будет содержать один тендер.

Остаётся покопаться внутри этих XML файлов что бы выянить какие из них удовлетворяют оставшимся фильтрам - это делается по кнопке Filter.
Перебор всех XML файлов в \out\YYYYMMDD осуществляется в процедуре TTenderList.ApplyFilter(), проверка на соответствие фильтрам в FilterXmlFile - если возвращает True то извлекаются нужные данные из XML файла для последующего сохранения. Текст TTenderList.ApplyFilter() приводить не буду - там примитивный поиск файлов в директории.
uTenderList.pas

function TTenderList.FilterXmlFile(const fz,filename:string):boolean;
var
  XMLDoc : IXMLDOMDocument;
  PriceSumRub : double;
  PriceStr : string;
begin
  result := false;
  XMLDoc := CoDOMDocument.Create();
  try
    XMLDoc.load(filename);
    if XMLDoc.parseError.errorCode <> 0 then
      raise Exception.Create('XML Load error:' + XMLDoc.parseError.reason);

    if fz = 'fz44' then
    begin
      if IsCode(XMLDoc,'OKPD','code') or IsMatchKeyword(XMLDoc,'purchaseObjectInfo') or IsMatchKeyword(XMLDoc,'lotObjectInfo') then
      begin
        PriceSumRub := GetPriceSumRub(XMLDoc,'lot/maxPrice','','lot/currency','code',PriceStr);
        if ((SummaMin<0) or (PriceSumRub>=SummaMin)) and ((SummaMax<0) or (PriceSumRub<=SummaMax))then
          result := true;
      end;
    end else
    if fz = 'fz223' then
    begin
      if IsCode(XMLDoc,'okdp','code') or IsMatchKeyword(XMLDoc,'ns2:name') or IsMatchKeyword(XMLDoc,'subject') then
      begin
        PriceSumRub := GetPriceSumRub(XMLDoc,'lotData/initialSum','','lotData/currency','code',PriceStr);
        if ((SummaMin<0) or (PriceSumRub>=SummaMin)) and ((SummaMax<0) or (PriceSumRub<=SummaMax))then
          result := true;
      end;
    end;
    { else
    if fz = 'fz94' then
    begin
      if IsCode(XMLDoc,'oos:lot/oos:products/oos:product','oos:code') or IsMatchKeyword(XMLDoc,'oos:orderName') or IsMatchKeyword(XMLDoc,'oos:lot/oos:subject') then
        result := true;
    end;}
  finally
    XMLDoc := nil;
  end;
end;

Форматы XML файлов, точнее имена узлов, отличаются для разных ФЗ, поэтому пришлось разделить код по ФЗ. Фильтры проверяются функциями IsCode,IsMatchKeyword,GetPriceSumRub. В них в качестве параметра передаётся XML-документ и имя узла. Документацию в которой описываются узлы и что в них содержиться я не нашёл, просто смотрел и сопоставлял с тем что выводится на сайте.
По-поводу функций фильтров: они однотипные в качестве примера приведу одну IsMatchKeyword

(ниже перегруженный вариант функции который вызывается в конечном итоге, она в качестве первого параметра принимает xml-узел)

function TTenderList.IsMatchKeyword(const node:IXMLDOMNode; const TagName:string):boolean;
var
  item : IXMLDOMNode;
  list : IXMLDOMNodeList;
  cnt,cnt_patt : Integer;
  Info,patt,s : string;
begin
  Result := false;
  s := IfThen(Assigned(node.parentNode) and Assigned(node.parentNode.parentNode),'.')+'//'+TagName;
  list := node.selectNodes(s);
  if Assigned(list) then
  begin
    for cnt := 1 to list.length do
    begin
      item := list.nextNode();
      if Assigned(item) then
      begin
        Info := item.text;
        for cnt_patt := 1 to KeywordList.Count do
        begin
          if not TKeyword(KeywordList.Items[cnt_patt-1]).check then
            Continue;

          patt := TKeyword(KeywordList.Items[cnt_patt-1]).Name;
          if Length(patt)>3 then
            if AnsiContainsText(Info, patt) then
            begin
              result := true;
              exit;
            end;
        end;
      end;
    end;
  end;
end;

строка 3019: тут добавляется точка к строке для selectNodes. Точка указывает что нужно искать только в пределах указанного узла. Без точки поиск идёт по всему документу. В данном случае это не используется - поиск всегда идёт по всему документу но при работе с XML это важно помнить.
Дальше все найденные узлы (их может быть несколько, например в при проверке вхождения словосочетания в описание лота если лотов > 1) проверяются на вхождение для каждого словосочетания (KeywordList - список словосочетаний из filter.xml). Если один из них входит то возвращается положительный результат.

Хочу сказать пару слов о функции GetPriceSumRu. Она возвращает суммарную цену тендера. Дело в том что если в тендере один лот то цена тендера равна стоимости лота. В этом случае корректно говорить о цене тендера. Но если лотов несколько то корректно говорить только о цене отдельных лотов. Это как со средней температурой по больнице. Но заказчик захотел сумму, что бы она была одна, что бы её можно было с чем-то сравнить. Скажу ещё что валюта лотов даже в одном тендере может отличаться. Для этого пришлось подтягивать курсы (два основных: USD/EUR) что бы приводить эту цену к рос.рублям. В этом примере т.к нет базы я жёстко прописал курсы в файле uTenderList.pas.

В функии выше TTenderList.FilterXmlFile видно какие узлы XML-файлов ФЗ-223/ФЗ-44 я использовал для проверки фильтров. Теперь приведу класс для хранения нужных мне данных и узлы из которых извлекаются эти данные:
uConst.pas

type
  TPurchase = class
  public
    fz : string;
    placingWay : string;
    Number : string;
    Name : string;
    Lots : string;
    Price : string;
    Customer : string;
    Address : string;
    Customer_RegNum : string;
    Customer_INN : string;
    ContactName : string;
    ContactPhone : string;
    ContactEmail : string;
    StartDate : TDateTime;
    EndDate : TDateTime;
    RassmotrenieDT : TDateTime;
    AuktsionDT : TDateTime;
    Version : integer;
    Code : string;
    url : string;
    id_tender_region : integer; // это поле заполняется только когда данные берутся из базы
    tender_region : string; // название региона
    PriceSumRub : double; // суммарнуая цена тендера
  end;

Не буду вдаваться в подробности из названий полей очевидно для чего они. Если нет то достаточно сравнить результат с тем что выводит сайт.
uTenderList.pas

procedure TTenderList.XmlToTPurchase(const fz,filename:string);
var
  XMLDoc : IXMLDOMDocument;
var
  Purchase : TPurchase;
  l1 : TStringList;
  Version_str, dt_str : string;
begin
  if Assigned(output) then output.Add(filename);

  XMLDoc := CoDOMDocument.Create();
  try
    XMLDoc.load(filename);
    if XMLDoc.parseError.errorCode <> 0 then
      raise Exception.Create('XML Load error:' + XMLDoc.parseError.reason);

    Purchase := nil;
    try
      if fz = 'fz44' then
      begin
        Purchase:= TPurchase.Create();
        Purchase.id_tender_region := GetIdRegionFromFileName(filename);
        Purchase.tender_region := GetRegionFromFileName(filename);
        Purchase.fz := fz;
        Purchase.placingWay := GetNodeValue(XMLDoc,'placingWay','name');
        Purchase.Number := GetNodeValue(XMLDoc,'purchaseNumber');
        Purchase.Name := Trim(GetNodeValue(XMLDoc,'purchaseObjectInfo'));
        Purchase.Lots := Trim(GetGroupNodeValues(XMLDoc,'lotObjectInfo'));
        if Purchase.Lots = Purchase.Name then
          Purchase.Lots := '';

        Purchase.PriceSumRub := GetPriceSumRub(XMLDoc,'lot/maxPrice','','lot/currency','code',Purchase.Price);

        Purchase.Customer := GetNodeValue(XMLDoc,'responsibleOrg/fullName');
        Purchase.Address := GetNodeValue(XMLDoc,'responsibleOrg/factAddress');
        Purchase.Customer_RegNum := GetNodeValue(XMLDoc,'responsibleOrg/regNum');
        Purchase.Customer_INN := GetNodeValue(XMLDoc,'responsibleOrg/INN');
        Purchase.ContactName := GetNodeValue(XMLDoc,'contactPerson');
        Purchase.ContactPhone := GetNodeValue(XMLDoc,'responsibleInfo/contactPhone');
        Purchase.ContactEmail := GetNodeValue(XMLDoc,'responsibleInfo/contactEMail');

        dt_str := GetNodeValue(XMLDoc,'procedureInfo/collecting/startDate');
        Purchase.StartDate := MyStrToDateTime(dt_str);
        dt_str := GetNodeValue(XMLDoc,'procedureInfo/collecting/endDate');
        Purchase.EndDate := MyStrToDateTime(dt_str);
        dt_str := GetNodeValue(XMLDoc,'procedureInfo/scoring/date');
        Purchase.RassmotrenieDT := MyStrToDateTime(dt_str);
        dt_str := GetNodeValue(XMLDoc,'procedureInfo/bidding/date');
        Purchase.AuktsionDT := MyStrToDateTime(dt_str);

        Version_str := GetNodeValue(XMLDoc,'modificationNumber');
        if Version_str = '' then
          Version_str := '0';
        Purchase.Version := StrToInt(Version_str);

        l1 := TStringList.Create();
        try
          l1 := TStringList.Create();
          l1.Sorted := True;
          l1.Duplicates := dupIgnore;
          l1.Delimiter:=#13;
          l1.DelimitedText := GetGroupNodeValues(XMLDoc,'OKPD','',#13,'"');
          Purchase.Code := Trim(l1.Text);
        finally
          FreeAndNil(l1);
        end;

        Purchase.url := GetNodeValue(XMLDoc,'href');
        PurchaseList.Add(Purchase);
      end else
      if fz = 'fz223' then
      begin
        Purchase:= TPurchase.Create();
        Purchase.id_tender_region := GetIdRegionFromFileName(filename);
        Purchase.tender_region := GetRegionFromFileName(filename);
        Purchase.fz := fz;
        Purchase.placingWay := GetNodeValue(XMLDoc,'ns2:purchaseCodeName');
        Purchase.Number := GetNodeValue(XMLDoc,'ns2:registrationNumber');
        Purchase.Name := Trim(GetNodeValue(XMLDoc,'ns2:name'));
        Purchase.Lots := Trim(GetGroupNodeValues(XMLDoc,'lotData/subject'));
        if Purchase.Lots = Purchase.Name then
          Purchase.Lots := '';

        Purchase.PriceSumRub := GetPriceSumRub(XMLDoc,'lotData/initialSum','','lotData/currency','code',Purchase.Price);

        Purchase.Customer := GetNodeValue(XMLDoc,'ns2:customer/mainInfo/fullName');
        Purchase.Address := GetNodeValue(XMLDoc,'ns2:contact/organization/mainInfo/legalAddress');
        Purchase.Customer_RegNum := GetNodeValue(XMLDoc,'ns2:contact/organization/mainInfo/ogrn');
        Purchase.Customer_INN := GetNodeValue(XMLDoc,'ns2:contact/organization/mainInfo/inn');
        Purchase.ContactName := GetNodeValue(XMLDoc,'ns2:contact/firstName');
        Purchase.ContactPhone := GetNodeValue(XMLDoc,'ns2:contact/phone');
        Purchase.ContactEmail := GetNodeValue(XMLDoc,'ns2:contact/email');

        dt_str := GetNodeValue(XMLDoc,'ns2:createDateTime');
        Purchase.StartDate := MyStrToDateTime(dt_str);
        dt_str := GetNodeValue(XMLDoc,'ns2:submissionCloseDateTime');
        Purchase.EndDate := MyStrToDateTime(dt_str);
        dt_str := GetNodeValue(XMLDoc,'ns2:examinationDateTime');
        Purchase.RassmotrenieDT := MyStrToDateTime(dt_str);
        dt_str := GetNodeValue(XMLDoc,'ns2:summingupDateTime');
        Purchase.AuktsionDT := MyStrToDateTime(dt_str);

        Purchase.Version := StrToInt(GetNodeValue(XMLDoc,'ns2:version'));

        l1 := TStringList.Create();
        try
          l1 := TStringList.Create();
          l1.Sorted := True;
          l1.Duplicates := dupIgnore;
          l1.Delimiter:=#13;
          l1.DelimitedText := GetGroupNodeValues(XMLDoc,'lotItem/okdp','',#13,'"');
          Purchase.Code := Trim(l1.Text);
        finally
          FreeAndNil(l1);
        end;

        Purchase.url := 'http://zakupki.gov.ru/epz/order/quicksearch/search.html?searchString='+Purchase.Number;
        PurchaseList.Add(Purchase);
      end;

    except
      FreeAndNil(Purchase);
      raise;
    end;

  finally
    XMLDoc := nil;
  end;
end;

Листинг получился длинный но ничего не поделаешь: разделение по ФЗ и строки.

После фильтрации я получаю список объектов TPurchase который сохраняю в XML-файл. Для сохранения я ничего не изобретал: в блокноте создал XML файл которых хотел получить на выходе (template.xml в корне в исходниках) и воспользовался XML Data Binding (File -> New -> Other -> Delphi projects -> XML). Дельфи сгенерировала прокси-классы (немного пришлось подправить типы данных) элементов и списка который умеет сохранять в файл (файл template.pas). Тут приводить не буду, т.к никаких нюансов там нет, может за исключением добавления в файл инструкции XSL-трансформации, но если интересно есть в исходниках.

Последний скриншот результат в Excel

Исходный код и исполняемый файл

comments powered by Disqus
Яндекс.Метрика