An important announcement from the founder of property-bee.com: The future of Property Bee is assured.

Zoopla, FindAProperty and Vebra support

An area to discuss the development and technical aspects of the toolbar

Zoopla, FindAProperty and Vebra support

Postby rlph on Wed Dec 15, 2010 3:15 pm

Hi all - was introduced to PropertyBee recently. Great tool, but didn't support all the sites I wished it did. Decided I'd fix that, and bashed together support for Zoopla, FindAProperty and Vebra. It's a bit rough in places but usable.

Here's the diff, for want of the ability to attach a file...

Code: Select all
Index: chrome/content/pb/rule.js
===================================================================
--- chrome/content/pb/rule.js   (revision 185)
+++ chrome/content/pb/rule.js   (working copy)
@@ -175,6 +175,15 @@
     }
 
     /**
+     *   Functor to obtain the alternate text from an image, and pass the attribute string through a child functor.
+     */
+    this.Alt = function (/**Functor*/ fn) /**PB.Rule.Alt*/ {
+       this.evaluate = function(/**Node*/ context) /**Variant*/ {
+           return fn.evaluate(context.alt);
+       }
+    }
+
+    /**
      *   Functor to obtain the id attribute from a node, and pass the attribute string through a child functor.
      */
     this.ID = function (/**Functor*/ fn) /**PB.Rule.ID*/ {
@@ -466,6 +475,22 @@
            return samples;
        }
     }
+   
+    /**
+     *  Functor which evaluates a regular expression against a String.  If the regexp matches,
+     *  passes the contents of the specified capture group through a child functor.
+     *  If not a match, return the empty string.
+     */
+    this.RegExpCapture = function( /**RegExp*/ regexp, /** Integer */ captureGroup, /**Functor*/ fn) /**PB.Rule.RegExpCapture*/ {
+        this.evaluate = function(/**String*/ text ) /**Variant*/ {
+            var result = "";
+            var groups = regexp.exec(text);
+            if (groups != null) {
+               result = fn.evaluate(groups[captureGroup]);
+            }
+            return result;
+       }
+    }
 });
 
 PB.unitTests(function () {
@@ -600,5 +625,15 @@
               .test("dummy test", function () { return undefined; })
           .end_fn()
       .end_class()
+        .class("RegExpCapture")
+            .fn("evaluate")
+              .test("dummy test", function () { return undefined; })
+          .end_fn()
+      .end_class()
+        .class("Alt")
+            .fn("evaluate")
+              .test("dummy test", function () { return undefined; })
+          .end_fn()
+      .end_class()
     .end_namespace();
 });
\ No newline at end of file
Index: chrome/content/pb/add_on/rule/zoopla.js
===================================================================
--- chrome/content/pb/add_on/rule/zoopla.js   (revision 0)
+++ chrome/content/pb/add_on/rule/zoopla.js   (revision 0)
@@ -0,0 +1,177 @@
+PB.namespace("PB.AddOn.Rule", function () {
+
+   /**
+    *   Rules for parsing Zoopla webpages.
+    */
+   this.Zoopla = function () {
+
+      function _resultsPage() {
+         var AGENT_REGEXP = /Marketed\s+by\s+(.*?)(?:,\s+([A-Z][A-Z]?\d\d?[A-Z]?))?\.\s+Contact\s+on\s+(\d[\d\s]+\d)(?:\s|$)/;
+         var AGENT_XPATH = ".//div[@class='listing-results-right']//p[@class='top-half'][1]";
+         var DETAIL_LINK_XPATH = ".//h3[@class='bottom-half']/a";
+      
+           return new PB.Rule.NodesMatchingXPath(
+              "//ul[@class='listing-results']//li",
+              new PB.Rule.ConstructProperty(
+              
+               // reference
+               new PB.Rule.FirstNodeMatchingXPath(".",
+                  new PB.Rule.ID(
+                     new PB.Rule.TextAfter("listing_",
+                        new PB.Rule.AddPrefix("zo", new PB.Rule.NoConversion())))),
+
+               // url
+               new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                     new PB.Rule.HRef(new PB.Rule.NoConversion())),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath("./div[@class='listing-results-right']",
+                  new PB.Rule.NoConversion()),
+
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {                       
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='listing-results-right']//strong[contains(@class,'price')]",
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(/(\xa3?\d{1,3}(?:,\d{3})*)(?=[^\d]|$)/, 1, new PB.Rule.NoConversion()))),
+
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='listing-results-right']//span[@class='small']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//div[@class='listing-results-left']//span[starts-with(@class,'listing-status-')]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+
+                     "Agent":
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 1, new PB.Rule.NoConversion()))),
+
+                     // TODO - doesn't work because FirstNodeMatchingXPath doesn't return same result 2x in a row!
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 2, new PB.Rule.NoConversion()))),
+                     
+                     // TODO - doesn't work because FirstNodeMatchingXPath doesn't return same result 3x in a row!
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 3, new PB.Rule.NoConversion()))),
+
+                     "Brief Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='listing-results-right']//p",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))                     
+                  }
+               )
+            )
+         );
+       };
+
+      function _detailPage() {
+         var AGENT_XPATH = ".//div[@class='seperate' and h2='Property description']/p[3]";
+         var AGENT_REGEXP = /\bplease\s+contact\s+(.*?),\s+(.*?)\s+on\s+([\d\s]+?)\s*$/;
+         
+           return new PB.Rule.NodesMatchingXPath(
+              ".//div[@id='mbody']",
+            new PB.Rule.ConstructProperty(
+               // reference
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(
+                        new PB.Rule.RegExpCapture(/\/details\/(\d+)/, 1,
+                           new PB.Rule.AddPrefix("zo", new PB.Rule.NoConversion()))))),
+               
+               // url
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(new PB.Rule.NoConversion()))),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(".//div[@class='seperate' and h2='Property description']/p[1]",
+                  new PB.Rule.NoConversion()),
+            
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//p/strong[@class='buyers']",
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(/(\xa3?\d{1,3}(?:,\d{3})*)(?=[^\d]|$)/, 1, new PB.Rule.NoConversion()))),
+                     
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//p/span[@class='nofontcolor']/strong[@class='nobold']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//span[starts-with(@class,'listing-status-')]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+                     
+                     "Agent" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 1, new PB.Rule.NoConversion()))),
+                     
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 2, new PB.Rule.NoConversion()))),
+
+                     "Agents Address" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@id='listings-agent']//br/../child::text()[last()]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 3, new PB.Rule.NoConversion()))),
+                     
+                     "Detailed Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='seperate' and h2='Property description']/p[1]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+                  }
+               )
+            )
+         );
+      };
+
+
+      /*
+       * Arguments; - documentNode is a handle to the document to capture info
+       *
+       * Return; - site identifies which site/rules were used - results contains
+       * the captured info as a hierarchy of objects (more on this later)
+       *
+       * If the capture method fails, an exception should be thrown.
+       */
+      this.capture = function(documentNode) {
+         var results = [];
+         if (/\bzoopla\.co\.uk\/for-(sale|rent)\/details\/\d+/.test(documentNode.location.href)) {
+            results = _detailPage().evaluate(documentNode);
+         } else if (/\bzoopla\.co\.uk\/for-(sale|rent)\//.test(documentNode.location.href)) {
+            results = _resultsPage().evaluate(documentNode);
+         }
+         return {
+            "site" : "zo",
+            "results" : results
+         };
+      };
+
+   };
+});
+
+PB.unitTests(function () {
+   this.namespace("PB.AddOn.Rule")
+      .class("Zoopla")
+      .fn("capture")
+      .test("dummy test", function () { return undefined; })
+      .end_fn()
+      .end_class()
+      .end_namespace();
+});
Index: chrome/content/pb/add_on/rule/find_a_property.js
===================================================================
--- chrome/content/pb/add_on/rule/find_a_property.js   (revision 0)
+++ chrome/content/pb/add_on/rule/find_a_property.js   (revision 0)
@@ -0,0 +1,171 @@
+PB.namespace("PB.AddOn.Rule", function () {
+
+   /**
+    *   Rules for parsing FindAProperty webpages.
+    */
+   this.FindAProperty = function () {
+      
+      function _resultsPage() {
+         var DETAIL_LINK_XPATH = ".//h2[@class='searchResultHeading']/a";
+         var DESCRIPTION_XPATH = ".//div[@class='searchResultPropDetail']//td";
+         var AGENT_XPATH = ".//div[@class='agent']/a";
+         var AGENT_PARSE_REGEXP = /^\s*(.*?)(?:\s*,\s*([^,]*?))?\s*$/;
+
+           return new PB.Rule.NodesMatchingXPath(
+              "//div[@class='searchResultTable']",
+              new PB.Rule.ConstructProperty(
+              
+               // reference
+               new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                     new PB.Rule.HRef(
+                        new PB.Rule.RegExpCapture(/[&?]pid=(\d+)/, 1,
+                           new PB.Rule.AddPrefix("fi", new PB.Rule.NoConversion())))),
+
+               // url
+               new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                     new PB.Rule.HRef(new PB.Rule.NoConversion())),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(DESCRIPTION_XPATH,
+                  new PB.Rule.NoConversion()),
+
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {                       
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='searchResultPrice']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='searchResultsPropLocation']/h4",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//span[starts-with(@class,'status')]/img",
+                           new PB.Rule.Alt(new PB.Rule.NoConversion()),
+                           "Available"),
+
+                     "Agent":
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 1, new PB.Rule.NoConversion()))),
+
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 2, new PB.Rule.NoConversion()))),
+                     
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='agent_contact_number']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Brief Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(DESCRIPTION_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+                  }
+               )
+            )
+         );
+       };
+
+
+      function _detailPage() {
+         var TITLE_SUBTITLE_XPATH = ".//div[@class='intro_content']/h2/b";
+         var TITLE_SUBTITLE_REGEXP = /^\s*(.*?)\s*:\s*(.*?)\s*$/;
+         
+           return new PB.Rule.NodesMatchingXPath(
+            "/html/body",
+            new PB.Rule.ConstructProperty(
+
+               // reference
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.RegExpCapture(/[&?]pid=(\d+)/, 1,
+                        new PB.Rule.AddPrefix("fi", new PB.Rule.NoConversion())))),
+               
+               // url
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(new PB.Rule.NoConversion()))),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(".//div[@class='intro_content']",
+                  new PB.Rule.NoConversion()),
+            
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='propPrice']/span[@class='highlight']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+      
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(TITLE_SUBTITLE_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(TITLE_SUBTITLE_REGEXP, 2, new PB.Rule.NoConversion()))),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(TITLE_SUBTITLE_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(TITLE_SUBTITLE_REGEXP, 1, new PB.Rule.NoConversion()))),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//div[starts-with(@class,'status')]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+                     
+                     "Agent" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='intro_content']/a[@class='agent_logo']/img",
+                           new PB.Rule.Alt(new PB.Rule.NoConversion())),
+                     
+                     "Detailed Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@id='DisplayPropSummary']/p",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+
+                  }
+               )
+            )
+         );
+         
+      };
+
+
+      /*
+       * Arguments; - documentNode is a handle to the document to capture info
+       *
+       * Return; - site identifies which site/rules were used - results contains
+       * the captured info as a hierarchy of objects (more on this later)
+       *
+       * If the capture method fails, an exception should be thrown.
+       */
+      this.capture = function(documentNode) {
+         var results = [];
+         if (/\.com\/searchresults.aspx\b/.test(documentNode.location.href)) {
+            results = _resultsPage().evaluate(documentNode);
+         } else if (/\.com\/displayprop.aspx\b/.test(documentNode.location.href)) {
+            results = _detailPage().evaluate(documentNode);
+         }
+         return {
+            "site" : "fi",
+            "results" : results
+         };
+      };
+
+   };
+});
+
+PB.unitTests(function () {
+   this.namespace("PB.AddOn.Rule")
+      .class("FindAProperty")
+      .fn("capture")
+      .test("dummy test", function () { return undefined; })
+      .end_fn()
+      .end_class()
+      .end_namespace();
+});
Index: chrome/content/pb/add_on/rule/vebra.js
===================================================================
--- chrome/content/pb/add_on/rule/vebra.js   (revision 0)
+++ chrome/content/pb/add_on/rule/vebra.js   (revision 0)
@@ -0,0 +1,171 @@
+PB.namespace("PB.AddOn.Rule", function () {
+
+   /**
+    *   Rules for parsing Vebra webpages.
+    */
+   this.Vebra = function () {
+
+      function _resultsPage() {
+         var AGENT_XPATH = ".//div[@class='rs-agent-tel']/p/em";
+         var AGENT_PARSE_REGEXP = /^\s*(.*?)(?:\s+-\s+(.*?))?\s*$/;
+
+           return new PB.Rule.NodesMatchingXPath(
+              "//div[@id='rs-container']/div[contains(@class,'rsprop')]",
+              new PB.Rule.ConstructProperty(
+              
+               // reference
+               new PB.Rule.FirstNodeMatchingXPath(".",
+                  new PB.Rule.ID(
+                     new PB.Rule.RegExpCapture(/^p(\d+)$/, 1,
+                        new PB.Rule.AddPrefix("ve", new PB.Rule.NoConversion())))),
+
+               // url
+               new PB.Rule.FirstNodeMatchingXPath(".//li[@class='details']/a",
+                     new PB.Rule.HRef(new PB.Rule.NoConversion())),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(".//div[@class='rsdesc']",
+                  new PB.Rule.NoConversion()),
+
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {                       
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h2/em",
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(/(\xa3?\d{1,3}(?:,\d{3})*)(?=[^\d]|$)/, 1, new PB.Rule.NoConversion()))),
+
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h2//span[@class='address']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h2/child::text()[last()]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//h2//span[@class='PropStatus']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+
+                     "Agent":
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 1, new PB.Rule.NoConversion()))),
+
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 2, new PB.Rule.NoConversion()))),
+                     
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='rs-agent-tel']/p/strong",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Brief Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='rsdesc']/p[@class='desc']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+                  }
+               )
+            )
+         );
+       };
+
+      function _detailPage() {
+         return new PB.Rule.NodesMatchingXPath(
+            "/html/body",
+            new PB.Rule.ConstructProperty(
+               // reference
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(
+                        new PB.Rule.RegExpCapture(/\/property\/(\d+)/, 1,
+                           new PB.Rule.AddPrefix("ve", new PB.Rule.NoConversion()))))),
+               
+               // url
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(new PB.Rule.NoConversion()))),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(".//div[@id='dtintrodesc']",
+                  new PB.Rule.NoConversion()),
+            
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1/em",
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(/(\xa3?\d{1,3}(?:,\d{3})*)(?=[^\d]|$)/, 1, new PB.Rule.NoConversion()))),
+
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1//span[@class='address']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1/child::text()[last()]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//h1//span[@class='PropStatus']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+                     
+                     "Agent" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//li[@id='agname']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+                     
+                     "Agents Address" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//li[@id='agaddress']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//li[@id='agtel']/span[@class='value']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+                     
+                     "Detailed Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='DetailsDescription']/p",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+                  }
+               )
+            )
+         );
+      };
+
+
+      /*
+       * Arguments; - documentNode is a handle to the document to capture info
+       *
+       * Return; - site identifies which site/rules were used - results contains
+       * the captured info as a hierarchy of objects (more on this later)
+       *
+       * If the capture method fails, an exception should be thrown.
+       */
+      this.capture = function(documentNode) {
+         var results = [];
+         if (/\/search\/results\//.test(documentNode.location.href)) {
+            results = _resultsPage().evaluate(documentNode);
+         } else if (/\/property\/\d+(?:\/|$)/.test(documentNode.location.href)) {
+            results = _detailPage().evaluate(documentNode);
+         }
+         return {
+            "site" : "ve",
+            "results" : results
+         };
+      };
+
+   };
+});
+
+PB.unitTests(function () {
+   this.namespace("PB.AddOn.Rule")
+      .class("Vebra")
+      .fn("capture")
+      .test("dummy test", function () { return undefined; })
+      .end_fn()
+      .end_class()
+      .end_namespace();
+});
Index: chrome/content/platform/firefox.js
===================================================================
--- chrome/content/platform/firefox.js   (revision 185)
+++ chrome/content/platform/firefox.js   (working copy)
@@ -18,11 +18,14 @@
 
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/daft.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/espc.js");
+    loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/find_a_property.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/gspc.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/prime_location.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/property_news.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/rightmove.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/sspc.js");
+    loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/vebra.js");
+    loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/zoopla.js");
 
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/tab/changes_only.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/tab/debug.js");
Index: chrome/content/pb_service.js
===================================================================
--- chrome/content/pb_service.js   (revision 185)
+++ chrome/content/pb_service.js   (working copy)
@@ -1447,6 +1447,17 @@
         var primelocationRules = new PB.AddOn.Rule.PrimeLocation();
         registerHostRules("primelocation.com", primelocationRules);
         registerHostRules("www.primelocation.com", primelocationRules);
+        var zooplaRules = new PB.AddOn.Rule.Zoopla();
+        registerHostRules("zoopla.co.uk", zooplaRules);
+        registerHostRules("www.zoopla.co.uk", zooplaRules);
+        var vebraRules = new PB.AddOn.Rule.Vebra();
+        registerHostRules("vebra.com", vebraRules);
+        registerHostRules("www.vebra.com", vebraRules);
+        var findAPropertyRules = new PB.AddOn.Rule.FindAProperty();
+        registerHostRules("findaproperty.com", findAPropertyRules);
+        registerHostRules("www.findaproperty.com", findAPropertyRules);
+        registerHostRules("findanewhome.com", findAPropertyRules);
+        registerHostRules("www.findanewhome.com", findAPropertyRules);
 
       _initialised = true;
 
rlph
 
Posts: 6
Joined: Mon Dec 13, 2010 6:05 pm

Re: Zoopla, FindAProperty and Vebra support

Postby s-p on Sat Dec 18, 2010 9:21 pm

Very interesting rlph and thanks for making the patch file available. Would you have any issue with including these particular changes in the main branch of Property Bee, should there be a demand to include these sites? Obviously we would have to factor in the support costs relative to the demand, but I think it's something we should consider.
Scott
s-p
 
Posts: 125
Joined: Mon Jun 09, 2008 10:20 pm

