完全指南:從 LxmlLinkExtractor 參數(shù)到源碼級解析原理)
Scrapy 鏈接提取器Link Extractors完全指南從 LxmlLinkExtractor 參數(shù)到源碼級解析原理【免費下載鏈接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.項目地址: https://gitcode.com/GitHub_Trending/sc/scrapy本文以 Scrapy 官方文檔docs/topics/link-extractors.rst為核心結(jié)合倉庫源碼 scrapy/linkextractors/lxmlhtml.py、scrapy/link.py 與測試用例 tests/test_linkextractors.py系統(tǒng)講解鏈接提取器的定位、全部構(gòu)造參數(shù)、extract_links調(diào)用鏈與過濾規(guī)則的真實執(zhí)行順序幫助你在 Spider 和CrawlSpider中精確控制提取哪些鏈接、如何規(guī)范化、如何去重。1. 什么是鏈接提取器對象模型與核心職責(zé)鏈接提取器Link Extractor是 Scrapy 中從響應(yīng)中抽取鏈接的對象。官方文檔給出了兩個基本事實鏈接提取器的__init__方法接收一組配置決定哪些鏈接可以被提取其extract_links方法從一個Response對象返回一個Link對象列表匹配配置的鏈接。鏈接提取器最常見的使用場景是在CrawlSpider中通過一組Rule對象驅(qū)動爬取同時它也完全可以在普通 Spider 中直接使用。官方文檔給出的標(biāo)準(zhǔn)用法是把提取器實例化為 Spider 類變量在回調(diào)中遍歷鏈接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)這里L(fēng)inkExtractor并不是一個獨立類而是對scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor的便捷別名。從源碼 scrapy/linkextractors/init.py 可以看到它只做了一次頂層重導(dǎo)出from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor as LinkExtractor __all__ [IGNORED_EXTENSIONS, LinkExtractor]CrawlSpider內(nèi)部同樣依賴這個默認(rèn)提取器在 scrapy/spiders/crawl.py 中模塊級變量_default_link_extractor LinkExtractor()在Rule未顯式傳入link_extractor時作為兜底self.link_extractor: LinkExtractor link_extractor or _default_link_extractor2. 鏈接對象 Linkextract_links 的返回結(jié)構(gòu)extract_links返回的是 scrapy/link.py 中定義的Link對象列表。每個Link有 4 個屬性源碼使用__slots__ [fragment, nofollow, text, url]屬性含義示例url鏈接所指向的絕對 URL相對鏈接已被拼合https://example.com/nofollow.htmltext錨標(biāo)簽內(nèi)的文本內(nèi)容Dont follow this onefragmentURL 中#之后的部分foonofollow錨標(biāo)簽rel屬性是否含nofollowTrue/False文檔中的示意 HTML 為a hrefhttps://example.com/nofollow.html#foo relnofollowDont follow this one/aLink實現(xiàn)了__eq__與__hash__比較/哈希均基于四個屬性——這正是CrawlSpider能夠在多條 Rule 之間去重_requests_to_follow中的seen: set[Link]的基礎(chǔ)。3. LxmlLinkExtractor 全參數(shù)詳解LxmlLinkExtractor的__init__簽名源碼 scrapy/linkextractors/lxmlhtml.py 第 305-323 行決定了提取行為逐參數(shù)說明如下參數(shù)類型默認(rèn)值作用allow正則字符串或正則列表()絕對 URL 必須匹配才提取為空則不過濾deny正則字符串或正則列表()URL 匹配則不提取優(yōu)先級高于allowallow_domains域名或域名列表()只提取來自這些域名的鏈接deny_domains域名或域名列表()排除這些域名的鏈接restrict_xpathsXPath 或 XPath 列表()只在這些 XPath 選中的區(qū)域內(nèi)掃描鏈接restrict_cssCSS 選擇器或列表()同restrict_xpaths在構(gòu)造時轉(zhuǎn)成 XPathrestrict_text正則或正則列表None鏈接文本必須匹配才提取tags標(biāo)簽或標(biāo)簽列表(a, area)掃描哪些標(biāo)簽*表示所有標(biāo)簽attrs屬性或?qū)傩粤斜?href,)在指定標(biāo)簽中掃描哪些屬性*表示所有屬性deny_tags標(biāo)簽或列表()排除的標(biāo)簽優(yōu)先級高于tags可與tags*組合2.17.0 新增deny_attrs屬性或列表()排除的屬性優(yōu)先級高于attrs2.17.0 新增canonicalizeboolFalse用w3lib.url.canonicalize_url規(guī)范化每個 URLuniqueboolTrue是否對提取結(jié)果去重process_value可調(diào)用對象None等價lambda x: x轉(zhuǎn)換/改寫提取到的屬性值返回None丟棄該鏈接deny_extensions擴展名或列表None即內(nèi)置IGNORED_EXTENSIONS排除這些擴展名的鏈接stripboolTrue是否去除屬性值首尾空白幾個參數(shù)的關(guān)鍵行為可以從源碼中得到印證3.1 tags / attrs 與 deny_tags / deny_attrs 的匹配邏輯tags/attrs/deny_tags/deny_attrs最終被編譯成一個判定函數(shù)_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 集合優(yōu)先allowed 集合含*通配符則匹配任意名字。deny_tags默認(rèn)空元組不排除任何標(biāo)簽所以tags*deny_tags(script,)是提取一切標(biāo)簽的鏈接、但排除 script的標(biāo)準(zhǔn)組合。3.2 deny_extensions 的默認(rèn)值 IGNORED_EXTENSIONS不傳deny_extensions時默認(rèn)使用 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判斷。注意構(gòu)造器會給每個擴展名補前導(dǎo)點. e所以deny_extensionspdf與deny_extensions.pdf效果一致。若你只想提取文件鏈接可顯式傳deny_extensions()關(guān)閉該過濾。3.3 canonicalize 與 unique 的語義差異文檔特別警示canonicalizeTrue時 URL 經(jīng)canonicalize_url處理而canonicalize_url是為去重設(shè)計的它會改變服務(wù)端可見的 URL如去默認(rèn)端口、去 fragment 差異等因此若用提取器跟隨鏈接保持默認(rèn)canonicalizeFalse更穩(wěn)健。去重鍵的選擇也與此聯(lián)動源碼_canonicalize_link_urldef _canonicalize_link_url(link: Link) - str: return canonicalize_url(link.url, keep_fragmentsTrue)canonicalizeFalse默認(rèn)去重鍵為保留 fragment的規(guī)范 URLsample3.html與sample3.html#foo視為不同鏈接canonicalizeTrue去重鍵直接取link.url此時 URL 已被規(guī)范化且 fragment 被去掉兩者視為相同。測試用例test_extract_filter_allow_with_duplicates_canonicalizetests/test_linkextractors.py 第 77-95 行正驗證了這一點uniqueFalse, canonicalizeTrue時重復(fù)鏈接全部保留且?guī)?fragment 的版本 URL 被規(guī)范化為無 fragment 形式。3.4 process_value在過濾之前改寫值process_value接收從標(biāo)簽/屬性中掃描到的原始值可以修改并返回新值或返回None丟棄該鏈接。文檔給出的經(jīng)典例子是提取 JS 跳轉(zhuǎn)里的目標(biāo)地址頁面 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 ) )執(zhí)行時機很關(guān)鍵文檔原文強調(diào)process_value在allow/deny等過濾參數(shù)之前被調(diào)用過濾參數(shù)匹配的是它返回的值。若你想基于最終 URL 丟棄鏈接應(yīng)改用CrawlSpider的Rule.process_links它只會收到經(jīng)過過濾后保留下來的鏈接。3.5 strip為什么默認(rèn)去空白按 HTML5 標(biāo)準(zhǔn)a、area、img、iframe等元素的href/src屬性的首尾空白必須被剝離因此提取器默認(rèn)stripTrue使用 w3lib 的strip_html5_whitespace處理。測試數(shù)據(jù) tests/sample_data/link_extractor/linkextractor.html 中有一條a hrefpage 4.html提取結(jié)果為http://example.com/page%204.html——首尾空白被去除、空格被轉(zhuǎn)義而test_strip_false則驗證了stripFalse時空白被保留并觸發(fā) URL 拼接異常被跳過或按原樣處理的路徑。4. 提取流程剖析extract_links 到底做了什么LxmlLinkExtractor.extract_links(response)的執(zhí)行鏈源碼第 405-428 行可以概括為五步解析 base URLget_base_url(response)從頁面base href標(biāo)簽提取基準(zhǔn)地址圈定掃描區(qū)域若配置了restrict_xpaths含restrict_css轉(zhuǎn)換后的 XPath只對response.xpath(x)命中的子文檔逐段提取否則對整個response.selector提取逐節(jié)點提取底層LxmlParserLinkExtractor._extract_links遍歷 lxml 文檔元素document.iter(etree.Element)按scan_tag/scan_attr謂詞篩選標(biāo)簽與屬性對每個屬性值strip_html5_whitespace若stripTrue→urljoin(base_url, attr_val)拼成絕對 URL →process_value改寫 →safe_url_string按響應(yīng)編碼安全化 → 再urljoin(response_url, url)兜底修正相對鏈接最后用 XPathstring()取錨內(nèi)文本并根據(jù)rel屬性解析nofollow構(gòu)造Link對象過濾與規(guī)范化_process_links先按_link_allowed過濾順序見第 5 節(jié)再按canonicalize決定是否規(guī)范化 URL最后委托底層去重全局去重若uniqueTrue對跨區(qū)域匯總的結(jié)果做最終unique_list鍵為 3.3 節(jié)所述的去重函數(shù)。幾個值得注意的實現(xiàn)細(xì)節(jié)無效 scheme 直接丟棄_is_valid_url只接受http、https、file、ftp四種 schemescrapy/linkextractors/init.py 第 123-124 行javascript:鏈接除非被process_value轉(zhuǎn)成真實 URL否則不會進入結(jié)果壞 URL 靜默跳過safe_url_string拋ValueError時僅記 debug 日志并continue不會中斷整個頁面提取XHTML 命名空間透明處理_nons輔助函數(shù)會剝掉http://www.w3.org/1999/xhtml前綴因此 XHTML 頁面中a也能被tags(a,)命中restrict_css與restrict_xpaths可疊加構(gòu)造器中 CSS 選擇器經(jīng)parsel.csstranslator.HTMLTranslator轉(zhuǎn) XPath 后追加到restrict_xpaths元組測試test_restrict_css_and_restrict_xpaths_together驗證了兩者可同時生效。5. 過濾規(guī)則的執(zhí)行順序matches() 與 _link_allowed()過濾分兩層。第一層是matches(url)方法源碼第 381-393 行只處理域名與正則供需要單獨判斷 URL 的場景復(fù)用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 優(yōu)先于 allowallow_domains——配置了則 URL 主機必須屬于該域精確匹配或子域見url_is_from_any_domain的endswith(f.4cl6bnsl)邏輯deny_domains——命中即拒絕deny_extensions——path 以任一被禁擴展名結(jié)尾即拒絕restrict_text——配置了則鏈接text必須匹配其一。注意正則用的是re.search而非re.match_matches函數(shù)即子串命中也算匹配寫allow規(guī)則時應(yīng)自行考慮加邊界如^/$。6. 典型配置示例可直接運行以下示例以官方測試數(shù)據(jù)頁為參照該頁含相對鏈接、重復(fù)鏈接、帶 fragment 鏈接、跨域鏈接與空白 hreffrom scrapy.linkextractors import LinkExtractor # 1) 默認(rèn)行為提取 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 優(yōu)先 le LinkExtractor(allowsample, deny3) # 4) 域名白/黑名單 le LinkExtractor(allow_domainsexample.com) le LinkExtractor(deny_domains[example.com]) # 5) 限定頁面區(qū)域XPath 與 CSS 二選一或疊加 le LinkExtractor(restrict_xpaths//div[idwrapper]) le LinkExtractor(restrict_css#wrapper a) # 6) 按鏈接文本過濾正則 le LinkExtractor(restrict_textr^sample \d) # 7) 擴大掃描面所有標(biāo)簽的所有屬性僅排除 script le LinkExtractor(tags*, attrs*, deny_tagsscript) # 8) 提取文件鏈接關(guān)閉擴展名過濾只保留 pdf le LinkExtractor(deny_extensions[]) # 注意完全關(guān)閉 # 9) 不去重保留重復(fù)鏈接如頁面內(nèi)重復(fù)出現(xiàn)的入口 le LinkExtractor(uniqueFalse) # 10) 跟隨鏈接場景下推薦保持 canonicalizeFalse默認(rèn) # 僅在純?nèi)ブ亟y(tǒng)計場景考慮 canonicalizeTrue le LinkExtractor(canonicalizeTrue, uniqueFalse)7. 與 CrawlSpider 的協(xié)作Rule 如何消費提取結(jié)果在 scrapy/spiders/crawl.py 的_requests_to_follow中每條 Rule 的執(zhí)行路徑是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 的提取結(jié)果先與seen集合做Link級去重跨 Rule 不會重復(fù)請求同一鏈接這依賴Link.__hash__生成的Request的meta里寫入ruleRule 序號與link_text回調(diào)self._callback據(jù)此還原是哪條 Rule 命中實現(xiàn)一條 Rule 一個回調(diào)的分發(fā)Rule(process_links...)是文檔推薦的按最終 URL 丟棄鏈接的位置——它只接收allow/deny等參數(shù)保留下來的鏈接晚于提取器自身的過濾響應(yīng)若非HtmlResponse如 JSON/XML_requests_to_follow直接返回不產(chǎn)生跟隨請求。8. 驗證行為用倉庫測試與樣例數(shù)據(jù)自檢tests/sample_data/link_extractor/linkextractor.html官方測試頁含base href、area、重復(fù)鏈接、fragment 鏈接、空白 href是復(fù)現(xiàn)提取行為的最小樣本tests/test_linkextractors.py覆蓋test_extract_all_links默認(rèn)行為、test_extract_filter_allow_and_denydeny 優(yōu)先、test_nofollowrel 解析、test_restrict_css/test_restrict_css_and_restrict_xpaths_together區(qū)域限定、test_process_value值改寫、test_strip_false空白處理等tests/sample_data/link_extractor/ 下另有l(wèi)inkextractor_latin1.html、linkextractor_noenc.html等編碼邊界樣本驗證非 UTF-8 頁面的safe_url_string路徑。本地復(fù)現(xiàn)任一用例的最簡方式在安裝了本倉庫的開發(fā)環(huán)境python -m pytest tests/test_linkextractors.py -k extract_all_links -v9. 小結(jié)與實用建議默認(rèn)值即最佳起點LxmlLinkExtractor()的默認(rèn)組合a/areahref 文件擴展名過濾 URL 去重 空白剝離已能覆蓋絕大多數(shù)跟隨場景按需疊加allow/restrict_css即可寫正則前先想清楚執(zhí)行順序process_value→allow→deny→ 域名 → 擴展名 →restrict_text且正則均為search子串匹配去重鍵與規(guī)范化聯(lián)動需要區(qū)分 fragment 差異時用默認(rèn)canonicalizeFalse做 URL 指紋統(tǒng)計時才考慮canonicalizeTrue在 CrawlSpider 里控制最終取舍提取器參數(shù)管提什么Rule.process_links管留什么兩者分層使用比把一切塞進正則更可維護?!久赓M下載鏈接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.項目地址: https://gitcode.com/GitHub_Trending/sc/scrapy創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考