scrapy + selenium爬取网易新闻
前言:这算是一个比较综合的案例,理清了该案例会感到最近学的知识变的很条例、很清晰。需求是爬取五大板块对应的新闻标题以及每个标题对饮的新闻内容。
(一)分析网易页面
对于首页,通过定位发现每个板块都是嵌套在<ul>中,以单独的<li>存在。
点击每个板块,进去后发现页面是这样加载的:
说明每个板块的新闻标题都是动态加载出来的数据(也对,新闻这种实时更新的内容是动态加载出来的也在意料之中),动态加载的数据我们不能直接通过当前url来解析到,那怎么办呢?
回顾五大中间件,调度器给到引擎url后,引擎将它发送到下载器,然后下载器去互联网上获取response,那么目前这种情况response里面是不包含动态加载数据的,因此我们需要对返回的response进行篡改,让它包含了动态加载数据。
问题来了,获取动态加载数据最便捷的方法是之前学过的selenium,可以通过selenium实现获取动态加载数据,那么在哪进行篡改呢,又在哪用selenium呢?------ 既然要篡改返回的response,那自然就在下载中间件的地方实现篡改喽。
至于每条新闻标题对应的新闻内容,这就好说了,可以看到是直接写在源码中的,可以直接解析到,然后获取即可。
(二)代码编写
首先,获取五大板块的url并把它们存储到一个列表中,并依次对列表中的每个url发起请求:
# 解析首页五大板块url
def parse(self, response):
li_list = response.xpath('//*[@id="index2016_wrap"]/div[3]/div[2]/div[2]/div[2]/div/ul/li')
alist = [2,3,5,6]
for index in alist:
model_url = li_list[index].xpath('./a/@href').extract_first()
self.model_urls.append(model_url)
# 依次对各个板块url发起请求
for url in self.model_urls:
yield scrapy.Request(url,callback=self.parse_model)
接下来,我们要编写parse()中的回调函数parse_model(),涉及到response,那么就得去编写下载中间件进行篡改了。
先分析一波:所谓的篡改,无非就是用包含动态加载数据的new_response替代原response,那么new_response如何获得呢?
答:通过selenium再次向五大板块的url发起请求,此时的response中就包含了动态加载数据,然后用scrapy封装好的一个类HtmlResponse进行获取。
那么首先需要实例化一个浏览器对象,通过该对象进行selenium的模拟请求:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
def __init__(self):
self.s = Service(r'D:\Python\spider\selenium\chromedriver.exe')
self.driver = webdriver.Chrome(service=self.s)
接着在middlewares中进行编写,由于process_response继承了spider,也就意味着spider中涉及到的url需要进行一次检验,是五大板块的url在进行selenium的重新请求,否则直接用原response就行。
【注意:middlewares中有两个类,对应两个中间件,一个是爬虫中间件一个是下载中间件,别写错地方了,我们要写在类DownloaderMiddleware中】
# 拦截五大板块对应的响应对象,进行篡改
def process_response(self, request, response, spider):
# 获取在爬虫类中定义的浏览器对象
driver = spider.driver
# 挑选出需要篡改的response对应的request对应的url
if request.url in spider.model_urls:
# 针对五大板块对应的response进行篡改
# 对五大板块的url发起请求
driver.get(request.url)
sleep(2)
# 获取了包含动态加载数据的页面
page_text = driver.page_source
# 实例化一个新的响应对象(包含动态加载的数据)来代替原来旧的响应对象
new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)
return new_response
else:
# 其它请求的response
return response
那经过上述操作,此时我们的response已经实现功能全覆盖,直接在脚本文件中编写回调函数即可:
# 解析每个板块对应的新闻标题(新闻标题是动态加载出来的,获取动态加载的数据用Selenium)
# 编写完下载中间件的process_response后,此时的response已经是new_response
def parse_model(self,response):
div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div[1]/div/ul/li/div/div')
for div in div_list:
news_title = div.xpath('./div/div[1]/h3/a/text() | ./div/h3/a/text()').extract_first()
news_detail_url = div.xpath('./div/div[1]/h3/a/@href | ./div/h3/a/@href').extract_first()
item = WangyiproItem()
item['news_title'] = news_title
# 对新闻详情页的url发起请求
yield scrapy.Request(url=news_detail_url,callback=self.parse_detail,meta={'item':item})
注意用到了请求传参,我们需要去item中定义一下item的内容:
class WangyiproItem(scrapy.Item):
# define the fields for your item here like:
news_title = scrapy.Field()
news_content = scrapy.Field()
# pass
另外注意请求传参的使用:
def parse_detail(self,response):
news_content = response.xpath('//*[@id="content"]/div[2]//text() | //*[@id="content"]/div[2]/div[1]//text()').extract()
news_content = ''.join(news_content)
item = response.meta['item']
item['news_content'] = news_content
yield item
注意一个小细节,在开头实例化了浏览器驱动程序后,记得在最后给它关闭,减少没必要的资源消耗:
# 关闭浏览器驱动程序
def closed(self,spider):
self.driver.quit()
最后,我们把获取到的新闻内容存储到一个txt文本中,方便观察运行结果,采用管道存储方式:
class WangyiproPipeline:
fp = None
# 重写父类方法,打开存储文件
def open_spider(self,spider):
print('开始爬取......')
self.fp = open('./网易新闻.txt','w',encoding='utf-8')
# 该方法专门用来处理item类型的对象,接收爬虫文件提交过来的item对象
# 该方法每接收到一个item就会被调用一次
def process_item(self, item, spider):
news_title = item['news_title']
news_content = item['news_content']
self.fp.write(news_title + ':' + '\n' + news_content + ':' + '\n')
return item
# 关闭文件
def close_spider(self,spider):
print('爬取完成!!!')
self.fp.close()
至此,代码内容已经编写完成,还需要设置一下,不遵守robots协议,UA伪装,设置输出日志的类型,同时打开下载中间件和管道类的配置信息:
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'wangyiPro.pipelines.WangyiproPipeline': 300,
}
(三)报错处理
在运行后,一般可能有两个问题,一是:
二是:
ERROR:device_event_log_impl.cc(211)] USB: usb_device_handle_win.cc:1020 Failed to read
descriptor from node connection: 连到系统上的设备没有发挥作用。 (0x1F) 3ERROR:device_event_log_impl.cc(211)] Bluetooth: bluetooth_adapter_winrt.cc:1204 Gettin
g Radio failed. Chrome will be unable to change the power state by itself.
ERROR:device_event_log_impl.cc(211)] Bluetooth: bluetooth_adapter_winrt.cc:1297 OnPowe
redRadiosEnumerated(), Number of Powered Radios: 0”
问题一可能两种情况,一是数据解析时候xpath路径定位错了,二是获取列表元素时候忘加索引项了;至于问题二其实会正常运行,但强迫症的我还是想去掉这个提示,加几行代码即可:
# 实例化浏览器对象
def __init__(self):
options = webdriver.ChromeOptions()
options.add_experimental_option('excludeSwitches', ['enable-logging'])
self.s = Service(r'D:\Python\spider\selenium\chromedriver.exe')
self.driver = webdriver.Chrome(service=self.s,options=options)
(四)运行效果:
(马上要过年了,祝各位博友春节快乐,学业有成!!!)