четверг, 6 декабря 2007 г.

День четвёртый (web harvesting)

ЗАДАЧА
Для работы с данными из веба надо сначало извлечь их. Затем сконвертировать в объекты, понимаемые R. Поскольку содержание страниц может меняться, надо как - то автоматизировать сбор данных, по возможности сделав его устойчивым к изменению страницы или, по крайней мере, легко модифицируемым.

РЕШЕНИЕ
Полностью автоматизировать сбор, основываясь, например, на регулярности элементов DOM не получится - слишком часто встречаются тучи без смысла вложенных TABLE. Поэтому я сделал ставку на визуальное выделение "интересных" элементов страницы с последующим назначением инструкций по их обработке. Для работы плагина нужен прокси, чтобы добавить в тело страницы строчки:

script src="scripts/jquery.js" type="text/javascript"
script src="scripts/jqDnR.js" type="text/javascript"
script src="scripts/selector.rule.js" type="text/javascript"
а также загрузить сами сценарии из scripts/. Можно обойтись и без jqDnR.js - функциональность небольшая, а мороки много. Кое - что я сам поправил - убрал прозрачность (были из - за неё проблемы). С остальным разбираться - нет знаний и желания. Плагин работает неторопливо, поэтому, в частности, два режима работы. В одном клик на навигационной панели приводит к прыжку (откату выделения), в другом - к предпросмотру содержимого элемента.
/*
* SelectorRule - jQuery plugin for a quick navigation over 
* html tree
*
* Author: dimiii
*
* Dual licensed under the MIT and GPL licenses:
*   http://www.opensource.org/licenses/mit-license.php
*   http://www.gnu.org/licenses/gpl.html
*
* Version: r1
*
*/

