Форум » GUI » экспорт в Excel (тормоза) » Ответить

экспорт в Excel (тормоза)

gustow: Не пойму, в чем причина... Объясните, братцы, "тупому" :) ---- делаю экспорт в Excel (база с лекарствами) .... oExcel := TOleAuto():New( "Excel.Application" ) oExcel:Visible := .F. oLibros := oExcel:Get( "WorkBooks" ) oLibro := oLibros:Add() oHoja := oExcel:Get( "ActiveSheet" ) oHoja:Cells:Font:Name := "Arial" oHoja:Cells:Font:Size := 10 row0:=3 pbi:=1 ... do while .not. AFTG_X->(eof()) // AFTG_X - база, из которой экспортирую (лекарства, цены...) // (aftg_fi - массив макросов для выводимых выражений) oHoja:Cells( row0+pbi, 1 ):Value := " " for iii:=1 to 20 do case case iii=9 // цена oHoja:Cells( row0+pbi, iii ):Value := strtran( str(&(aftg_fi[iii]), 11, 2), ".", ",") oHoja:Cells( row0+pbi, iii ):Set( "NumberFormat", "#######0,00" ) oHoja:Cells( row0+pbi, iii ):HorizontalAlignment := XL_RIGHT case iii=11 // сумма oHoja:Cells( row0+pbi, iii ):Value := strtran( &(aftg_fi[iii]), ".", ",") oHoja:Cells( row0+pbi, iii ):Set( "NumberFormat", "##########0,00" ) oHoja:Cells( row0+pbi, iii ):HorizontalAlignment := XL_RIGHT otherwise // все остальные - просто взять значение if valtype(&(aftg_fi[iii]))="C" // если это строка oHoja:Cells( row0+pbi, iii ):Value := trim(&(aftg_fi[iii])) else if iii=3 // "Код ЛС" oHoja:Cells( row0+pbi, iii ):Value := &(aftg_fi[iii]) oHoja:Cells( row0+pbi, iii ):HorizontalAlignment := XL_RIGHT else // другие oHoja:Cells( row0+pbi, iii ):Value := &(aftg_fi[iii]) endif endif endcase if valtype(&(aftg_fi[iii]))="N" // если это число - выравнять вправо oHoja:Cells( row0+pbi, iii ):HorizontalAlignment := XL_RIGHT endif next iii AFTG_X->(dbskip()) pbi++ enddo ... // формируем заголовок таблицы oHoja:Cells( 1, 1 ):Value := "Выборка из БД формуляров МУ" oHoja:Cells( 1, 1 ):Font:Size := 12 oHoja:Cells( 1, 1 ):Font:Bold := .T. oHoja:Range("A1:F1"):Merge() oHoja:Range("A1:F1"):HorizontalAlignment := XL_CENTER oHoja:Columns("A:"+ckmax):AutoFit() // ckmax - max.буква (самой правой из заполняемых граф) в Экселе for iii:=1 to 20 if at( str(iii,3), " 2 4 5 6 12 13 14 15" ) > 0 // устанавливаем ширину колонок: // ФТГ (наим.), МНН, Торг, Форма, ЛПУ, Терр., Основание, Цел.Прог. if oHoja:Columns(chr(asc("A")-1+iii)+":"+chr(asc("A")-1+iii)):ColumnWidth > 20 oHoja:Columns(chr(asc("A")-1+iii)+":"+chr(asc("A")-1+iii)):ColumnWidth := 20 endif endif next iii // центрируем данные по Ед.Изм. oHoja:Range("G"+ltrim(str(row0))+; ":G"+ltrim(str(row0+pbi-1))):HorizontalAlignment := XL_CENTER oHoja:Cells( 2, 1 ):Select() oExcel:Visible := .T. oHoja:End() oLibro:End() oLibros:End() oExcel:End() ..... и вроде всё... Непонятка в том, что на небольших выборках (100-200-400) всё отрабатывает "мухой", а при попытке выкинуть 25-30 тысяч - ОППАНЬКИ... и идём нервно курить минут на ..дцать. Комп шуршит там чего-то, трудится... а процесс "нескончаем" (во всяком случае, за "разумное" время - ну пусть ЕДИНИЦЫ минут! а это "рубилово" идет минут 20-30!!!). На всякий - операционная обстановка: Win98, Office-97 [увы -так НАДО!..], Athlon 2400, оперативки 512 [сам знаю, что мало! но на "обычное экселЕние" хватает выше головы] И - заодно уж - подскажите неразумному: не пойму, как пользовать (для того же форматирования в ячейках, к примеру) экселовские константы (xlRight, xlCenter и пр.)? Пишу, например: oHoja:Cells( row0+pbi, iii ):HorizontalAlignment := oExcel:Constants():xlRight (или что-то типа) - и, естественно, бываю послан... Пришлось вверху писать дифайны вроде #define XL_RIGHT -4152 (понимаю, что глупо ["всё уже украдено до нас!"] - но "с налету" не понял еще, как правильно сделать) TsBrowse'овский Excel2() не выходит использовать (по ряду причин) (хотя потестил - получил тот же "нервный перекур"...) Или это "напряги" Эксела?.. Не может переварить 30 тыс.строк "за раз"? Заранее спасибо! "То ли лыжи погнулись... то ли я..." :)