Re: Zoopla, FindAProperty and Vebra support

Postby Beerhunter on Sun Dec 19, 2010 11:05 am

Thanks rlph.

My view is that there is probably demand for findaproperty, possibly for zoopla, and maybe for verba - tho if rlph is happy for the code to go in the main branch I'm happy for all 3 sites to be supported to see what the demand is.

My only concern is that we may have to do more releases as sites change, and users get more updates than they need (eg they get updates for sites they are not interested in), so I think we should look at how rule changes can get passed to the client without having update the add-on each time early next year.

BH
User avatar
Beerhunter
Site Admin
 
Posts: 1788
Joined: Tue Jan 22, 2008 12:05 am

Re: Zoopla, FindAProperty and Vebra support

Postby rlph on Tue Dec 21, 2010 6:36 pm

I've had to make some fixes:
  • FindAProperty - some new-build listings were being skipped in the results page; detail page was getting different values scraped for a couple of fields
  • Zoopla - status and detailed description not being properly picked up on detail page
  • Vebra - agent name not parsing correctly on detail page
New patch is included below.

I'm quite happy for my code to go into the main branch - wouldn't have posted here if not. Keep only the sites you want. In particular I'm not too bothered if Vebra goes - I threw it in as I had rules ready to convert from an earlier property screen-scraper I wrote.

Side note: I had support partially working for Globrix, but that's heavily AJAX-driven. For foolproof support we'd have to do something like listening for completed XMLHttpRequest calls to "interesting" URL's, then re-scanning the document once the callback finishes. Not sure if beerhunter would want to accept that ugly patch...

Code: Select all
Index: chrome/content/pb/rule.js
===================================================================
--- chrome/content/pb/rule.js   (revision 190)
+++ chrome/content/pb/rule.js   (working copy)
@@ -175,6 +175,15 @@
     }
 
     /**
+     *   Functor to obtain the alternate text from an image, and pass the attribute string through a child functor.
+     */
+    this.Alt = function (/**Functor*/ fn) /**PB.Rule.Alt*/ {
+       this.evaluate = function(/**Node*/ context) /**Variant*/ {
+           return fn.evaluate(context.alt);
+       }
+    }
+
+    /**
      *   Functor to obtain the id attribute from a node, and pass the attribute string through a child functor.
      */
     this.ID = function (/**Functor*/ fn) /**PB.Rule.ID*/ {
@@ -466,6 +475,22 @@
            return samples;
        }
     }
+   
+    /**
+     *  Functor which evaluates a regular expression against a String.  If the regexp matches,
+     *  passes the contents of the specified capture group through a child functor.
+     *  If not a match, return the empty string.
+     */
+    this.RegExpCapture = function( /**RegExp*/ regexp, /** Integer */ captureGroup, /**Functor*/ fn) /**PB.Rule.RegExpCapture*/ {
+        this.evaluate = function(/**String*/ text ) /**Variant*/ {
+            var result = "";
+            var groups = regexp.exec(text);
+            if (groups != null) {
+               result = fn.evaluate(groups[captureGroup]);
+            }
+            return result;
+       }
+    }
 });
 
 PB.unitTests(function () {
@@ -600,5 +625,15 @@
               .test("dummy test", function () { return undefined; })
           .end_fn()
       .end_class()
+        .class("RegExpCapture")
+            .fn("evaluate")
+              .test("dummy test", function () { return undefined; })
+          .end_fn()
+      .end_class()
+        .class("Alt")
+            .fn("evaluate")
+              .test("dummy test", function () { return undefined; })
+          .end_fn()
+      .end_class()
     .end_namespace();
-});
\ No newline at end of file
+});
Index: chrome/content/pb/add_on/rule/zoopla.js
===================================================================
--- chrome/content/pb/add_on/rule/zoopla.js   (revision 0)
+++ chrome/content/pb/add_on/rule/zoopla.js   (revision 0)
@@ -0,0 +1,177 @@
+PB.namespace("PB.AddOn.Rule", function () {
+
+   /**
+    *   Rules for parsing Zoopla webpages.
+    */
+   this.Zoopla = function () {
+
+      function _resultsPage() {
+         var AGENT_REGEXP = /Marketed\s+by\s+(.*?)(?:,\s+([A-Z][A-Z]?\d\d?[A-Z]?))?\.\s+Contact\s+on\s+(\d[\d\s]+\d)(?:\s|$)/;
+         var AGENT_XPATH = ".//div[@class='listing-results-right']//p[@class='top-half'][1]";
+         var DETAIL_LINK_XPATH = ".//h3[@class='bottom-half']/a";
+      
+           return new PB.Rule.NodesMatchingXPath(
+              "//ul[@class='listing-results']//li",
+              new PB.Rule.ConstructProperty(
+              
+               // reference
+               new PB.Rule.FirstNodeMatchingXPath(".",
+                  new PB.Rule.ID(
+                     new PB.Rule.TextAfter("listing_",
+                        new PB.Rule.AddPrefix("zo", new PB.Rule.NoConversion())))),
+
+               // url
+               new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                     new PB.Rule.HRef(new PB.Rule.NoConversion())),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath("./div[@class='listing-results-right']",
+                  new PB.Rule.NoConversion()),
+
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {                       
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='listing-results-right']//strong[contains(@class,'price')]",
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(/(\xa3?\d{1,3}(?:,\d{3})*)(?=[^\d]|$)/, 1, new PB.Rule.NoConversion()))),
+
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='listing-results-right']//span[@class='small']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//div[@class='listing-results-left']//span[starts-with(@class,'listing-status-')]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+
+                     "Agent":
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 1, new PB.Rule.NoConversion()))),
+
+                     // TODO - doesn't work because FirstNodeMatchingXPath doesn't return same result 2x in a row!
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 2, new PB.Rule.NoConversion()))),
+                     
+                     // TODO - doesn't work because FirstNodeMatchingXPath doesn't return same result 3x in a row!
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 3, new PB.Rule.NoConversion()))),
+
+                     "Brief Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='listing-results-right']//p",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))                     
+                  }
+               )
+            )
+         );
+       };
+
+      function _detailPage() {
+         var AGENT_XPATH = ".//div[@class='seperate' and h2='Property description']/p[3]";
+         var AGENT_REGEXP = /\bplease\s+contact\s+(.*?),\s+(.*?)\s+on\s+([\d\s]+?)\s*$/;
+         
+           return new PB.Rule.NodesMatchingXPath(
+              ".//div[@id='mbody']",
+            new PB.Rule.ConstructProperty(
+               // reference
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(
+                        new PB.Rule.RegExpCapture(/\/details\/(\d+)/, 1,
+                           new PB.Rule.AddPrefix("zo", new PB.Rule.NoConversion()))))),
+               
+               // url
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(new PB.Rule.NoConversion()))),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(".//div[@class='seperate' and h2='Property description']/p[1]",
+                  new PB.Rule.NoConversion()),
+            
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//p/strong[@class='buyers']",
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(/(\xa3?\d{1,3}(?:,\d{3})*)(?=[^\d]|$)/, 1, new PB.Rule.NoConversion()))),
+                     
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//p/span[@class='nofontcolor']/strong[@class='nobold']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//div[@id='photos-frame']//span[starts-with(@class,'listing-status-')][1]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+                     
+                     "Agent" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 1, new PB.Rule.NoConversion()))),
+                     
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 2, new PB.Rule.NoConversion()))),
+
+                     "Agents Address" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@id='listings-agent']//br/../child::text()[last()]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.RegExpCapture(AGENT_REGEXP, 3, new PB.Rule.NoConversion()))),
+                     
+                     "Detailed Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='seperate' and h2='Property description']/div[@class='top'][1]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+                  }
+               )
+            )
+         );
+      };
+
+
+      /*
+       * Arguments; - documentNode is a handle to the document to capture info
+       *
+       * Return; - site identifies which site/rules were used - results contains
+       * the captured info as a hierarchy of objects (more on this later)
+       *
+       * If the capture method fails, an exception should be thrown.
+       */
+      this.capture = function(documentNode) {
+         var results = [];
+         if (/\bzoopla\.co\.uk\/for-(sale|rent)\/details\/\d+/.test(documentNode.location.href)) {
+            results = _detailPage().evaluate(documentNode);
+         } else if (/\bzoopla\.co\.uk\/for-(sale|rent)\//.test(documentNode.location.href)) {
+            results = _resultsPage().evaluate(documentNode);
+         }
+         return {
+            "site" : "zo",
+            "results" : results
+         };
+      };
+
+   };
+});
+
+PB.unitTests(function () {
+   this.namespace("PB.AddOn.Rule")
+      .class("Zoopla")
+      .fn("capture")
+      .test("dummy test", function () { return undefined; })
+      .end_fn()
+      .end_class()
+      .end_namespace();
+});
Index: chrome/content/pb/add_on/rule/find_a_property.js
===================================================================
--- chrome/content/pb/add_on/rule/find_a_property.js   (revision 0)
+++ chrome/content/pb/add_on/rule/find_a_property.js   (revision 0)
@@ -0,0 +1,173 @@
+PB.namespace("PB.AddOn.Rule", function () {
+
+   /**
+    *   Rules for parsing FindAProperty webpages.
+    */
+   this.FindAProperty = function () {
+      
+      function _resultsPage() {
+         var DETAIL_LINK_XPATH = ".//h2[@class='searchResultHeading']/a";
+         var DESCRIPTION_XPATH = ".//div[@class='searchResultPropDetail']//td";
+         var AGENT_XPATH = ".//div[@class='agent']/a";
+         var AGENT_PARSE_REGEXP = /^\s*(.*?)(?:\s*,\s*([^,]*?))?\s*$/;
+
+           return new PB.Rule.NodesMatchingXPath(
+              "//div[starts-with(@class,'searchResultTable')]",
+              new PB.Rule.ConstructProperty(
+              
+               // reference
+               new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                     new PB.Rule.HRef(
+                        new PB.Rule.RegExpCapture(/[&?]pid=(\d+)/, 1,
+                           new PB.Rule.AddPrefix("fi", new PB.Rule.NoConversion())))),
+
+               // url
+               new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                     new PB.Rule.HRef(new PB.Rule.NoConversion())),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(DESCRIPTION_XPATH,
+                  new PB.Rule.NoConversion()),
+
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {                       
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='searchResultPrice']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='searchResultsPropLocation']/h4",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(DETAIL_LINK_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.Replace(/Bedroom/, "bedroom", new PB.Rule.NoConversion()))),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//span[starts-with(@class,'status')]/img",
+                           new PB.Rule.Alt(new PB.Rule.NoConversion()),
+                           "Available"),
+
+                     "Agent":
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 1, new PB.Rule.NoConversion()))),
+
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 2, new PB.Rule.NoConversion()))),
+                     
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='agent_contact_number']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Brief Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(DESCRIPTION_XPATH,
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+                  }
+               )
+            )
+         );
+       };
+
+
+      function _detailPage() {
+         var TITLE_SUBTITLE_XPATH = ".//div[@class='intro_content']/h2/b";
+         var TITLE_SUBTITLE_REGEXP = /^\s*(.*?)\s*:\s*(.*?)\s*$/;
+         
+           return new PB.Rule.NodesMatchingXPath(
+            "/html/body",
+            new PB.Rule.ConstructProperty(
+
+               // reference
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.RegExpCapture(/[&?]pid=(\d+)/, 1,
+                        new PB.Rule.AddPrefix("fi", new PB.Rule.NoConversion())))),
+               
+               // url
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(new PB.Rule.NoConversion()))),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(".//div[@class='intro_content']",
+                  new PB.Rule.NoConversion()),
+            
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='propPrice']",
+                           new PB.Rule.TextContent(
+                              new PB.Rule.TextBefore("Find ", new PB.Rule.NoConversion()))),
+      
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(TITLE_SUBTITLE_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(TITLE_SUBTITLE_REGEXP, 2, new PB.Rule.NoConversion()))),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1",
+                           new PB.Rule.TextContent(
+                              new PB.Rule.TextBefore(",", new PB.Rule.NoConversion()))),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//div[starts-with(@class,'status')]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+                     
+                     "Agent" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='intro_content']/a[@class='agent_logo']/img",
+                           new PB.Rule.Alt(new PB.Rule.NoConversion())),
+                     
+                     "Detailed Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@id='DisplayPropSummary']/p",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+
+                  }
+               )
+            )
+         );
+         
+      };
+
+
+      /*
+       * Arguments; - documentNode is a handle to the document to capture info
+       *
+       * Return; - site identifies which site/rules were used - results contains
+       * the captured info as a hierarchy of objects (more on this later)
+       *
+       * If the capture method fails, an exception should be thrown.
+       */
+      this.capture = function(documentNode) {
+         var results = [];
+         if (/\.com\/searchresults.aspx\b/.test(documentNode.location.href)) {
+            results = _resultsPage().evaluate(documentNode);
+         } else if (/\.com\/displayprop.aspx\b/.test(documentNode.location.href)) {
+            results = _detailPage().evaluate(documentNode);
+         }
+         return {
+            "site" : "fi",
+            "results" : results
+         };
+      };
+
+   };
+});
+
+PB.unitTests(function () {
+   this.namespace("PB.AddOn.Rule")
+      .class("FindAProperty")
+      .fn("capture")
+      .test("dummy test", function () { return undefined; })
+      .end_fn()
+      .end_class()
+      .end_namespace();
+});
Index: chrome/content/pb/add_on/rule/vebra.js
===================================================================
--- chrome/content/pb/add_on/rule/vebra.js   (revision 0)
+++ chrome/content/pb/add_on/rule/vebra.js   (revision 0)
@@ -0,0 +1,179 @@
+PB.namespace("PB.AddOn.Rule", function () {
+
+   /**
+    *   Rules for parsing Vebra webpages.
+    */
+   this.Vebra = function () {
+
+      function _resultsPage() {
+         var AGENT_XPATH = ".//div[@class='rs-agent-tel']/p/em";
+         var AGENT_PARSE_REGEXP = /^\s*(.*?)(?:\s+-\s+(.*?))?\s*$/;
+
+           return new PB.Rule.NodesMatchingXPath(
+              "//div[@id='rs-container']/div[contains(@class,'rsprop')]",
+              new PB.Rule.ConstructProperty(
+              
+               // reference
+               new PB.Rule.FirstNodeMatchingXPath(".",
+                  new PB.Rule.ID(
+                     new PB.Rule.RegExpCapture(/^p(\d+)$/, 1,
+                        new PB.Rule.AddPrefix("ve", new PB.Rule.NoConversion())))),
+
+               // url
+               new PB.Rule.FirstNodeMatchingXPath(".//li[@class='details']/a",
+                     new PB.Rule.HRef(new PB.Rule.NoConversion())),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(".//div[@class='rsdesc']",
+                  new PB.Rule.NoConversion()),
+
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {                       
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h2/em",
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(/(\xa3?\d{1,3}(?:,\d{3})*)(?=[^\d]|$)/, 1, new PB.Rule.NoConversion()))),
+
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h2//span[@class='address']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h2/child::text()[last()]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//h2//span[@class='PropStatus']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+
+                     "Agent":
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 1, new PB.Rule.NoConversion()))),
+
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 2, new PB.Rule.NoConversion()))),
+                     
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='rs-agent-tel']/p/strong",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Brief Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='rsdesc']/p[@class='desc']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+                  }
+               )
+            )
+         );
+       };
+
+      function _detailPage() {
+         var AGENT_XPATH = ".//li[@id='agname']";
+         var AGENT_PARSE_REGEXP = /^\s*(.*?)(?:\s+-\s+(.*?))?\s*$/;
+         return new PB.Rule.NodesMatchingXPath(
+            "/html/body",
+            new PB.Rule.ConstructProperty(
+               // reference
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(
+                        new PB.Rule.RegExpCapture(/\/property\/(\d+)/, 1,
+                           new PB.Rule.AddPrefix("ve", new PB.Rule.NoConversion()))))),
+               
+               // url
+               new PB.Rule.ExtractFromDocument(
+                  new PB.Rule.Location(
+                     new PB.Rule.HRef(new PB.Rule.NoConversion()))),
+
+               // append history to node
+               new PB.Rule.FirstNodeMatchingXPath(".//div[@id='dtintrodesc']",
+                  new PB.Rule.NoConversion()),
+            
+               // property details
+               new PB.Rule.ConstructSample(
+                  new PB.Rule.Today(),
+                  {
+                     "Price" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1/em",
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(/(\xa3?\d{1,3}(?:,\d{3})*)(?=[^\d]|$)/, 1, new PB.Rule.NoConversion()))),
+
+                     "Title" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1//span[@class='address']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Subtitle" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//h1/child::text()[last()]",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Status" :
+                        new PB.Rule.AllNodesMatchingXPath(".//h1//span[@class='PropStatus']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()),
+                           "Available"),
+                     
+                     "Agent" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 1, new PB.Rule.NoConversion()))),
+
+                     "Agents Location" :
+                        new PB.Rule.FirstNodeMatchingXPath(AGENT_XPATH,
+                           new PB.Rule.TextContent(
+                              new PB.Rule.RegExpCapture(AGENT_PARSE_REGEXP, 2, new PB.Rule.NoConversion()))),
+                     
+                     "Agents Address" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//li[@id='agaddress']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+
+                     "Agents Telephone" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//li[@id='agtel']/span[@class='value']",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion())),
+                     
+                     "Detailed Description" :
+                        new PB.Rule.FirstNodeMatchingXPath(".//div[@class='DetailsDescription']/p",
+                           new PB.Rule.TextContent(new PB.Rule.NoConversion()))
+                  }
+               )
+            )
+         );
+      };
+
+
+      /*
+       * Arguments; - documentNode is a handle to the document to capture info
+       *
+       * Return; - site identifies which site/rules were used - results contains
+       * the captured info as a hierarchy of objects (more on this later)
+       *
+       * If the capture method fails, an exception should be thrown.
+       */
+      this.capture = function(documentNode) {
+         var results = [];
+         if (/\/search\/results\//.test(documentNode.location.href)) {
+            results = _resultsPage().evaluate(documentNode);
+         } else if (/\/property\/\d+(?:\/|$)/.test(documentNode.location.href)) {
+            results = _detailPage().evaluate(documentNode);
+         }
+         return {
+            "site" : "ve",
+            "results" : results
+         };
+      };
+
+   };
+});
+
+PB.unitTests(function () {
+   this.namespace("PB.AddOn.Rule")
+      .class("Vebra")
+      .fn("capture")
+      .test("dummy test", function () { return undefined; })
+      .end_fn()
+      .end_class()
+      .end_namespace();
+});
Index: chrome/content/platform/firefox.js
===================================================================
--- chrome/content/platform/firefox.js   (revision 190)
+++ chrome/content/platform/firefox.js   (working copy)
@@ -18,11 +18,14 @@
 
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/daft.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/espc.js");
+    loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/find_a_property.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/gspc.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/prime_location.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/property_news.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/rightmove.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/sspc.js");
+    loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/vebra.js");
+    loader.loadSubScript("chrome://property-bee/content/pb/add_on/rule/zoopla.js");
 
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/tab/changes_only.js");
     loader.loadSubScript("chrome://property-bee/content/pb/add_on/tab/debug.js");
