tip: 超链接对应的文案通常被称为"锚文本"(anchor text)
在继承CrawlSpider父类的前提下,编写一个 fetch_referer 方法获取当前response.url的父链接和锚文本。
实现逻辑,通过一个例子简要说明:
- 如果设置 start_url="www.baidu.com", Rule(LinkExtractor())匹配链接的规则是任何链接。
- 那么第一个为 response.url="www.baidu.com",假设 www.baidu.com 的response的页面源码存在链接 www.ibaidu.com, www.etbaidu.com;那么此时 refere_extract_link = {"www.baidu.com": ["www.ibaidu.com","www.etbaidu.com"]};进入for循环判断,因为 response.url="www.baidu.com"不匹配list的links ["www.ibaidu.com","www.etbaidu.com"] 的任何一个,所以链接 www.baidu.com 的父链接 referer_url 和 锚文本 referer_url_text 都为None。
- scrapy CrawlSpider会将links的链接继续提交给下载器访问,response.url 重新被赋值为 response.url = "www.ibaidu.com",假设 www.ibaidu.com 的response的页面源码存在链接 "www.ibaidu_child.com","www.ibaidu_second.com";那么此时 refere_extract_link = {"www.baidu.com": ["www.ibaidu.com","www.etbaidu.com"], "www.ibaidu.com": ["www.ibaidu_child.com","www.ibaidu_second.com"]};进入for循环判断,因为 response.url="www.ibaidu.com" 匹配list的links ["www.ibaidu.com","www.etbaidu.com"] 的"www.ibaidu.com",而该list的key是 www.baidu.com,所以链接 www.ibaidu.com 的父链接 referer_url = "www.baidu.com", 锚文本referer_url_text = "百度一下,你就知道"。
- 其它链接重复步骤3,最后把所有链接的父链接和锚文本存储到itme里面。
完整代码:
python
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from developer_docs_scrapy.items import DeveloperDocsScrapyItem
from helpers.operate_redis import operate_redis
# 从redis获取配置
env_config = operate_redis().env_redis_config()
print(env_config)
class DeveloperDocsSpider(CrawlSpider):
name = env_config["spider_name"] # "developer-docs"
allowed_domains = [env_config["spider_allowed_domains"]] # ["org.com"]
start_urls = [env_config["spider_start_urls"]] # ["https://developer.org.com/docs-service/"]
# allow: 满足括号中的're'表达式的url会被提取,如果为空,则全部匹配
# deny: 满足括号中的're'表达式的url不会被提取,优先级高于allow
# deny要放在同一个linkextrator里面,不然,其它link规则会爬取deny的链接
# 为了过滤非文档中心的链接,如:网页存在大量的org.com域名的新闻页面,所以,设置爬取规则:
# 允许爬取URL包含初始URL的链接,如:https://developer.org.com/docs-service/search-result/?keyword=API 包含 https://developer.org.com/docs-service/
rules = [Rule(LinkExtractor(allow=(start_urls), deny=()), process_links="deal_links", callback="parse_item", follow=True),
# Rule(LinkExtractor(deny=r'knowledge'))
]
# 从link_extractor中获取到链接列表时将会调用该函数。该方法主要用来过滤。或者对获取的url进行修改后再进行请求下载
def deal_links(self, links):
print("deal_links")
for link in links:
print(link, link.url, link.text)
# 修改link的url
# link.url = link.url.resplace("","").replace("","")
return links
refere_extract_link = {}
def parse_item(self, response):
item = DeveloperDocsScrapyItem()
item["url"] = response.url
# 从redis读取需要匹配元素xpath路径
# redis读取的是str的列表,用eval转成list类型
title_xpaths = eval(env_config["title_xpaths"])
for xpath in title_xpaths:
# 获取链接访问后的response获取的xpath内容
selected_titles = response.xpath(xpath).extract()
# 如果不为空,则存储改内容且退出当前for循环,不执行else内容
if selected_titles:
item["title"] = selected_titles[0]
break
# 如果for循环所有list后,selected_titles都为空,则执行else
else:
item["title"] = None
referer_url, referer_url_text = self.fetch_referer(response=response)
item["referer_url"] = referer_url
item["referer_url_text"] = referer_url_text
yield item
def fetch_referer(self,response):
"""
获取跳转到 response.url 的父链接 referer_url 和 父链接的文案 referer_url_text
"""
# 获取url访问后的response里面的链接(从源码中摘取部分)
seen = set()
for rule_index, rule in enumerate(self._rules):
links = [
# 提取 response.url的页面的 链接+【链接文案】存储在list
lnk.url + "【" + lnk.text + "】" for lnk in rule.link_extractor.extract_links(response) if lnk not in seen
]
# print(response.url, links)
# 把每一个链接访问后的页面获取到的链接组成 key: list 存储到 refere_extract_link
self.refere_extract_link.update({response.url: links})
# print(self.refere_extract_link)
# 判断每一个response.url是否在refere_extract_link字典的所有links中,如果是,表示是从链接key访问的页面获取的,key是父链接
for key,value in self.refere_extract_link.items():
for v in value:
extract_url = v[0:v.find("【")]
extract_url_text = v[v.find("【")+1: v.find("】")]
if response.url == extract_url:
referer_url = key
referer_url_text = extract_url_text
return referer_url, referer_url_text
else:
# 如果refere_extract_link字典的所有links都没有满足条件return,就会执行else
return None, None
"""
当迭代对象完成所有迭代后且此时的迭代对象为空时,如果存在else子句则执行else子句,没有则继续执行后续代码;
如果迭代对象因为某种原因(如带有break关键字)提前退出迭代,则else子句不会被执行,程序将会直接跳过else子句继续执行后续代码
"""
备注:逻辑想法记录,方便以后可以直接复用