Ответов - 32, стр: 1 2 All

Pasha: Наверное ничего с этим не поделаешь, Эксель так и работает Я когда-то пытался понять причину, и у меня возникло впечатление, что с каждой следующей Row работа идет медленнее. Т.е, чтобы выбрать строку с большим номером N, мелкософтовский продукт лопатит строки 1 .. N-1 Возможно в более поздних версиях это улучшено Но все рано код надо оптимизировать, введя промежуточные переменные вместо oHoja:Cells( row0+pbi, iii ):Value := strtran( str(&(aftg_fi[iii]), 11, 2), ".", ",") oHoja:Cells( row0+pbi, iii ):Set( "NumberFormat", "#######0,00" ) oHoja:Cells( row0+pbi, iii ):HorizontalAlignment := XL_RIGHT надо oCell := oHoja:Cells( row0+pbi, iii ) oCell:Value := strtran( str(&(aftg_fi[iii]), 11, 2), ".", ",") oCell:NumberFormat := "#######0,00" oCell:HorizontalAlignment := XL_RIGHT Метод Set лучше не использовать, с прямым обращением работает лучше

les: Эта тема уже здесь обсуждалась года два назад. Набери в поиске TOleAuto, почитай. Виноват не Excel, а механизм OLE. Тут ничего не поделаешь. Для себя я решил проблему так: 1. Вывод информации в HTML-файл с расширением .xls программой на основе TB2Html от Jovan Bulajic 2. Вызов Excel с параметром <имя файла>. Работает ГОРАЗДО быстрее чем OLE!

Andrey: les пишет: 1. Вывод информации в HTML-файл с расширением .xls программой на основе TB2Html от Jovan Bulajic А можно поподробнее в этом месте, с примером ? Заранее спасибо.


les: Andrey пишет: А можно поподробнее в этом месте, с примером Извини, поподробнее не могу - давно это было. Бери первоисточник здесь: http://www.the-oasis.net/files/general/tb2html.zip Там есть описалово. И ещё здесь: http://www.pctoledo.com.br/forum/viewtopic.php?p=20287&sid=a58d9a3586d06e9baa844bf150f87d18

gustow: Спасибо за помощь! Буду "копать"... А то начальство захотело непременно "экселить" выборки (ну привыкли они!). Кстати: писал сперва, что "затыкается" на 20-30 тыщах... фигвам! те же "тормоза", оказывается, уже после 1-1,5 тысяч. :((

gustow: сыскал решение проблемы! гонит в Эксел "мухой"! в нижележащем примере (на 1000 строк): через OLE - 32 сек :(( ; через буфер обмена ~2 сек. ===================== /* from comp.lang.xharbour: Yes, cell by cell is still slow. I use the clipboard functions when I have more than a screen full of data. Run the code below and you will see the dramatic difference between cell update versus paste from clipboard method. */ #include "minigui.ch" #include "common.ch" FUNCTION MAIN() LOCAL oExcel, oSheet LOCAL nRow LOCAL nCounter, nStart, nSeconds, nSecOle, nSecClip // oExcel = CREATEOBJECT( "Excel.Application" ) oExcel := TOleAuto():New( "Excel.Application" ) oExcel:WorkBooks:Add() // oSheet := oExcel:ActiveSheet oSheet := oExcel:Get("ActiveSheet") // GAL oExcel:Visible := .T. // GAL (добавил - просто чтоб видеть процесс) nRow := 2 oSheet:Cells( nRow, 1 ):Value = "Counter" oSheet:Cells( nRow, 2 ):Value = "Date" oSheet:Cells( nRow, 3 ):Value = "Row" nCounter := 1 nStart := nCounter // ------------------ Start Cell by Cell method nSeconds := seconds() DO WHILE nCounter < 1000 oSheet:Cells( nCounter+nRow, 1 ):Value := nCounter oSheet:Cells( nCounter+nRow, 2 ):Value := date()-nCounter oSheet:Cells( nCounter+nRow, 3 ):Value := nCounter-1 nCounter++ ENDDO nSecOle := seconds()-nSeconds // ------------------ Stop Cell by Cell oSheet:Cells( 1, 5):Value := "OLE time, sec" oSheet:Cells( 1, 6):Value := nSecOle nRow += nCounter+2 oSheet:Cells( nRow, 1 ):Value := "Counter" oSheet:Cells( nRow, 2 ):Value := "Date" oSheet:Cells( nRow, 3 ):Value := "Row" // ------------------ Start Clipboard method nSeconds := seconds() nCounter := 1 nStart := nCounter cMemo := '' DO WHILE nCounter < 1000 // build record cMemo += ltrim( str( nCounter ) ) cMemo += chr(9)+dtoc( date()-nCounter ) cMemo += chr(9)+ltrim( str( nCounter+nRow-1 ) ) cMemo += chr(10) nCounter++ // update sheet every 1000 records or eof() *IF mod( nCounter, 1000 ) = 0 // .or. eof() IF mod( nCounter, 100 ) = 0 // .or. eof() /* почему-то выдает ошибку ("нет такой ф-и" - хотя RTL.LIB подключается) GTSetClipboard( cMemo ) заменил на CopyToClipboard( cMemo ) */ CopyToClipboard( cMemo ) oSheet:Cells( nRow+nStart, 1 ):Select() oSheet:paste() nStart := nCounter cMemo := '' ENDIF ENDDO nSecClip := seconds()-nSeconds // ------------------ Stop Clipboard method oSheet:Cells( 1, 8):Value := "CLIP time, sec" oSheet:Cells( 1, 9):Value := nSecClip oSheet:Cells( 1, 11):Value := "1000 rows" // ------------------ Results on Screen * ? 'Ole = '+ltrim(str(nSecOle)) * ? 'Clip = '+ltrim(str(nSecClip)) * wait * oSheet:Columns( "A:C" ):AutoFit() oSheet:Columns( "A:I" ):AutoFit() // чтобы форматировало и графы тайминга oSheet:Cells( 1, 1 ):Select() // GAL oExcel:Visible := .T. // добавил для закрытия (не было) oSheet:End() oExcel:End() RETURN( nil ) =====================

Петр: gustow пишет: сыскал решение проблемы! гонит в Эксел "мухой"! В некоторых случаях такое решение может быть небезопасным. Есть еще вариант выгрузки в файл с разделителями ( например CHR(9) ) средствами [x]Harbour и импорт средствами Excel.

Pasha: gustow пишет: И - заодно уж - подскажите неразумному: не пойму, как пользовать (для того же форматирования в ячейках, к примеру) экселовские константы (xlRight, xlCenter и пр.)? Ай хэв файлы word.ch и excel.ch с определениями констант Не помню, откуда он у меня взялся, то ли сам делал, то ли скачал где-то. Давно это было Могу выслать

Andrey: Pasha пишет: Ай хэв файлы word.ch и excel.ch с определениями констант Очень надо ! 30195@mail.ru Заранее благодарен

ММК: Петр пишет: В некоторых случаях такое решение может быть небезопасным. В каких? А то я постоянно его ( клипборд) использую.. aDb_cr := Ra->( dbstruct() ) for nCol := 1 to len( aDb_cr ) aadd( aHd, aDb_cr[ nCol, 1 ] ) next aHd[1]:="Попа" oClip := TClipBoard():New() oExc := TExcels():New() oExc :Font("Arial Cyr") oExc :SetFont("Arial Cyr") m := 1 Ra->( dbgotop() ) do while !Ra->( eof() ) cStr := "" for n := 1 to len( aDb_cr ) cField := aDb_cr[ n, 1 ] cStr1 := Ra->&cField if aDb_cr[ n, 2 ] == "N" cStr1 := alltrim( str( cStr1 ) ) endif cStr += cStr1 + if( n # len( aDb_cr ), chr( 9 ), "" ) next if !empty( cStr ) oClip:SetText( cStr ) oExc:SetPos( "A" + alltrim( str( m ) ) ) oExc:Paste() oClip:Clear() m ++ endif Ra->( dbskip() ) enddo

Петр: ММК пишет: В каких? А то я постоянно его ( клипборд) использую.. И все работает прекрасно? Случай первый: пользователь запустил программу, которая экспортирует таким образом базу обьемом так в 1000000 записей. Поскольку он (пользователь) знает, что подобный процесс довольно длительный, а времени терять не хочется, он запускает Word и что-то там начинает делать с использованием очень распространенного метода Copy-Paste. Случай второй: у пользователя установлена какая-то ну уж очень нужная программа, которая висит в трее и все время проверяет, что там в буфере обмена происходит (переводчик, клавиатурный нинзя или шпион, что-то другое ) и производит какие-то операции над содержимым буфера. Случай третий: у клиента установлена не одна, а целых две ваших (или чужих) программы, которые постоянно его ( клипборд) используют и он их запустил на выполнение одновременно.. И так далее.

ММК: Петр пишет: И все работает прекрасно? Вот черт!! :))) Еслиб я сначало прочитал все , что ниже наверняка бы не работало:))) А может каждая прожка использует свой кусочек памяти для клипборда? Хотя не могу с Вами не согласится - если подумать , вроде так . А, буду ждать пока чё не случится. Ну не все же они сразу будут клипбордом пользоваться:)

Петр: ММК пишет: А может каждая прожка использует свой кусочек памяти для клипборда? Я не думаю, что мы должны теряться в догадках msdn довольно подробно все излагает. Еше предлагаю по этой ссылке посмотреть как не надо писать программы http://www.flounder.com/badprogram.htm

ММК: Петр пишет: как не надо писать программы :))) Наверное было бы здорово если бы мы прислушивались ко всем советчикам:)) Но не все советы ... Действительно ( и без всяких догадок) клипборд он и есть клипборд, это всем понятно. Но работа с ним дает хороший результат , а все остальное просто надо иметь в виду:))На всякий случай я не жду заполнения всей страницы, oClip:SetText( cStr ) oExc:SetPos( "A" + alltrim( str( m ) ) ) oExc:Paste() oClip:Clear() Никто от ворда не отказался? :)

krutoff: Посмотрите код вывода таблицы в Excel напрямую - может не стоит париться с Ole?

Vlad04: В Делфи есть примеры (вроде не официальные ) по работе с Ехсел. Все аналогично по работе через буфер.Только там стоит еще процедура очистки и все ок. Сам пробовал, вопросов не было

Andrey: Народ, а за время написания этой статьи не появились ли новые возможности БЫСТРОГО перегона DBF для EXCEL ? Появилась нужда 65 000 записей (25-30 столбцов/полей ) перегнать в простую таблицу EXCEL с простым заголовком. В какую сторону копать, чтобы не наткнутся на Статьи на форуме читал....

Vlad04: А способ, который описывает ММК не пробовал? Он должен быть достаточно быстрый (относительно)

santy: Есть класс texcel разработанный Marcelo Torres. Архив находится по ссылке http://www.the-holms.org/xharbour/addon.htm Grid to Excel Class By Marcelo Torres там используется COPYCLIPBOARD.

Andrey: santy пишет: Есть класс texcel разработанный Marcelo Torres. Что-то даже демка не работает ...

