一、Scrapy的简介与安装
python环境的安装:
python 环境,使用miniconda搭建,安装miniconda的参考链接:https://blog.csdn.net/pdcfighting/article/details/111503057。
在安装完miniconda之后,创建一个新闻推荐的虚拟环境,可以将其命名为news_rec_py3,这个环境将会在整个新闻推荐项目中使用。
conda create -n news_rec_py3 python==3.8
1.0 爬虫工具介绍
需要掌握定向网络数据爬取和网页解析的基本能力。
requests库和scrapy爬虫框架对比:
相同点:可用性逗号,文档丰富,入门简单,都没有处理js、提交表单、应对验证码等功能(可扩展)。
requests:页面级爬虫,有功能库,并发性考虑不足,重点在于页面下载、定制灵活,上手简单;scrapy:网站级爬虫,是框架,并发性好,重点在于爬虫结构;一般定制灵活,深度定制困难。
非常小的需求可以用requests库;大一点的需求用scrapy框架,定制程度很高的需求用自搭框架。
(1)requests库
get构造一个向服务器请求资源的Requst对象,返回一个包含服务器资源的Response对象。
# -*- coding: utf-8 -*- """ Created on Tue Dec 21 09:10:31 2021 @author: 86493 """ import requests r=requests.get("http://www.baidu.com") #检测请求的状态码,200则访问成功 r.status_code # 200 #如果没有赋值则显示'ISO-8859-1',显示的不是正常字符 r.encoding='utf-8' r.text type(r) # requests.models.Response r.headers #返回访问页面的头部信息 """ {'Cache-Control': 'private, no-cache, no-store, proxy-revalidate, no-transform', 'Connection': 'keep-alive', 'Content-Encoding': 'gzip', 'Content-Type': 'text/html', 'Date': 'Tue, 21 Dec 2021 01:10:54 GMT', 'Last-Modified': 'Mon, 23 Jan 2017 13:27:56 GMT', 'Pragma': 'no-cache', 'Server': 'bfe/1.0.8.18', 'Set-Cookie': 'BDORZ=27315; max-age=86400; domain=.baidu.com; path=/', 'Transfer-Encoding': 'chunked'} """
(2)request爬取网页的通用框架
def getHTMLText(url): try: r=requests.get(url,timeout=30) r.raise_for_status() #如果状态不是200,引发HTTPEroor异常 r.encoding=r.apparent_encoding return r.text except: return "产生异常" if __name__=="__main__": url="http://www.baidu.com" print(getHTMLText(url))
检测请求的状态码为200(表示访问成功,如果是404则失败),爬取后返回http响应内容的字符串形式,即百度url对应的页面内容:
<!DOCTYPE html> <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
(3)Scrapy框架
Scrapy常用命令:
Scrapy框架的流程图:
入口spiders(负责页面解析),出口是item pipelines(负责对提取的信息后处理);
右下角的三个模块已编写好(用户不需编写),中间键download middleware对这三个模块进行用户可配置的控制(用户可修改、丢弃、新增请求或响应);如果用户不需要对request或response进行修改则不需要改中间键。(有两个中间键:spiders和engine之间,engine和downloader之间)
3条主要的数据流即上图中的三条不同颜色的路径。
用户主要编写的模块:
最核心的模块是spider:解析downloader返回的响应(response);产生爬取项(scraped item);产生额外的爬取请求(request)。
item pipelines:
以流水线方式处理spider产生的爬取项;
由一组操作顺序组成,类似流水线,每个操作是一个item pipeline类型;
可能操作:清理、检验和查重爬取项中的HTML数据、将数据存储到数据库中。
1.1 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
在新闻推荐系统虚拟conda环境中安装scrapy
pip install scrapy
1.2 scrapy项目结构
默认情况下,所有scrapy项目的项目结构都是相似的,在指定目录对应的命令行中输入如下命令,就会在当前目录创建一个scrapy项目
scrapy startproject myproject
项目的目录结构如下:
myproject/ scrapy.cfg myproject/ __init__.py items.py middlewares.py pipelines.py settings.py spiders/ __init__.py
scrapy.cfg: 项目配置文件,注意:这里的部署更多的是指将爬放在特定的服务器上,并且在服务器配置好相关的的操作接口,如果是在本机的爬虫,不需要改变部署的配置文件。
myproject/ : 项目python模块, 代码将从这里导入
myproject/ items.py: 项目items文件
# Define here the models for your scraped items # # See documentation in: # https://docs.scrapy.org/en/latest/topics/items.html import scrapy class MyprojectItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass
- myproject/ pipelines.py: 项目管道文件,将爬取的数据进行持久化存储
# Define your item pipelines here # # Don't forget to add your pipeline to the ITEM_PIPELINES setting # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html # useful for handling different item types with a single interface from itemadapter import ItemAdapter class MyprojectPipeline: def process_item(self, item, spider): return item
myproject/ settings.py: 项目配置文件,可以配置数据库等。
myproject/ spiders/: 放置spider的目录,爬虫的具体逻辑就是在这里实现的(具体逻辑写在spider.py文件中),可以使用命令行创建spider,也可以直接在这个文件夹中创建spider相关的py文件。
myproject/ middlewares:中间件,请求和响应都将经过他,可以配置请求头、代理、cookie、会话维持等。
二、scrapy项目结构详解
2.1 spider的抓取周期
spider:定义一个特定站点(或一组站点)如何被抓取的类,包括如何执行抓取(即跟踪链接)以及如何从页面中提取结构化数据(即抓取项)。
spider:为特定站点(或者在某些情况下,一组站点)定义爬行和解析页面的自定义行为的地方。
爬行器是自己定义的类(ps:在spiders文件夹中),Scrapy使用它从一个网站(或一组网站)中抓取信息。它们必须继承 Spider父类 并定义要做出的初始请求,可选的是如何跟随页面中的链接,以及如何解析下载的页面内容以提取数据。
对于spider来说,抓取周期是这样的:
首先生成对第一个URL进行爬网的初始请求,然后指定一个回调函数(该函数使用从这些请求下载的响应进行调用)。要执行的第一个请求是通过调用 start_requests() 方法,该方法(默认情况下)生成 Request 中指定的URL的 start_urls 以及 parse 方法作为请求的回调函数。
在回调函数中,解析响应(网页)并返回 item objects , Request 对象,或这些对象的可迭代。这些请求还将包含一个回调(可能相同),然后由Scrapy下载,然后由指定的回调处理它们的响应。
在回调函数中,解析页面内容,通常使用 选择器,CSS Selector (但也可以使用beautifulsoup、lxml或其他机制)并使用解析的数据生成项。
最后,从spider返回的项目通常被持久化到数据库(在某些 Item Pipeline )或者使用 Feed 导出 .
下面是官网给出的Demo:
import scrapy class QuotesSpider(scrapy.Spider): # 表示一个spider 它在一个项目中必须是唯一的,即不能为不同的spider设置相同的名称。 name = "quotes" # 必须返回请求的可迭代(您可以返回请求列表或编写生成器函数),spider将从该请求开始爬行。 # 后续请求将从这些初始请求中相继生成。 def start_requests(self): urls = [ 'http://quotes.toscrape.com/page/1/', 'http://quotes.toscrape.com/page/2/', ] for url in urls: yield scrapy.Request(url=url, callback=self.parse) # 注意,这里callback调用了下面定义的parse方法 # 将被调用以处理为每个请求下载的响应的方法。 # Response参数是 TextResponse 它保存页面内容,并具有进一步有用的方法来处理它。 def parse(self, response): # 下面是直接从response中获取内容,为了更方便的爬取内容, # 后面会介绍使用selenium来模拟人用浏览器, # 并且使用对应的方法来提取我们想要爬取的内容 page = response.url.split("/")[-2] filename = f'quotes-{page}.html' with open(filename, 'wb') as f: f.write(response.body) self.log(f'Saved file {filename}')
2.2 Xpath语言
XPath 是一门在 XML 文档中查找信息的语言,XPath 可用来在 XML 文档中对元素和属性进行遍历。在爬虫的时候使用xpath来选择我们想要爬取的内容是非常方便的。具体参考Xpath教程。要了解xpath, 需要先了解一下HTML(是用来描述网页的一种语言)。
(1)xpath路径表达式
XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。节点是通过沿着路径 (path) 或者步 (steps) 来选取的。
(2)熟悉xpath基本语法
了解如何使用xpath语法选取我们想要的内容,需要熟悉xpath的基本语法。
三、scrapy爬取新闻内容实战
在介绍这个项目之前先说一下这个项目的基本逻辑。
3.1 环境准备:
(1)首先Ubuntu系统里面需要安装好MongoDB数据库,这个可以参考开源项目MongoDB基础
(2)python环境中安装好了scrapy, pymongo包
3.2 项目逻辑:
每天定时从新浪新闻网站上爬取新闻数据存储到mongodb数据库中,并且需要监控每天爬取新闻的状态(比如某天爬取的数据特别少可能是哪里出了问题,需要进行排查)
每天爬取新闻的时候只爬取当天日期的新闻,主要是为了防止相同的新闻重复爬取(当然这个也不能完全避免爬取重复的新闻,爬取新闻之后需要有一些单独的去重的逻辑)
爬虫项目中实现三个核心文件,分别是sina.py(spider),items.py(抽取数据的规范化及字段的定义),pipelines.py(数据写入数据库)
新闻爬取项目和新闻推荐系统是放在一起的,项目的目录结构以及重要文件中的代码实现,最终的项目将会和新闻推荐系统一起开源出来。
1. 创建一个scrapy项目:
scrapy startproject sinanews
2. 实现item.py逻辑(敲黑板)
sinanews/ items.py: 项目items文件
# Define here the models for your scraped items # # See documentation in: # https://docs.scrapy.org/en/latest/topics/items.html import scrapy class MyprojectItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass
# Define here the models for your scraped items # # See documentation in: # https://docs.scrapy.org/en/latest/topics/items.html import scrapy from scrapy import Item, Field # 定义新闻数据的字段 class SinanewsItem(scrapy.Item): """ 数据格式化,数据不同字段的定义 """ title = Field() # 新闻标题 ctime = Field() # 新闻发布时间 url = Field() # 新闻原始url raw_key_words = Field() # 新闻关键词(爬取的关键词) content = Field() # 新闻的具体内容 cate = Field() # 新闻类别
3. 实现sina.py (spider)逻辑(敲黑板)
这里需要注意的一点,这里在爬取新闻的时候选择的是一个比较简洁的展示网站进行爬取的,相比直接去最新的新浪新闻观光爬取新闻简单很多,简洁的网站大概的链接:https://news.sina.com.cn/roll/#pageid=153&lid=2509&k=&num=50&page=1
# -*- coding: utf-8 -*- import re import json import random import scrapy from scrapy import Request from ..items import SinanewsItem from datetime import datetime class SinaSpider(scrapy.Spider): # spider的名字 name = 'sina_spider' def __init__(self, pages=None): super(SinaSpider).__init__() self.total_pages = int(pages) # base_url 对应的是新浪新闻的简洁版页面,方便爬虫,并且不同类别的新闻也很好区分 self.base_url = 'https://feed.mix.sina.com.cn/api/roll/get?pageid=153&lid={}&k=&num=50&page={}&r={}' # lid和分类映射字典 self.cate_dict = { "2510": "国内", "2511": "国际", "2669": "社会", "2512": "体育", "2513": "娱乐", "2514": "军事", "2515": "科技", "2516": "财经", "2517": "股市", "2518": "美股" } def start_requests(self): """返回一个Request迭代器 """ # 遍历所有类型的论文 for cate_id in self.cate_dict.keys(): for page in range(1, self.total_pages + 1): lid = cate_id # 这里就是一个随机数,具体含义不是很清楚 r = random.random() # cb_kwargs 是用来往解析函数parse中传递参数的 yield Request(self.base_url.format(lid, page, r), callback=self.parse, cb_kwargs={"cate_id": lid}) def parse(self, response, cate_id): """解析网页内容,并提取网页中需要的内容 """ json_result = json.loads(response.text) # 将请求回来的页面解析成json # 提取json中我们想要的字段 # json使用get方法比直接通过字典的形式获取数据更方便,因为不需要处理异常 data_list = json_result.get('result').get('data') for data in data_list: item = SinanewsItem() item['cate'] = self.cate_dict[cate_id] item['title'] = data.get('title') item['url'] = data.get('url') item['raw_key_words'] = data.get('keywords') # ctime = datetime.fromtimestamp(int(data.get('ctime'))) # ctime = datetime.strftime(ctime, '%Y-%m-%d %H:%M') # 保留的是一个时间戳 item['ctime'] = data.get('ctime') # meta参数传入的是一个字典,在下一层可以将当前层的item进行复制 yield Request(url=item['url'], callback=self.parse_content, meta={'item': item}) def parse_content(self, response): """解析文章内容 """ item = response.meta['item'] content = ''.join(response.xpath('//*[@id="artibody" or @id="article"]//p/text()').extract()) content = re.sub(r'\u3000', '', content) content = re.sub(r'[ \xa0?]+', ' ', content) content = re.sub(r'\s*\n\s*', '\n', content) content = re.sub(r'\s*(\s)', r'\1', content) content = ''.join([x.strip() for x in content]) item['content'] = content yield item
4. 数据持久化实现,piplines.py(敲黑板)
这里需要注意的就是实现SinanewsPipeline
类的时候,里面很多方法都是固定的,不是随便写的,不同的方法又不同的功能,这个可以参考scrapy官方文档。
# Define your item pipelines here # # Don't forget to add your pipeline to the ITEM_PIPELINES setting # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html # useful for handling different item types with a single interface import time import datetime import pymongo from pymongo.errors import DuplicateKeyError from sinanews.items import SinanewsItem from itemadapter import ItemAdapter # 新闻item持久化 class SinanewsPipeline: """ 数据持久化:将数据存放到mongodb中 """ def __init__(self, host, port, db_name, collection_name): self.host = host self.port = port self.db_name = db_name self.collection_name = collection_name @classmethod def from_crawler(cls, crawler): """ 自带的方法,这个方法可以重新返回一个新的pipline对象,并且可以调用配置文件中的参数 """ return cls( host = crawler.settings.get("MONGO_HOST"), port = crawler.settings.get("MONGO_PORT"), db_name = crawler.settings.get("DB_NAME"), # mongodb中数据的集合按照日期存储 collection_name = crawler.settings.get("COLLECTION_NAME") + \ "_" + time.strftime("%Y%m%d", time.localtime()) ) def open_spider(self, spider): """开始爬虫的操作,主要就是链接数据库及对应的集合 """ self.client = pymongo.MongoClient(self.host, self.port) self.db = self.client[self.db_name] self.collection = self.db[self.collection_name] def close_spider(self, spider): """ 关闭爬虫操作的时候,需要将数据库断开 """ self.client.close() def process_item(self, item, spider): """ 处理每一条数据,注意这里需要将item返回 注意:判断新闻是否是今天的,每天只保存当天产出的新闻,这样可以增量的添加新的新闻数据源 """ if isinstance(item, SinanewsItem): try: # TODO 物料去重逻辑,根据title进行去重,先读取物料池中的所有物料的title然后进行去重 cur_time = int(item['ctime']) str_today = str(datetime.date.today()) min_time = int(time.mktime(time.strptime(str_today + " 00:00:00", '%Y-%m-%d %H:%M:%S'))) max_time = int(time.mktime(time.strptime(str_today + " 23:59:59", '%Y-%m-%d %H:%M:%S'))) if cur_time > min_time and cur_time <= max_time: self.collection.insert(dict(item)) except DuplicateKeyError: """ 说明有重复 """ pass return item
5. 配置文件,settings.py
from typing import Collection import sys # sys.path.append("/home/recsys/news_rec_server") sys.path.append("D:/桌面文件/fun-rec-master/codes/news_recsys/news_rec_server") from conf.dao_config import mongo_hostname, mongo_port, sina_db_name, sina_collection_name_prefix BOT_NAME = 'sinanews' SPIDER_MODULES = ['sinanews.spiders'] NEWSPIDER_MODULE = 'sinanews.spiders' ROBOTSTXT_OBEY = True ITEM_PIPELINES = { 'sinanews.pipelines.SinanewsPipeline': 300, } MONGO_HOST = mongo_hostname MONGO_PORT = mongo_port SINA_DB_NAME = sina_db_name COLLECTION_NAME_PRFIX = sina_collection_name_prefix
6. 监控脚本,monitor_news.py
# -*- coding: utf-8 -*- import sys, time import pymongo import scrapy from sinanews.settings import MONGO_HOST, MONGO_PORT, DB_NAME, COLLECTION_NAME if __name__ == "__main__": news_num = int(sys.argv[1]) time_str = time.strftime("%Y%m%d", time.localtime()) # 实际的collection_name collection_name = COLLECTION_NAME + "_" + time_str # 链接数据库 client = pymongo.MongoClient(MONGO_HOST, MONGO_PORT) db = client[DB_NAME] collection = db[collection_name] # 查找当前集合中所有文档的数量 cur_news_num = collection.count() print(cur_news_num) if (cur_news_num < news_num): print("the news nums of {}_{} collection is less then {}".\ format(COLLECTION_NAME, time_str, news_num))
7. 运行脚本,run_scrapy_sina.sh
# -*- coding: utf-8 -*- """ 新闻爬取及监控脚本 """ # 设置python环境 python="/home/recsys/miniconda3/envs/news_rec_py3/bin/python" # 新浪新闻网站爬取的页面数量 page="1" min_news_num="1000" # 每天爬取的新闻数量少于500认为是异常 # 爬取数据 scrapy crawl sina_spider -a pages=${page} if [ $? -eq 0 ]; then echo "scrapy crawl sina_spider --pages ${page} success." else echo "scrapy crawl sina_spider --pages ${page} fail." fi # 检查今天爬取的数据是否少于min_news_num篇文章,这里也可以配置邮件报警 python monitor_news.py ${min_news_num} if [ $? -eq 0 ]; then echo "run python monitor_news.py success." else echo "run python monitor_news.py fail." fi
8. 运行项目命令
sh run_scrapy_sina.sh
如果是在命令行打命令,最终查看数据库中的数据:
我在datagrip中的命令:
show dbs; use SinaNews; show collections;
我们将数据存入mongodb数据库内,显示当前的集合collections:
显示2条爬取的新闻: