新闻推荐实战(四):scrapy爬虫框架基础
优采云 发布时间: 2022-05-29 21:10新闻推荐实战(四):scrapy爬虫框架基础
本文属于新闻推荐实战-数据层-构建物料池之scrapy爬虫框架基础。对于开源的推荐系统来说数据的不断获取是非常重要的,scrapy是一个非常易用且强大的爬虫框架,有固定的文件结构、类和方法,在实际使用过程中我们只需要按照要求实现相应的类方法,就可以完成我们的爬虫任务。文中给出了新闻推荐系统中新闻爬取的实战代码,希望读者可以快速掌握scrapy的基本使用方法,并能够举一反三。
Scrapy基础及新闻爬取实战python环境的安装
python 环境,使用miniconda搭建,安装miniconda的参考链接:。
在安装完miniconda之后,创建一个新闻推荐的虚拟环境,我这边将其命名为news_rec_py3,这个环境将会在整个新闻推荐项目中使用。
conda create -n news_rec_py3 python==3.8<br />
Scrapy的简介与安装
Scrapy 是一种快速的高级 web crawling 和 web scraping 框架,用于对网站内容进行爬取,并从其页面提取结构化数据。
Ubuntu下安装Scrapy,需要先安装依赖Linux依赖
sudo apt-get install python3 python3-dev python3-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev<br />
在新闻推荐系统虚拟conda环境中安装scrapy
pip install scrapy<br />
scrapy项目结构
默认情况下,所有scrapy项目的项目结构都是相似的,在指定目录对应的命令行中输入如下命令,就会在当前目录创建一个scrapy项目
scrapy startproject myproject<br />
项目的目录结构如下:
myproject/<br /> scrapy.cfg<br /> <br /> myproject/ <br /> __init__.py<br /> items.py<br /> middlewares.py<br /> pipelines.py<br /> settings.py<br /> spiders/<br /> __init__.py<br />
spider
spider是定义一个特定站点(或一组站点)如何被抓取的类,包括如何执行抓取(即跟踪链接)以及如何从页面中提取结构化数据(即抓取项)。换言之,spider是为特定站点(或者在某些情况下,一组站点)定义爬行和解析页面的自定义行为的地方。
爬行器是自己定义的类,Scrapy使用它从一个网站(或一组网站)中抓取信息。它们必须继承 Spider 并定义要做出的初始请求,可选的是如何跟随页面中的链接,以及如何解析下载的页面内容以提取数据。
对于spider来说,抓取周期是这样的:
首先生成对第一个URL进行爬网的初始请求,然后指定一个回调函数,该函数使用从这些请求下载的响应进行调用。要执行的第一个请求是通过调用 start_requests() 方法,该方法(默认情况下)生成 Request 中指定的URL的 start_urls 以及 parse 方法作为请求的回调函数。在回调函数中,解析响应(网页)并返回 item objects , Request 对象,或这些对象的可迭代。这些请求还将包含一个回调(可能相同),然后由Scrapy下载,然后由指定的回调处理它们的响应。在回调函数中,解析页面内容,通常使用 选择器 (但您也可以使用beautifulsoup、lxml或任何您喜欢的机制)并使用解析的数据生成项。最后,从spider返回的项目通常被持久化到数据库(在某些 Item Pipeline )或者使用 Feed 导出 .
下面是官网给出的Demo:
import scrapy<br /><br />class QuotesSpider(scrapy.Spider):<br /> name = "quotes" # 表示一个spider 它在一个项目中必须是唯一的,即不能为不同的spider设置相同的名称。<br /> <br /> # 必须返回请求的可迭代(您可以返回请求列表或编写*敏*感*词*函数),spider将从该请求开始爬行。后续请求将从这些初始请求中相继生成。<br /> def start_requests(self):<br /> urls = [<br /> 'http://quotes.toscrape.com/page/1/',<br /> 'http://quotes.toscrape.com/page/2/',<br /> ]<br /> for url in urls:<br /> yield scrapy.Request(url=url, callback=self.parse) # 注意,这里callback调用了下面定义的parse方法<br /> <br /> # 将被调用以处理为每个请求下载的响应的方法。Response参数是 TextResponse 它保存页面内容,并具有进一步有用的方法来处理它。<br /> def parse(self, response):<br /> # 下面是直接从response中获取内容,为了更方便的爬取内容,后面会介绍使用selenium来模拟人用浏览器,并且使用对应的方法来提取我们想要爬取的内容<br /> page = response.url.split("/")[-2]<br /> filename = f'quotes-{page}.html'<br /> with open(filename, 'wb') as f:<br /> f.write(response.body)<br /> self.log(f'Saved file {filename}')<br />
Xpath
XPath 是一门在 XML 文档中查找信息的语言,XPath 可用来在 XML 文档中对元素和属性进行遍历。在爬虫的时候使用xpath来选择我们想要爬取的内容是非常方便的,这里就提一下xpath中需要掌握的内容,参考资料中的内容更加的详细(建议花一个小时看看)。
要了解xpath, 需要先了解一下HTML(是用来描述网页的一种语言), 这个的细节就不详细展开
划重点:
**xpath路径表达式:**XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。节点是通过沿着路径 (path) 或者步 (steps) 来选取的。
了解如何使用xpath语法选取我们想要的内容,所以需要熟悉xpath的基本语法
scrapy爬取新闻内容实战
在介绍这个项目之前先说一下这个项目的基本逻辑。
环境准备:
首先Ubuntu系统里面需要安装好MongoDB数据库,这个可以参考开源项目MongoDB基础python环境中安装好了scrapy, pymongo包
项目逻辑:
每天定时从新浪新闻网站上爬取新闻数据存储到mongodb数据库中,并且需要监控每天爬取新闻的状态(比如某天爬取的数据特别少可能是哪里出了问题,需要进行排查)每天爬取新闻的时候只爬取当天日期的新闻,主要是为了防止相同的新闻重复爬取(当然这个也不能完全避免爬取重复的新闻,爬取新闻之后需要有一些单独的去重的逻辑)爬虫项目中实现三个核心文件,分别是sina.py(spider),items.py(抽取数据的规范化及字段的定义),pipelines.py(数据写入数据库)
因为新闻爬取项目和新闻推荐系统是放在一起的,为了方便提前学习,下面直接给出项目的目录结构以及重要文件中的代码实现,最终的项目将会和新闻推荐系统一起开源出来
创建一个scrapy项目:
scrapy startproject sinanews<br />
实现item.py逻辑
# Define here the models for your scraped items<br />#<br /># See documentation in:<br /># https://docs.scrapy.org/en/latest/topics/items.html<br /><br />import scrapy<br />from scrapy import Item, Field<br /><br /># 定义新闻数据的字段<br />class SinanewsItem(scrapy.Item):<br /> """数据格式化,数据不同字段的定义<br /> """<br /> title = Field() # 新闻标题<br /> ctime = Field() # 新闻发布时间<br /> url = Field() # 新闻原始url<br /> raw_key_words = Field() # 新闻关键词(爬取的关键词)<br /> content = Field() # 新闻的具体内容<br /> cate = Field() # 新闻类别<br />
实现sina.py (spider)逻辑
这里需要注意的一点,这里在爬取新闻的时候选择的是一个比较简洁的展示网站进行爬取的,相比直接去最新的新浪新闻观光爬取新闻简单很多,简洁的网站大概的链接:#pageid=153&lid=2509&k=&num=50&page=1
# -*- coding: utf-8 -*-<br />import re<br />import json<br />import random<br />import scrapy<br />from scrapy import Request<br />from ..items import SinanewsItem<br />from datetime import datetime<br /><br /><br />class SinaSpider(scrapy.Spider):<br /> # spider的名字<br /> name = 'sina_spider'<br /><br /> def __init__(self, pages=None):<br /> super(SinaSpider).__init__()<br /><br /> self.total_pages = int(pages)<br /> # base_url 对应的是新浪新闻的简洁版页面,方便爬虫,并且不同类别的新闻也很好区分<br /> self.base_url = 'https://feed.mix.sina.com.cn/api/roll/get?pageid=153&lid={}&k=&num=50&page={}&r={}'<br /> # lid和分类映射字典<br /> self.cate_dict = {<br /> "2510": "国内",<br /> "2511": "国际",<br /> "2669": "社会",<br /> "2512": "体育",<br /> "2513": "娱乐",<br /> "2514": "军事",<br /> "2515": "科技",<br /> "2516": "财经",<br /> "2517": "股市",<br /> "2518": "美股"<br /> }<br /><br /> def start_requests(self):<br /> """返回一个Request迭代器<br /> """<br /> # 遍历所有类型的论文<br /> for cate_id in self.cate_dict.keys():<br /> for page in range(1, self.total_pages + 1):<br /> lid = cate_id<br /> # 这里就是一个随机数,具体含义不是很清楚<br /> r = random.random()<br /> # cb_kwargs 是用来往解析函数parse中传递参数的<br /> yield Request(self.base_url.format(lid, page, r), callback=self.parse, cb_kwargs={"cate_id": lid})<br /> <br /> def parse(self, response, cate_id):<br /> """解析网页内容,并提取网页中需要的内容<br /> """<br /> json_result = json.loads(response.text) # 将请求回来的页面解析成json<br /> # 提取json中我们想要的字段<br /> # json使用get方法比直接通过字典的形式获取数据更方便,因为不需要处理异常<br /> data_list = json_result.get('result').get('data')<br /> for data in data_list:<br /> item = SinanewsItem()<br /><br /> item['cate'] = self.cate_dict[cate_id]<br /> item['title'] = data.get('title')<br /> item['url'] = data.get('url')<br /> item['raw_key_words'] = data.get('keywords')<br /><br /> # ctime = datetime.fromtimestamp(int(data.get('ctime')))<br /> # ctime = datetime.strftime(ctime, '%Y-%m-%d %H:%M')<br /><br /> # 保留的是一个时间戳<br /> item['ctime'] = data.get('ctime')<br /><br /> # meta参数传入的是一个字典,在下一层可以将当前层的item进行复制<br /> yield Request(url=item['url'], callback=self.parse_content, meta={'item': item})<br /> <br /> def parse_content(self, response):<br /> """解析文章内容<br /> """<br /> item = response.meta['item']<br /> content = ''.join(response.xpath('//*[@id="artibody" or @id="article"]//p/text()').extract())<br /> content = re.sub(r'\u3000', '', content)<br /> content = re.sub(r'[ \xa0?]+', ' ', content)<br /> content = re.sub(r'\s*\n\s*', '\n', content)<br /> content = re.sub(r'\s*(\s)', r'\1', content)<br /> content = ''.join([x.strip() for x in content])<br /> item['content'] = content<br /> yield item <br />
数据持久化实现,piplines.py
这里需要注意的就是实现SinanewsPipeline类的时候,里面很多方法都是固定的,不是随便写的,不同的方法又不同的功能,这个可以参考scrapy官方文档。 <p># Define your item pipelines here<br />#<br /># Don't forget to add your pipeline to the ITEM_PIPELINES setting<br /># See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html<br /># useful for handling different item types with a single interface<br />import time<br />import datetime<br />import pymongo<br />from pymongo.errors import DuplicateKeyError<br />from sinanews.items import SinanewsItem<br />from itemadapter import ItemAdapter<br /><br /><br /># 新闻item持久化<br />class SinanewsPipeline:<br /> """数据持久化:将数据存放到mongodb中<br /> """<br /> def __init__(self, host, port, db_name, collection_name):<br /> self.host = host<br /> self.port = port<br /> self.db_name = db_name<br /> self.collection_name = collection_name<br /><br /> @classmethod <br /> def from_crawler(cls, crawler):<br /> """自带的方法,这个方法可以重新返回一个新的pipline对象,并且可以调用配置文件中的参数<br /> """<br /> return cls(<br /> host = crawler.settings.get("MONGO_HOST"),<br /> port = crawler.settings.get("MONGO_PORT"),<br /> db_name = crawler.settings.get("DB_NAME"),<br /> # mongodb中数据的集合按照日期存储<br /> collection_name = crawler.settings.get("COLLECTION_NAME") + \<br /> "_" + time.strftime("%Y%m%d", time.localtime())<br /> )<br /><br /> def open_spider(self, spider):<br /> """开始爬虫的操作,主要就是链接数据库及对应的集合<br /> """<br /> self.client = pymongo.MongoClient(self.host, self.port)<br /> self.db = self.client[self.db_name]<br /> self.collection = self.db[self.collection_name]<br /> <br /> def close_spider(self, spider):<br /> """关闭爬虫操作的时候,需要将数据库断开<br /> """<br /> self.client.close()<br /><br /> def process_item(self, item, spider):<br /> """处理每一条数据,注意这里需要将item返回<br /> 注意:判断新闻是否是今天的,每天只保存当天产出的新闻,这样可以增量的添加新的新闻数据源<br /> """<br /> if isinstance(item, SinanewsItem):<br /> try:<br /> # TODO 物料去重逻辑,根据title进行去重,先读取物料池中的所有物料的title然后进行去重<br /><br /> cur_time = int(item['ctime'])<br /> str_today = str(datetime.date.today())<br /> min_time = int(time.mktime(time.strptime(str_today + " 00:00:00", '%Y-%m-%d %H:%M:%S')))<br /> max_time = int(time.mktime(time.strptime(str_today + " 23:59:59", '%Y-%m-%d %H:%M:%S')))<br /> if cur_time > min_time and cur_time