Index: chrome/content/pb_service.js
===================================================================
--- chrome/content/pb_service.js   (revision 190)
+++ chrome/content/pb_service.js   (working copy)
@@ -1447,6 +1447,17 @@
         var primelocationRules = new PB.AddOn.Rule.PrimeLocation();
         registerHostRules("primelocation.com", primelocationRules);
         registerHostRules("www.primelocation.com", primelocationRules);
+        var zooplaRules = new PB.AddOn.Rule.Zoopla();
+        registerHostRules("zoopla.co.uk", zooplaRules);
+        registerHostRules("www.zoopla.co.uk", zooplaRules);
+        var vebraRules = new PB.AddOn.Rule.Vebra();
+        registerHostRules("vebra.com", vebraRules);
+        registerHostRules("www.vebra.com", vebraRules);
+        var findAPropertyRules = new PB.AddOn.Rule.FindAProperty();
+        registerHostRules("findaproperty.com", findAPropertyRules);
+        registerHostRules("www.findaproperty.com", findAPropertyRules);
+        registerHostRules("findanewhome.com", findAPropertyRules);
+        registerHostRules("www.findanewhome.com", findAPropertyRules);
 
       _initialised = true;
 
rlph
 
Posts: 6
Joined: Mon Dec 13, 2010 6:05 pm

