Repository: scrapy/scrapy

Issue:
_nons function is not used in scrapy.contrib.linkextractors.lxmlhtml
There is either a bug or some stray code in https://github.com/scrapy/scrapy/blob/master/scrapy/contrib/linkextractors/lxmlhtml.py#L37: `tag = _nons(el.tag)` local variable is not used, and so `_nons` function is also unused. @redapple - what was the intended behavior?



Discussion:
Oops, my bad... That code for _nons is copied from lxml.html.iter_links().
Removing XHTML namespace in tag before comparison looks useful. scan_tag should check this cleaned tag instead of el.tag.
Are we missing a test case for links in XHTML documents?

Yes, it looks like there are no tests for XHTML and namespaces. 
`_nons` could be also useful in `_extract_links` method.


Resolving patch:
diff --git a/scrapy/contrib/linkextractors/lxmlhtml.py b/scrapy/contrib/linkextractors/lxmlhtml.py
--- a/scrapy/contrib/linkextractors/lxmlhtml.py
+++ b/scrapy/contrib/linkextractors/lxmlhtml.py
@@ -36,30 +36,30 @@ def __init__(self, tag="a", attr="href", process=None, unique=False):
 
     def _iter_links(self, document):
         for el in document.iter(etree.Element):
-            tag = _nons(el.tag)
-            if not self.scan_tag(el.tag):
+            if not self.scan_tag(_nons(el.tag)):
                 continue
             attribs = el.attrib
             for attrib in attribs:
+                if not self.scan_attr(attrib):
+                    continue
                 yield (el, attrib, attribs[attrib])
 
     def _extract_links(self, selector, response_url, response_encoding, base_url):
         links = []
         # hacky way to get the underlying lxml parsed document
         for el, attr, attr_val in self._iter_links(selector._root):
-            if self.scan_tag(el.tag) and self.scan_attr(attr):
-                # pseudo _root.make_links_absolute(base_url)
-                attr_val = urljoin(base_url, attr_val)
-                url = self.process_attr(attr_val)
-                if url is None:
-                    continue
-                if isinstance(url, unicode):
-                    url = url.encode(response_encoding)
-                # to fix relative links after process_value
-                url = urljoin(response_url, url)
-                link = Link(url, _collect_string_content(el) or u'',
-                    nofollow=True if el.get('rel') == 'nofollow' else False)
-                links.append(link)
+            # pseudo lxml.html.HtmlElement.make_links_absolute(base_url)
+            attr_val = urljoin(base_url, attr_val)
+            url = self.process_attr(attr_val)
+            if url is None:
+                continue
+            if isinstance(url, unicode):
+                url = url.encode(response_encoding)
+            # to fix relative links after process_value
+            url = urljoin(response_url, url)
+            link = Link(url, _collect_string_content(el) or u'',
+                nofollow=True if el.get('rel') == 'nofollow' else False)
+            links.append(link)
 
         return unique_list(links, key=lambda link: link.url) \
                 if self.unique else links


Test patch:
diff --git a/scrapy/tests/test_contrib_linkextractors.py b/scrapy/tests/test_contrib_linkextractors.py
--- a/scrapy/tests/test_contrib_linkextractors.py
+++ b/scrapy/tests/test_contrib_linkextractors.py
@@ -1,7 +1,7 @@
 import re
 import unittest
 from scrapy.contrib.linkextractors.regex import RegexLinkExtractor
-from scrapy.http import HtmlResponse
+from scrapy.http import HtmlResponse, XmlResponse
 from scrapy.link import Link
 from scrapy.contrib.linkextractors.htmlparser import HtmlParserLinkExtractor
 from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor, BaseSgmlLinkExtractor
@@ -419,6 +419,51 @@ def test_tags_attrs(self):
             Link(url='http://example.com/get?id=2', text=u'Item 2', fragment='', nofollow=False)
         ])
 
+    def test_xhtml(self):
+        xhtml = """
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+    <title>XHTML document title</title>
+</head>
+<body>
+    <div class='links'>
+    <p><a href="/about.html">About us</a></p>
+    </div>
+    <div>
+    <p><a href="/follow.html">Follow this link</a></p>
+    </div>
+    <div>
+    <p><a href="/nofollow.html" rel="nofollow">Dont follow this one</a></p>
+    </div>
+    <div>
+    <p><a href="/nofollow2.html" rel="blah">Choose to follow or not</a></p>
+    </div>
+</body>
+</html>
+        """
+
+        response = HtmlResponse("http://example.com/index.xhtml", body=xhtml)
+
+        lx = self.extractor_cls()
+        self.assertEqual(lx.extract_links(response),
+                         [Link(url='http://example.com/about.html', text=u'About us', fragment='', nofollow=False),
+                          Link(url='http://example.com/follow.html', text=u'Follow this link', fragment='', nofollow=False),
+                          Link(url='http://example.com/nofollow.html', text=u'Dont follow this one', fragment='', nofollow=True),
+                          Link(url='http://example.com/nofollow2.html', text=u'Choose to follow or not', fragment='', nofollow=False)]
+                        )
+
+        response = XmlResponse("http://example.com/index.xhtml", body=xhtml)
+
+        lx = self.extractor_cls()
+        self.assertEqual(lx.extract_links(response),
+                         [Link(url='http://example.com/about.html', text=u'About us', fragment='', nofollow=False),
+                          Link(url='http://example.com/follow.html', text=u'Follow this link', fragment='', nofollow=False),
+                          Link(url='http://example.com/nofollow.html', text=u'Dont follow this one', fragment='', nofollow=True),
+                          Link(url='http://example.com/nofollow2.html', text=u'Choose to follow or not', fragment='', nofollow=False)]
+                        )
 
 
 class LxmlLinkExtractorTestCase(SgmlLinkExtractorTestCase):