gustow: Протестировал копипаст в Excel 2007 Вылетает (Error BASE/1004 Метод не экспортирован: SELECT) (Win XP, HMG Ext. 2.0.1) при попытке вставить в строку экселки с номером больше 65536 (64к). Т.ч. все работает в общем-то быстро (10'000 записей около 20 сек.) - но аппетиты по кол-ву вставляемых строк надо соразмерять :) Возможно, в 2010м Экселе этого ограничения нет?..

Andrey: gustow пишет: Протестировал копипаст в Excel 2007 Пример в студию... gustow пишет: Вылетает (Error BASE/1004 Метод не экспортирован: SELECT) (Win XP, HMG Ext. 2.0.1) при попытке вставить в строку экселки с номером больше 65536 (64к). Т.е. записи больше 65536 не попадут в таблицу ?

gustow: если надо кусок рабочего примера (криворукий, конечно, но работает :) ) - держи: [pre2] private gtim0 := seconds(), gtim1 // для тайминга use SPMESLPU codepage "RU866" new oSheet:Cells( 5, 1 ):Select() nRow00 := 5 // самая начальная строка, с которой вставляем nRow := 5 // текущая строка nRow0 := 5 // с какой строки вставлять очередной кусок do while .not.SPMESLPU->(EoF()) cClip := "" for j := 1 to 200 // чем больше буфер, тем быстрее // (НО! чем "шире" запись, тем легче упрёмся // в макс.длину строки = 65к... или неправ?) cClip := cClip + ; ltrim( str(SPMESLPU->LPU , 7, 0) ) + chr(9) + ; // еще бы (если десятичные точки есть) ltrim( str(SPMESLPU->MESBEG, 8, 0) ) + chr(9) + ; // заменять по ходу точки на запятые ltrim( str(SPMESLPU->MESEND, 8, 0) ) + chr(9) + ; // strtran( ... , ".", "," ) ltrim( dtoc(SPMESLPU->DBEG) ) + chr(9) + ; ltrim( dtoc(SPMESLPU->DEND) ) + chr(9) + ; ltrim( str(SPMESLPU->POCL , 2, 0) ) + chr(9) + ; ltrim( str(SPMESLPU->OTD , 4, 0) ) + chr(9) + ; ltrim( str(SPMESLPU->TYPEMS, 1, 0) ) skip nRow++ if SPMESLPU->(EoF()) exit endif if j < 200 cClip := cClip + chr(10) endif next j // копируем-вставляем CopyToClipboard( cClip ) // встаем на ту ячейку, начиная откуда надо вставлять oSheet:Cells( nRow0, 1 ):Select() // начиная с 1-й // вставляем из буфера обмена: oSheet:Paste() if .not.SPMESLPU->(EoF()) nRow0 := nRow endif enddo // выделяем весь диапазон вставленного oSel:=oSheet:Range( "A" + ltrim(str( nRow00, 0 )) + ":H" + ltrim(str( nRow-1, 0 )) ) oSel:VerticalAlignment := xlTop oSel:Rows:AutoFit() oSheet:Cells( 4, 1 ):Select() // ставим курсор (в этом листе) над шапкой gtim1 := seconds() close SPMESLPU MsgInfo("Тайминг: " + ltrim(str(gtim1-gtim0, 1)) + " сек." )[/pre2]

gustow: Andrey пишет: Т.е. записи больше 65536 не попадут в таблицу ? В 2007м, получается, что нет :(( Может, я неправ?.. но пока так выходит....

Andrey: А как к этой таблице заголовки (на русском) приделать и название таблицы сделать (типа МОЯ ТАБЛИЦА - шрифт побольше) ?

gustow: Ну что-то типа (коррекция примера начиная с "enddo"): [pre2] enddo // выделяем весь диапазон вставленного oSel:=oSheet:Range( Diapazon( "A", nRow0, "H", nRow-1 ) ) oSel:VerticalAlignment := xlTop oSel:Rows:AutoFit() // выделяем ячейки во 2-й строке для заголовка над таблицей oSel:=oSheet:Range( "A2:H2" ) // объединяем ячейки oSel:Merge() // заголовок над таблицей oSheet:Cells( 2, 1 ):Value := "Список лицензированных МЭС для всех ЛПУ" // заголовки для колонок oSheet:Cells( 4, 1 ):Value := "Код ЛПУ" oSheet:Cells( 4, 2 ):Value := "Нач. код МЭС" oSheet:Cells( 4, 3 ):Value := "Кон. код МЭС" oSheet:Cells( 4, 4 ):Value := "Нач. дата" oSheet:Cells( 4, 5 ):Value := "Кон. дата" oSheet:Cells( 4, 6 ):Value := "Поколение" oSheet:Cells( 4, 7 ):Value := "Отделение" oSheet:Cells( 4, 8 ):Value := "Тип МС" // выделяем общий и колоночные заголовки oSel:=oSheet:Range( "A2:H4" ) oSel:VerticalAlignment := xlCenter oSel:HorizontalAlignment := xlCenter // заголовки полужирно oSel:Font:Bold := .T. oSel:Rows:AutoFit() oSheet:Cells( 4, 1 ):Select() // ставим курсор (в этом листе) над шапкой gtim1:=seconds() // тайминг close SPMESLPU[/pre2] Или, наоборот, все дела про заголовки сделать ДО "do while ..." - а потом встал на ячейку, откуда начинать вставлять, и погнали наши городских... :)

Andrey: gustow пишет: если надо кусок рабочего примера Спасибо БОЛЬШОЕ !

gustow: шрифт у общего (надтабличного) заголовка побольше: [pre2] // выделяем ячейки во 2-й строке для заголовка над таблицей oSel:=oSheet:Range( "A2:H2" ) // объединяем ячейки oSel:Merge() oSel:Font:Size := 14 // заголовок над таблицей oSheet:Cells( 2, 1 ):Value := "Список лицензированных МЭС для всех ЛПУ"[/pre2]

Andrey: gustow пишет: oSheet:Cells( 5, 1 ):Select() gustow пишет: // выделяем весь диапазон вставленного oSel:=oSheet:Range( "A" + ltrim(str( nRow00, 0 )) + ":H" + ltrim(str( nRow-1, 0 )) ) oSel:VerticalAlignment := xlTop oSel:Rows:AutoFit() Немного запутался... oSel: это oSel := oExcel:ActiveSheet() Просто нет в начале открытия ОЛЕ... Каждый по разному открывает... Приведи пожалуйста кусок открытия ОЛЕ ! И как потом результат в файл сохранить ?

gustow: Звиняйте, батьку :) думал, понятно (потому как "стандартно"). В начале (открытие OLE) банально так: [pre2] oExcel := TOleAuto():New( "Excel.Application" ) if Ole2TxtError() != 'S_OK' MsgStop('А у вас не установлен MS Excel !', "Ай-яй-яй! Низззяяяя..." ) Return Nil endif oExcel:Visible := .F. // невидимость Эксела при формировании // (хотя тут тестил БЕЗ невидимости - все равно быстро) //oExcel:ScreenUpdating := .F. // выключил прорисовку листа (ускоряет) // отменить автоматическую калькуляцию формул (ускоряет) //oExcel:Calculation := xlManual oExcel:WorkBooks:Add() oBook := oExcel:Get("ActiveWorkBook") oSheet := oExcel:Get( "ActiveSheet" ) ... oSheet:Cells( 5, 1 ):Select() и т.д.[/pre2] И уже selection'ы диапазонов ячеек (называя указатель на выделенный диапазон как oSel) делаю (в данном случае) в этом конкретном текущем листе, который oSheet. Извиняй, может запутал ненамеренно... :) Сохранение в файл в большинстве случаев я как раз не делаю - потому как просто Excel делаю видимым и показываю пользователю, ЧТО вывелось в пока еще безымянную экселку; а он потом, если надо, сам "сохранит как", распечатает и т.п. (зачем винчестер мусором загаживать - сперва "сохранив как"... а вдруг не надо было вообще?) А если надо автоматом "сохранить в файл с предписанным именем и закрыться без вопросов", то так, например: [pre2] ... oBook:SaveAs( fold + "\" +"Список МУ.xls", xlExcel8 ) // сохранить в формате "совместимый с Excel 97-2003" (xlExcel8 = 56) // если сохранить "в формате по умолчанию", параметр опускаем // "fold" - папка, куда сохраняем (например, где-то выше "fold:=GetCurrentFolder()" ) oBook:Close(0) // закрыть БЕЗ вопросов о сохранении oExcel:Quit() // закрываем сеанс OLE oSheet:=Nil // зачищаем следы антигосударственной деятельности oBook:=Nil // не думаю, что это уж так обязательно - но вдруг неправ? oExcel:=Nil [/pre2] что-то вроде этого...

Andrey: gustow пишет: что-то вроде этого... Спасибо БОЛЬШОЕ !

gustow: Во! :) А теперь на основе этого "ликбеза" надо, думаю, написать главку в Викикнигу про Харбор (назвав типа "Харбор и Эксел"). Кто б подмог из "техписо-способных"?



полная версия страницы