Re: Zoopla, FindAProperty and Vebra support

Postby s-p on Tue Dec 21, 2010 7:28 pm

Thanks again rlph. :D

Following Beerhunter's comments, I'll look to get the patch moved into the main trunk soon along with identifying the server side changes that are required to support the new sites.
Scott
s-p
 
Posts: 125
Joined: Mon Jun 09, 2008 10:20 pm

Re: Zoopla, FindAProperty and Vebra support

Postby Beerhunter on Wed Dec 22, 2010 1:40 am

I've done the necessary backend config/database changes to support all 3 sites for the dev toolbar, so it should be just a case of getting the rules into the trunk now.
User avatar
Beerhunter
Site Admin
 
Posts: 1788
Joined: Tue Jan 22, 2008 12:05 am

Re: Zoopla, FindAProperty and Vebra support

Postby s-p on Mon Jan 10, 2011 8:25 pm

Beerhunter wrote:I've done the necessary backend config/database changes to support all 3 sites for the dev toolbar, so it should be just a case of getting the rules into the trunk now.


Thanks for that Beerhunter. Real busy at work at the moment, so will probably need at least a week before I can get this code into the branch and tested. :(
Scott
s-p
 
Posts: 125
Joined: Mon Jun 09, 2008 10:20 pm

Re: Zoopla, FindAProperty and Vebra support

Postby Beerhunter on Tue Jan 25, 2011 11:53 pm

I've been immensely busy workwise too :( so no problem.

I'm vaguely planning a new release start of Feb - possibly the 6th Feb or the following week with the new goodies ;) I did over christmas/new year

If one or more of zoopla, findaproperty or vebra is in by then that would be very cool - if it isn't it isn't and not a problem.
User avatar
Beerhunter
Site Admin
 
Posts: 1788
Joined: Tue Jan 22, 2008 12:05 am


Return to Development

Who is online

Users browsing this forum: No registered users and 7 guests

cron