十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Scrapy 链接提取器(Link Extractors)完全指南:从 LxmlLinkExtractor 参数到源码级解析原理

Scrapy 链接提取器(Link Extractors)完全指南:从 LxmlLinkExtractor 参数到源码级解析原理 Scrapy 链接提取器Link Extractors完全指南从 LxmlLinkExtractor 参数到源码级解析原理【免费下载链接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapy本文以 Scrapy 官方文档docs/topics/link-extractors.rst为核心结合仓库源码 scrapy/linkextractors/lxmlhtml.py、scrapy/link.py 与测试用例 tests/test_linkextractors.py系统讲解链接提取器的定位、全部构造参数、extract_links调用链与过滤规则的真实执行顺序帮助你在 Spider 和CrawlSpider中精确控制提取哪些链接、如何规范化、如何去重。1. 什么是链接提取器对象模型与核心职责链接提取器Link Extractor是 Scrapy 中从响应中抽取链接的对象。官方文档给出了两个基本事实链接提取器的__init__方法接收一组配置决定哪些链接可以被提取其extract_links方法从一个Response对象返回一个Link对象列表匹配配置的链接。链接提取器最常见的使用场景是在CrawlSpider中通过一组Rule对象驱动爬取同时它也完全可以在普通 Spider 中直接使用。官方文档给出的标准用法是把提取器实例化为 Spider 类变量在回调中遍历链接from scrapy.linkextractors import LinkExtractor class MySpider(spider.Spider): name my_spider link_extractor LinkExtractor() def parse(self, response): for link in self.link_extractor.extract_links(response): yield Request(link.url, callbackself.parse)这里LinkExtractor并不是一个独立类而是对scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor的便捷别名。从源码 scrapy/linkextractors/init.py 可以看到它只做了一次顶层重导出from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor as LinkExtractor __all__ [IGNORED_EXTENSIONS, LinkExtractor]CrawlSpider内部同样依赖这个默认提取器在 scrapy/spiders/crawl.py 中模块级变量_default_link_extractor LinkExtractor()在Rule未显式传入link_extractor时作为兜底self.link_extractor: LinkExtractor link_extractor or _default_link_extractor2. 链接对象 Linkextract_links 的返回结构extract_links返回的是 scrapy/link.py 中定义的Link对象列表。每个Link有 4 个属性源码使用__slots__ [fragment, nofollow, text, url]属性含义示例url链接所指向的绝对 URL相对链接已被拼合https://example.com/nofollow.htmltext锚标签内的文本内容Dont follow this onefragmentURL 中#之后的部分foonofollow锚标签rel属性是否含nofollowTrue/False文档中的示意 HTML 为a hrefhttps://example.com/nofollow.html#foo relnofollowDont follow this one/aLink实现了__eq__与__hash__比较/哈希均基于四个属性——这正是CrawlSpider能够在多条 Rule 之间去重_requests_to_follow中的seen: set[Link]的基础。3. LxmlLinkExtractor 全参数详解LxmlLinkExtractor的__init__签名源码 scrapy/linkextractors/lxmlhtml.py 第 305-323 行决定了提取行为逐参数说明如下参数类型默认值作用allow正则字符串或正则列表()绝对 URL 必须匹配才提取为空则不过滤deny正则字符串或正则列表()URL 匹配则不提取优先级高于allowallow_domains域名或域名列表()只提取来自这些域名的链接deny_domains域名或域名列表()排除这些域名的链接restrict_xpathsXPath 或 XPath 列表()只在这些 XPath 选中的区域内扫描链接restrict_cssCSS 选择器或列表()同restrict_xpaths在构造时转成 XPathrestrict_text正则或正则列表None链接文本必须匹配才提取tags标签或标签列表(a, area)扫描哪些标签*表示所有标签attrs属性或属性列表(href,)在指定标签中扫描哪些属性*表示所有属性deny_tags标签或列表()排除的标签优先级高于tags可与tags*组合2.17.0 新增deny_attrs属性或列表()排除的属性优先级高于attrs2.17.0 新增canonicalizeboolFalse用w3lib.url.canonicalize_url规范化每个 URLuniqueboolTrue是否对提取结果去重process_value可调用对象None等价lambda x: x转换/改写提取到的属性值返回None丢弃该链接deny_extensions扩展名或列表None即内置IGNORED_EXTENSIONS排除这些扩展名的链接stripboolTrue是否去除属性值首尾空白几个参数的关键行为可以从源码中得到印证3.1 tags / attrs 与 deny_tags / deny_attrs 的匹配逻辑tags/attrs/deny_tags/deny_attrs最终被编译成一个判定函数_name_matchesdef _name_matches(allowed: set[str], denied: set[str], name: str) - bool: if name in denied: return False return * in allowed or name in allowed即denied 集合优先allowed 集合含*通配符则匹配任意名字。deny_tags默认空元组不排除任何标签所以tags*deny_tags(script,)是提取一切标签的链接、但排除 script的标准组合。3.2 deny_extensions 的默认值 IGNORED_EXTENSIONS不传deny_extensions时默认使用 scrapy/linkextractors/init.py 中定义的IGNORED_EXTENSIONS覆盖五类资源压缩包7z、bz2、rar、tar.gz、zip等图片gif、jpg、png、svg、webp等音频mp3、wav、flac系外的常见格式视频mp4、avi、webm等Office 及其他xls/xlsx、doc/docx、pdf、css、js、py、exe等。匹配逻辑在 scrapy/utils/url.py 的url_has_any_extension取 URL path 小写后逐个endswith判断。注意构造器会给每个扩展名补前导点. e所以deny_extensionspdf与deny_extensions.pdf效果一致。若你只想提取文件链接可显式传deny_extensions()关闭该过滤。3.3 canonicalize 与 unique 的语义差异文档特别警示canonicalizeTrue时 URL 经canonicalize_url处理而canonicalize_url是为去重设计的它会改变服务端可见的 URL如去默认端口、去 fragment 差异等因此若用提取器跟随链接保持默认canonicalizeFalse更稳健。去重键的选择也与此联动源码_canonicalize_link_urldef _canonicalize_link_url(link: Link) - str: return canonicalize_url(link.url, keep_fragmentsTrue)canonicalizeFalse默认去重键为保留 fragment的规范 URLsample3.html与sample3.html#foo视为不同链接canonicalizeTrue去重键直接取link.url此时 URL 已被规范化且 fragment 被去掉两者视为相同。测试用例test_extract_filter_allow_with_duplicates_canonicalizetests/test_linkextractors.py 第 77-95 行正验证了这一点uniqueFalse, canonicalizeTrue时重复链接全部保留且带 fragment 的版本 URL 被规范化为无 fragment 形式。3.4 process_value在过滤之前改写值process_value接收从标签/属性中扫描到的原始值可以修改并返回新值或返回None丢弃该链接。文档给出的经典例子是提取 JS 跳转里的目标地址页面 HTMLa hrefjavascript:goToPage(../other/page.html); return falseLink text/a提取器配置import re from scrapy.linkextractors import LinkExtractor le LinkExtractor( process_valuelambda value: ( re.search(rjavascript:goToPage\((.*?), value).group(1) if re.search(rjavascript:goToPage\((.*?), value) else None ) )执行时机很关键文档原文强调process_value在allow/deny等过滤参数之前被调用过滤参数匹配的是它返回的值。若你想基于最终 URL 丢弃链接应改用CrawlSpider的Rule.process_links它只会收到经过过滤后保留下来的链接。3.5 strip为什么默认去空白按 HTML5 标准a、area、img、iframe等元素的href/src属性的首尾空白必须被剥离因此提取器默认stripTrue使用 w3lib 的strip_html5_whitespace处理。测试数据 tests/sample_data/link_extractor/linkextractor.html 中有一条a hrefpage 4.html提取结果为http://example.com/page%204.html——首尾空白被去除、空格被转义而test_strip_false则验证了stripFalse时空白被保留并触发 URL 拼接异常被跳过或按原样处理的路径。4. 提取流程剖析extract_links 到底做了什么LxmlLinkExtractor.extract_links(response)的执行链源码第 405-428 行可以概括为五步解析 base URLget_base_url(response)从页面base href标签提取基准地址圈定扫描区域若配置了restrict_xpaths含restrict_css转换后的 XPath只对response.xpath(x)命中的子文档逐段提取否则对整个response.selector提取逐节点提取底层LxmlParserLinkExtractor._extract_links遍历 lxml 文档元素document.iter(etree.Element)按scan_tag/scan_attr谓词筛选标签与属性对每个属性值strip_html5_whitespace若stripTrue→urljoin(base_url, attr_val)拼成绝对 URL →process_value改写 →safe_url_string按响应编码安全化 → 再urljoin(response_url, url)兜底修正相对链接最后用 XPathstring()取锚内文本并根据rel属性解析nofollow构造Link对象过滤与规范化_process_links先按_link_allowed过滤顺序见第 5 节再按canonicalize决定是否规范化 URL最后委托底层去重全局去重若uniqueTrue对跨区域汇总的结果做最终unique_list键为 3.3 节所述的去重函数。几个值得注意的实现细节无效 scheme 直接丢弃_is_valid_url只接受http、https、file、ftp四种 schemescrapy/linkextractors/init.py 第 123-124 行javascript:链接除非被process_value转成真实 URL否则不会进入结果坏 URL 静默跳过safe_url_string抛ValueError时仅记 debug 日志并continue不会中断整个页面提取XHTML 命名空间透明处理_nons辅助函数会剥掉http://www.w3.org/1999/xhtml前缀因此 XHTML 页面中a也能被tags(a,)命中restrict_css与restrict_xpaths可叠加构造器中 CSS 选择器经parsel.csstranslator.HTMLTranslator转 XPath 后追加到restrict_xpaths元组测试test_restrict_css_and_restrict_xpaths_together验证了两者可同时生效。5. 过滤规则的执行顺序matches() 与 _link_allowed()过滤分两层。第一层是matches(url)方法源码第 381-393 行只处理域名与正则供需要单独判断 URL 的场景复用def matches(self, url: str) - bool: if self.allow_domains and not url_is_from_any_domain(url, self.allow_domains): return False if self.deny_domains and url_is_from_any_domain(url, self.deny_domains): return False allowed ((regex.search(url) for regex in self.allow_res) if self.allow_res else [True]) denied (regex.search(url) for regex in self.deny_res) if self.deny_res else () return any(allowed) and not any(denied)第二层是_link_allowed(link)第 361-379 行完整判定顺序为_is_valid_url——scheme 必须是 http/https/file/ftp否则拒绝allow正则——配置了则必须命中其一deny正则——命中即拒绝deny 优先于 allowallow_domains——配置了则 URL 主机必须属于该域精确匹配或子域见url_is_from_any_domain的endswith(f.{d})逻辑deny_domains——命中即拒绝deny_extensions——path 以任一被禁扩展名结尾即拒绝restrict_text——配置了则链接text必须匹配其一。注意正则用的是re.search而非re.match_matches函数即子串命中也算匹配写allow规则时应自行考虑加边界如^/$。6. 典型配置示例可直接运行以下示例以官方测试数据页为参照该页含相对链接、重复链接、带 fragment 链接、跨域链接与空白 hreffrom scrapy.linkextractors import LinkExtractor # 1) 默认行为提取 a/area 的 href按 IGNORED_EXTENSIONS 排除文件链接 # 相对链接拼成绝对 URL并按 URL 去重 le LinkExtractor() # 2) 只用 URL 正则过滤search 语义子串即可命中 le LinkExtractor(allowr^https?://example\.com/sample\d\.html$) # 3) allow deny 组合deny 优先 le LinkExtractor(allowsample, deny3) # 4) 域名白/黑名单 le LinkExtractor(allow_domainsexample.com) le LinkExtractor(deny_domains[example.com]) # 5) 限定页面区域XPath 与 CSS 二选一或叠加 le LinkExtractor(restrict_xpaths//div[idwrapper]) le LinkExtractor(restrict_css#wrapper a) # 6) 按链接文本过滤正则 le LinkExtractor(restrict_textr^sample \d) # 7) 扩大扫描面所有标签的所有属性仅排除 script le LinkExtractor(tags*, attrs*, deny_tagsscript) # 8) 提取文件链接关闭扩展名过滤只保留 pdf le LinkExtractor(deny_extensions[]) # 注意完全关闭 # 9) 不去重保留重复链接如页面内重复出现的入口 le LinkExtractor(uniqueFalse) # 10) 跟随链接场景下推荐保持 canonicalizeFalse默认 # 仅在纯去重统计场景考虑 canonicalizeTrue le LinkExtractor(canonicalizeTrue, uniqueFalse)7. 与 CrawlSpider 的协作Rule 如何消费提取结果在 scrapy/spiders/crawl.py 的_requests_to_follow中每条 Rule 的执行路径是for rule_index, rule in enumerate(self._rules): links [lnk for lnk in rule.link_extractor.extract_links(response) if lnk not in seen] for link in rule.process_links(links): seen.add(link) request self._build_request(rule_index, link) yield rule.process_request(request, response)要点每条 Rule 的提取结果先与seen集合做Link级去重跨 Rule 不会重复请求同一链接这依赖Link.__hash__生成的Request的meta里写入ruleRule 序号与link_text回调self._callback据此还原是哪条 Rule 命中实现一条 Rule 一个回调的分发Rule(process_links...)是文档推荐的按最终 URL 丢弃链接的位置——它只接收allow/deny等参数保留下来的链接晚于提取器自身的过滤响应若非HtmlResponse如 JSON/XML_requests_to_follow直接返回不产生跟随请求。8. 验证行为用仓库测试与样例数据自检tests/sample_data/link_extractor/linkextractor.html官方测试页含base href、area、重复链接、fragment 链接、空白 href是复现提取行为的最小样本tests/test_linkextractors.py覆盖test_extract_all_links默认行为、test_extract_filter_allow_and_denydeny 优先、test_nofollowrel 解析、test_restrict_css/test_restrict_css_and_restrict_xpaths_together区域限定、test_process_value值改写、test_strip_false空白处理等tests/sample_data/link_extractor/ 下另有linkextractor_latin1.html、linkextractor_noenc.html等编码边界样本验证非 UTF-8 页面的safe_url_string路径。本地复现任一用例的最简方式在安装了本仓库的开发环境python -m pytest tests/test_linkextractors.py -k extract_all_links -v9. 小结与实用建议默认值即最佳起点LxmlLinkExtractor()的默认组合a/areahref 文件扩展名过滤 URL 去重 空白剥离已能覆盖绝大多数跟随场景按需叠加allow/restrict_css即可写正则前先想清楚执行顺序process_value→allow→deny→ 域名 → 扩展名 →restrict_text且正则均为search子串匹配去重键与规范化联动需要区分 fragment 差异时用默认canonicalizeFalse做 URL 指纹统计时才考虑canonicalizeTrue在 CrawlSpider 里控制最终取舍提取器参数管提什么Rule.process_links管留什么两者分层使用比把一切塞进正则更可维护。【免费下载链接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表