Показать сообщение отдельно
Старый 31.07.2006, 10:44   #14  
Gustav is offline
Gustav
Moderator
Аватар для Gustav
SAP
Лучший по профессии 2009
 
1,858 / 1152 (42) ++++++++
Регистрация: 24.01.2006
Адрес: Санкт-Петербург
Записей в блоге: 19
Несколько дней после обсуждения по горячим следам потихоньку мусолил тему. Не торопясь, в свободное время сваял джоб. Потом еще несколько дней раскрашивал его комментариями. А потом и забывать стал. Сегодня вспомнил, думаю, надо бы выложить, а то совсем забуду...
Цитата:
Сообщение от Gustav
Допустим, мы захотели регулярно читать его полученные и выданные репутации.
...
Однако и на этот случай "номерной неустойчивости таблиц" есть свое решение: веб-страница рассматривается целиком, без деления на таблицы, а точки начала интересующих нас таблиц определяются нахождением на листе "приёмник" ячеек, содержащих текст "Участник получил одобрение от других" и "Участник одобрил других" соответственно.
Джоб выгружает из АхФорума таблички репутаций четырех участников этой дискуссии и сводит их в одной таблице Excel, демонстрируя таким образом один из возможных вариантов получения и парсинга данных из web-страницы или html-файла. (Дальнейший путь информации из Excel в Axapta здесь не рассматривается в силу достаточно хорошей проработанности этой темы на Форуме.)
X++:
static void WebQuery_DemoJob(Args _args)
{
// ---------------------------------------------------------------------------
// ПРИМЕР получения табличной информации из web-страницы в Excel
// ---------------------------------------------------------------------------
    COM xlApp;     // Excel.Application
    
    COM wbks, wbk; // Workbooks, Workbook
    COM wkss;      // Worksheets
    
    COM wksReceiver;    // Worksheet "приёмник"
    COM wksAccumulator; // Worksheet "накопитель"
    
    COM rCells; // все ячейки (Worksheet.Cells) "приёмника"
    COM aCells; // все ячейки (Worksheet.Cells) "накопителя"
    
    COM qts, qt; // QueryTables, QueryTable
    
    COM rngUser; // Range - одиночная ячейка, из значения которой получается имя участника
    
    COM rngTO;   // Range - одиночная ячейка - "Точка Отсчета" ("Точка Опоры") очередной таблицы репутации,
                 // находится поиском на листе "приёмник" строки типа "Участник получил одобрение от других" и др.
                 // дальше от этой ТО "как от печки" всё "вытанцовывается"
    
    COM rngHeaders; // Range - блок ячеек - строка заголовков очередной таблицы репутации на листе "приёмник"
    COM rngRecords; // Range - блок ячеек - строки данных очередной таблицы репутации на листе "приёмник"
    
    COM rngAccHeaders; // Range - блок ячеек - строка заголовков таблицы репутации на листе "накопитель"
    COM rngAccRecords; // Range - блок ячеек - место "вставки" на листе "накопитель" очередной порции данных с листа "приёмник"
    
    COM comTemp; // временный COM-объект для промежуточных операций
                 // по превращению раннего связывания (многоточие VBA)
                 // в позднее связывание (одноточие X++)
    
    COMVariant cValue;
    
    boolean isFirst = true;
    
    int rowFirst, rowLast, countRecords, rowHeader;
    int colFirst, colLast, countCols;
    
    str strConnectionBeg = 'URL;http://axforum.info/forums/member.php?u=';
    // для файла будет что-то вроде strConnectionBeg = 'URL;c:\\myFolder\\myFile.htm'
    // или при использовании @: strConnectionBeg = @'URL;c:\myFolder\myFile.htm'
    str strConnection;
    str strUser;
    
    #define.xlAllTables(2)
    #define.xlSpecifiedTables(3)
    #define.xlWebFormattingNone(3)
    #define.xlDown(-4121)
    #define.xlToRight(-4161)
    #define.xlShiftToRight(-4161)
    
// ===================================================================================================
// СНАЧАЛА несколько служебных методов (потом - основной процесс)
    
// вычленение повторяющихся фрагментов из основного процесса и оформление их процедурами (методами),
// вызываемыми из основного процесса или друг из друга достаточно специфично для конкретной веб-страницы
// конкретный вариант такой декомпозиции обычно становится ясным на этапе предварительного анализа
    
// --------------------------------------------------------------------------------
// начальное оформление таблицы результатов (при обработке данных первого участника)
// --------------------------------------------------------------------------------
    void firstArrange()
    {
    
        // "накопитель" еще пустой
        // набиваем поля
        rngAccHeaders = wksAccumulator.Range(aCells.Item(1, 1), aCells.Item(1, countCols));
        rngAccHeaders.Value2( rngHeaders.Value2() );  // вот это неплохо :-), фактически матричное присваивание A=B
        rngAccRecords = rngAccHeaders.Offset(1, 0);
    
        // необходимое форматирование некоторых колонок "накопителя"
        // Дата
        comTemp = COM::createFromVariant(rngAccRecords.Item(1, 2));
        comTemp = comTemp.EntireColumn();
        comTemp.NumberFormat('ДД.ММ.ГГ чч:мм');
    
        // Автор - если появляется
        // всего в таблице репутации появляется или 3 колонки, или 5 (открываются колонки Очки и Автор)
        // в зависимости от того, сохранили ли вы в куках свой логин-пароль на АхФорум
        // соответственно вы входите на Форум веб-запросом как гость (будет 3 колонки в табл.реп.) или как участник (будет 5 колонок)
        if (countCols > 3)
        {
            // если вход - участника (а не гостя) - "не мальчика, но мужа!" :-)
            // устанавливаем текстовый формат для колонки "Автор" (иначе проблемы с попыткой Excel распознать формулы на символах =>)
            comTemp = COM::createFromVariant(rngAccRecords.Item(1, 4));
            comTemp = comTemp.EntireColumn();
            comTemp.NumberFormat([EMAIL="'@'"]'@'[/EMAIL]);
        }
    
        // на листе "накопитель" вставляем две дополнительные колонки слева от таблицы репутации
        comTemp = rngAccHeaders.Resize(1, 2);
        comTemp = comTemp.EntireColumn();
        comTemp.Insert(#xlShiftToRight);
    
        // расширяем диапазон заголовков на две добавленные колонки
        rngAccHeaders = rngAccHeaders.Offset(0, -2);
        rngAccHeaders = rngAccHeaders.Resize(1, countCols + 2);
    
        // заголовки двух добавленных слева колонок
        COM::createFromVariant(rngAccHeaders.Item(1, 1)).Value2('Участник');
        COM::createFromVariant(rngAccHeaders.Item(1, 2)).Value2('Направление');
    
        // заголовки - жирным
        comTemp = rngAccHeaders.Font();
        comTemp.Bold(true);
    
        // замораживаем строку заголовков
        COM::createFromVariant(rngAccHeaders.Item(2, 1)).Select(); // лист "накопитель" здесь уже активен
        comTemp = xlApp.ActiveWindow();
        comTemp.FreezePanes(true);
    
        isFirst = false;
    
    }
    
// --------------------------------------------------------------------
// обработка таблицы репутации участника (1 или 2 = верхняя или нижняя)
// --------------------------------------------------------------------
    void processReputationTable(int _tableNum)
    {
        switch (_tableNum)
        {
            case 1:
                rngTO = rCells.Find('Участник получил одобрение от других');
                break;
    
            case 2:
                rngTO = rCells.Find('Участник одобрил других');
                break;
        }
    
        if (rngTO == null)
            // выходим, если соответствующая таблица отсутствует в профиле участника
            return;
    
        // эксельный номер первой строки данных
        comTemp = rngTO.Offset(2, 1);
        rowFirst = comTemp.Row();
    
        // эксельный номер последней строки данных
        comTemp = comTemp.End(#xlDown);
        rowLast = comTemp.Row();
    
        // количество записей в данных (эксельных строк в таблице репутации)
        countRecords = rowLast - rowFirst + 1;
    
        // эксельный номер строки заголовков колонок (Тема, Дата и т.п.)
        rowHeader = rowFirst - 1;
    
        // эксельный номер первой колонки
        comTemp = rngTO.Offset(2, 1);
        colFirst = comTemp.Column();
    
        // эксельный номер последней колонки
        comTemp = rngTO.Offset(1, 2);
        comTemp = comTemp.End(#xlToRight);
        colLast = comTemp.Column();
    
        // количество столбцов (эксельных колонок в таблице репутации)
        countCols = colLast - colFirst + 1;
    
        // создаем объектные переменные для диапазонов: Заголовков и Записей
        rngHeaders = wksReceiver.Range(rCells.Item(rowHeader, colFirst), rCells.Item(rowHeader, colLast));
        rngRecords = wksReceiver.Range(rCells.Item(rowFirst, colFirst), rCells.Item(rowLast, colLast));
    
        // "возвращаем на место отъехавшую Тему" :-)
        // слово "Тема" при загрузке в Excel попадает не в ту ячейку, в которую нам надо
        // (все эти нюансы "поведения" изучаются на этапе предварительного анализа конкретной веб-страницы)
        COM::createFromVariant(rngHeaders.Item(1, 1)).Value2('Тема');
    
        // если проход - первый, то выполняем "аранжировку" накопителя
        if (isFirst)
            firstArrange();
    
        // переопределение диапазона в соответствии с требуемым кол-вом записей
        rngAccRecords = rngAccRecords.Resize(countRecords);
    
        // массовое прописывание значений в колонки "Тема"..."Комментарий" на листе "накопитель"
        rngAccRecords.Value2( rngRecords.Value2() ); // фактически "копирование без копирования"
    
        // массовое прописывание значений в колонку "Участник" на листе "накопитель"
        comTemp = rngAccRecords.Resize(COM::createFromObject(rngAccRecords.Rows()).Count(), 1);
        comTemp = comTemp.Offset(0, -2); // эта колонка "левее на 2" относительно колонки "Тема"
        comTemp.Value2(strUser);
    
        // массовое прописывание значений в колонку "Направление" на листе "накопитель"
        comTemp = rngAccRecords.Resize(COM::createFromObject(rngAccRecords.Rows()).Count(), 1);
        comTemp = comTemp.Offset(0, -1); // эта колонка "левее на 1" относительно колонки "Тема"
        switch (_tableNum)
        {
            case 1:
                comTemp.Value2('получил одобрение');
                break;
    
            case 2:
                comTemp.Value2('одобрил');
                break;
        }
    
        // переопределение диапазона для приема следующей порции данных
        rngAccRecords = rngAccRecords.Offset(countRecords);
        // фактически здесь нас интересует переопределенное положение только первой строки нового диапазона (новая "закладка")
        // а необходимое кол-во строк будет задано "выше" на следующей итерации в операторе: rngAccRecords = rngAccRecords.Resize(countRecords);
    
    }
    
// --------------------------------------
// обработка данных из профиля участника
// --------------------------------------
    void processUserData(int _userId)
    {
        strConnection = strConnectionBeg + int2str(_userId);
    
        if (isFirst)
        {
            // если проход - первый, то создаем новый web-запрос
            qts = wksReceiver.QueryTables();
            qt = qts.Add(strConnection, wksReceiver.Range('A1'));
    
            qt.Name('SelectUserProfileFromAxForum');
            // --------------------------------------
            // изначально было так:
            // qt.WebSelectionType(#xlAllTables);
            // но на одном из компьютеров, где были какие-то специфичные настройки Internet Explorer
            // таблицы репутаций стали "разрываться" дополнительными пустыми колонками (какой-то лишний Tab...)
            // --------------------------------------
    
            // --------------------------------------
            // проблема выше была решена за счет исключения из списка таблицы 2
            qt.WebSelectionType(#xlSpecifiedTables);
            qt.WebTables('1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30');
            // на самом деле таблиц на странице "Профиль участника" около 20,
            // остальные (до 30) - добавлены для надежности (ошибки это не вызывает)
            // --------------------------------------
    
            qt.WebFormatting(#xlWebFormattingNone);
            qt.Refresh(false); //BackgroundQuery:=False
        }
        else
        {
            // если не первый проход, то перестраиваем запрос
            qt.Connection(strConnection);
            qt.Refresh(false); //BackgroundQuery:=False
        }
    
        // получение имени участника
        rngUser = rCells.Find('Просмотр профиля: ');
        cValue = rngUser.Value2();
        strUser = strLRTrim(strReplace(cValue.bStr(), 'Просмотр профиля: ', ''));
    
        //обработка 1-й (верхней) таблицы репутации
        processReputationTable(1);
    
        //обработка 2-й (нижней) таблицы репутации
        processReputationTable(2);
    }
    
// ===================================================================================================
// ТЕПЕРЬ основной процесс
    
    xlApp = new COM('Excel.Application');
    
    wbks = xlApp.Workbooks();
    wbk = wbks.Add();
    
    wkss = wbk.Worksheets();
    
    wksReceiver = wkss.Item(1);
    wksReceiver.Name('Receiver');
    
    wksAccumulator = wkss.Item(2);
    wksAccumulator.Name('Accumulator');
    
    rCells = wksReceiver.Cells();
    aCells = wksAccumulator.Cells();
    
    wksAccumulator.Activate();
    
    processUserData( 259); // macklakov
    processUserData(2552); // belugin
    processUserData(5046); // Avick
    processUserData(5597); // Gustav
    
    xlApp.Visible(true);
    
}