(function($) {
 var style = {
   tasks_panel : {
     "position" : "absolute", "zIndex" : "500", "width" : "75%",
     "padding" : "5px", "margin" : "0px",
     "background-color" : "#eff",  "border" : "1px solid #999",
     "textAlign" : "left"
   },
   hint_area : {
     "padding" : "5px", "margin" : "5px", "width" : "95%", 
     "height" : "200px",
     "background-color" : "#eff", "border" : "1px dotted"
   },
   cmd_area : {
     "padding" : "5px", "margin" : "5px", 
     "width" : "50%", "height" : "80px", 
     "border" : "1px dotted"
   },
   mouseover : {
     "border" : "2px solid lime"
   },
   mouseclick : {
     "border" : "3px solid green"
   }
 };

 /*
  Node selector contains
 */
 $.fn.nodeSelector = function() {
   var click_trace = [];
   var tasks_panel = null;   

   if (!tasks_panel) {
     tasks_panel = $() // It seems blogger doesn't understand pre tag.
// So here is a commented tasks_panel's skeleton.
     .appendTo("BODY")
     .hide()
     .bind("click", function(e) {
       e.stopPropagation();
     });
        
     $(this).bind("contextmenu", function(e) {
       display(tasks_panel, e, click_trace);
       activate_actions(tasks_panel, click_trace);
       return false;
     });
   }

   mk_clickable(document.body, click_trace);
  
   return this;
 };

 function display(tasks_panel, e, click_trace) {
  
   $(".jqHandle").css("background-color", "lightgrey");

   $("#panel_stuff")
   .empty()
   .append(get_mode_slctr())
   .append(get_jump_nav(click_trace, true))
   .append(get_hint_text(click_trace))
   .append(get_command_panel())
   .append(get_btns());
  
   $("A")
   .css({"font-weight" : "bold", 
         "text-decoration" : "none", "color" : "black"})
   .bind("mouseover", function() { $(this).css("color", "red"); })
   .bind("mouseout", 
     function() { $(this).css("color", "black"); });
   
   tasks_panel
   .css(style.tasks_panel)
   .css({"left":0.15* document.width,"top":e.pageY})
   .show();
 };

 function activate_actions(tasks_panel, click_trace)
 {
   $(".jump_nav")
   .bind("click",
     function() {
       if ("selected" == $("#jumpModeBtn").attr("class")) {
      
         var  clcked = click_trace[$(this).attr("id")],
              trace_pos = click_trace.pop(); 
      
         while(trace_pos != clcked) {
           $(trace_pos.childNodes).each(
             function() { brk_clickable(this); });
           $(trace_pos).css({"border" : trace_pos.cssMemo || ""});
           trace_pos = click_trace.pop();
         }
     
         click_trace.push(clcked);//back
      
         $(clcked.childNodes).each(
           function() { mk_clickable(this, click_trace); });
        
         tasks_panel.hide();
       } else {
         $("#hint_area")
         .empty()
         .append($(click_trace[$(this).attr("id")]).text());
       }
     });

   $("#closeBtn")
   .bind("click", function(){ tasks_panel.hide(); });

   $("#commitBtn")
   .bind("click",  function(){ 
                     alert(get_jump_nav(click_trace, false));
                     tasks_panel.hide(); });
  
   $("#insPathBtn")
   .bind("click", 
     function(){ $("#cmd_area")
                 .append(get_jump_nav(click_trace, false)); });
  
   $("#jumpModeBtn")
   .bind("click",
     function() {
       if("selected" != $(this).attr("class")) {
         $(this).empty().append("[jump]").addClass("selected");
         $("#viewModeBtn")
         .empty().append("view").removeClass("selected");
       }
     });
  
   $("#viewModeBtn")
   .bind("click",
     function() {
       if("selected" != $(this).attr("class")) {
         $(this)
         .empty().append("[view]").addClass("selected");
         $("#jumpModeBtn")
         .empty().append("jump").removeClass("selected");
       }
     });
 
   tasks_panel.jqDrag(".jqDrag");
 };

 function mk_clickable(docnode, click_trace) {
   if ($(docnode).attr("id") == "tasks_panel") return;

   switch (docnode.nodeName.toString().toLowerCase()) {
     // we can't make clickable tbody and tr, 
     // so let's make clickable their childs
     case "tbody":
     case "tr":
       $(docnode.childNodes).each(
         function() { mk_clickable(this, click_trace); });
     break;

     case "body":
     case "div":
     case "p":
     case "span":
     case "a":
     case "table":
     case "td":
       try {
         docnode.cssMemo = $(docnode).css("border");
       }catch(exc){}

       $(docnode)
       .bind("mouseover", 
         function() { $(this).css(style.mouseover); })
       .bind("mouseout",  
         function() { $(this).css({"border" : docnode.cssMemo  
                                              || ""}); })
       .bind("click", 
         function() {
           click_trace.push(this);
           $(this).css(style.mouseclick);

           switch(docnode.parentNode.nodeName.toString()
                 .toLowerCase()) {
             case "tbody":
             case "tr":
               $(docnode.parentNode.parentNode.childNodes).each(
                 function() { brk_clickable(this); });
             break;

             default:
               $(docnode.parentNode.childNodes).each(
                 function() { brk_clickable(this); });
             break;
           }

           $(docnode.childNodes).each(
             function() { mk_clickable(this, click_trace); });
         });
     break;
   }
 };

 function brk_clickable(docnode) {
   switch(docnode.nodeName.toString().toLowerCase()) {
     case "tbody":
     case "tr": // reasons like for mk_clickable
       $(docnode.childNodes).each(
         function() { brk_clickable(this); });
     break;
        
     default:
       $(docnode)
       .unbind("mouseover").unbind("click").unbind("mouseout");
     break;
   }
 };

 /*
   Implementations of other functions are skipped.
 */

})(jQuery);
$(function() {
 $("BODY A").attr("href", "#");// defence
 $("BODY A").unbind("click", function() { mk_clickable(this);} );

 $("BODY").nodeSelector();
});
Инструкции предполагается записывать на DSL или непосредственно на R. Окончательный выбор сделаю после более подробного знакомства с библиотекой XML.

ВЫВОД
Работает и каши не просит.

БОНУС
Сегодня, через весьма насыщенный http://twit88.com узнал о существовании http://web-harvest.sourceforge.net. Основательный подход, но пока я никаких преимуществ перед моим "тынц-тынц" методом не вижу. Всё же, следует знать.

Комментариев нет: