diff --git a/scrapy/linkextractors/lxmlhtml.py b/scrapy/linkextractors/lxmlhtml.py
index dd8dcdf7c..23cbd0116 100644
--- a/scrapy/linkextractors/lxmlhtml.py
+++ b/scrapy/linkextractors/lxmlhtml.py
@@ -1,6 +1,7 @@
"""
Link extractor based on lxml.html
"""
+import logging
import operator
from functools import partial
from urllib.parse import urljoin, urlparse
@@ -23,6 +24,8 @@ from scrapy.utils.python import unique as unique_list
from scrapy.utils.response import get_base_url
from scrapy.utils.url import url_has_any_extension, url_is_from_any_domain
+logger = logging.getLogger(__name__)
+
# from lxml/src/lxml/html/__init__.py
XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
@@ -88,7 +91,12 @@ class LxmlParserLinkExtractor:
url = self.process_attr(attr_val)
if url is None:
continue
- url = safe_url_string(url, encoding=response_encoding)
+ try:
+ url = safe_url_string(url, encoding=response_encoding)
+ except ValueError:
+ logger.debug(f"Skipping extraction of link with bad URL {url!r}")
+ continue
+
# to fix relative links after process_value
url = urljoin(response_url, url)
link = Link(
diff --git a/tests/test_linkextractors.py b/tests/test_linkextractors.py
index f663013ba..e1ec19601 100644
--- a/tests/test_linkextractors.py
+++ b/tests/test_linkextractors.py
@@ -2,6 +2,10 @@ import pickle
import re
import unittest
+from packaging.version import Version
+from pytest import mark
+from w3lib import __version__ as w3lib_version
+
from scrapy.http import HtmlResponse, XmlResponse
from scrapy.link import Link
from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor
@@ -815,3 +819,34 @@ class LxmlLinkExtractorTestCase(Base.LinkExtractorTestCase):
def test_restrict_xpaths_with_html_entities(self):
super().test_restrict_xpaths_with_html_entities()
+
+ @mark.skipif(
+ Version(w3lib_version) < Version("2.0.0"),
+ reason=(
+ "Before w3lib 2.0.0, w3lib.url.safe_url_string would not complain "
+ "about an invalid port value."
+ ),
+ )
+ def test_skip_bad_links(self):
+ html = b"""
+ Why would you do this?
+ Good Link
+ Good Link 2
+ """
+ response = HtmlResponse("http://example.org/index.html", body=html)
+ lx = self.extractor_cls()
+ self.assertEqual(
+ [link for link in lx.extract_links(response)],
+ [
+ Link(
+ url="http://example.org/item2.html",
+ text="Good Link",
+ nofollow=False,
+ ),
+ Link(
+ url="http://example.org/item3.html",
+ text="Good Link 2",
+ nofollow=False,
+ ),
+ ],
+ )
diff --git a/tox.ini b/tox.ini
index d96a278ea..06b52f3dc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -101,6 +101,7 @@ install_command =
python -I -m pip install {opts} {packages}
[testenv:pinned]
+basepython = python3.7
deps =
{[pinned]deps}
PyDispatcher==2.0.5