pip install scrapy
安装scrapyscrapy startproject ArticleSpider
创建scrapy项目scrapy genspider 爬虫名称 网站域名
在spiders文件夹中创建爬虫文件pycharm没有scrapy模板,所以没法调试,我们可以通过创建main.py
文件调用命令行,完成调试。
在项目外侧创建main.py,代码如下
#execute函数可以执行scrapy脚本
from scrapy.cmdline import execute
#需要sys获得工程目录
import sys
#利用os获得工程目录
import os
#需要设置工程目录,设置完工程目录之后调用execute函数才会生效
#print(os.path.dirname(os.path.abspath(__file__)))则会获取到当前main文件的路径的上一级包名
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
#execute相当于执行命令
execute(["scrapy","crawl","jobbole"])
【注意】:设置setting中的ROBOTSTXT_OBEY=False
,如果不设置scrapy会默认读取每一个网站上的robots协议,会把不符合协议的url给过滤掉,很快就会执行结束
执行debug main则会以debug模式运行爬虫
xpath简介
xpath节点关系
xpath语法
表达式 | 说明 |
---|---|
article | 选取所有article元素的所有子节点 |
/article | 选取根元素article |
article/a | 选取所有属于article的子元素的a元素 |
//div | 选取所有div子元素(不论出现在文档的任何地方) |
article//div | 选取所有属于article元素的后代的div元素,不管它出现在article之下的任何位置 |
//@class | 选取所有名为class的属性 |
xpath语法-谓语
表达式 | 说明 |
---|---|
/article/div[1] | 选取属于article子元素的第一个div元素 |
/article/div[last()] | 选取属于article子元素的最后一个div元素 |
/article/div[last()-1] | 选取属于article子元素的倒数第二个div元素 |
//div[@lang] | 选取所有拥有lang属性的div元素 |
//div[@lang=‘eng’] | 选取所有lang属性为eng的div元素 |
xpath其他语法
表达式 | 说明 |
---|---|
/div/* | 选取属于div元素的所有子节点 |
//* | 选取所有元素 |
//div[@*] | 选取所有带属性的div元素 |
//div/a | //div/p | 选取所有div元素的a和p元素 |
//span | //ul | 选取文档中的span和ul元素 |
article/div/p | //span | 选取所有属于article元素和div元素的p元素 以及文档中所有span元素 |
@class="class类"
取值,则只会匹配class只有指定的元素;如果想指定包含指定class的元素则需要使用函数contains(@class,"class类")
xpath使用
[注意]:页面上的查看源码跟检查控制台的element不一定一样,源码是源代码的html文件,检查控制台的element会有js动态生成的dom
jobbole.py
# -*- coding: utf-8 -*-
import scrapy
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/114610/'] #放入想爬取的所有url
def parse(self, response):
#/html/body/div[3]/div[3]/div[1]/div[1]
#//*[@id="post-114610"]/div[1]/h1
#scrapy返回的是一个selector而不是node,是为了方便进一步获取selector下面的selector
re_selector = response.xpath('//*[@id="post-114610"]/div[1]/h1')
re2_selector = response.xpath('//*[@id="post-114610"]/div[1]/h1/text()') #利用text()函数获取元素中的值
pass
如果每一次调试都运行python脚本发送http请求获取内容效率低,可以通过scrapy shell url路径
的方式通过shell进行调试
#以下是在命令行输入
scrapy shell http://blog.jobbole.com/114610/
re_selector = response.xpath('//*[@id="post-114610"]/div[1]/h1')
re_selector.extract()#提取selector的内容['微软变了!招程序员的流程完全改了
']
re_selector.extract()[0]#获得列表中的第一个元素 '微软变了!招程序员的流程完全改了
'
#如果提取的字符串左右有回车符换行符等等,则需要使用strip()将其去掉
#re_selector.extract()[0].strip()
使用xpath爬取伯乐在线网站内容[jobbole.py]
# -*- coding: utf-8 -*-
import scrapy
import re
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/114610/'] #放入想爬取的所有url
def parse(self, response):
# 获取文章title
title = response.xpath('//*[@id="post-114610"]/div[1]/h1/text()')
# 获取文章创建时间
create_date = response.xpath('//*[@id="post-114610"]/div[2]/p/text()').extract()[0].strip().replace("·","").strip()
# 通过contains函数选择class中包含vote-post-up的元素,获得点赞数
praise = response.xpath('//*[contains(@class,"vote-post-up")]/h10/text()').extract()[0]
# 收藏数,获取的是字符串,需要使用正则表达式取出
collect = response.xpath('//*[contains(@class,"bookmark-btn")]/text()').extract()[0]
match_re = re.match(".*?(\d+).*",collect)
if match_re:
collect = match_re.group(1)
else:
collect = 0
# 评论数
comment = response.xpath('//a[@href="#article-comment"]/span/text()').extract()[0]
match_re = re.match(".*?(\d+).*", comment)
if match_re:
comment = match_re.group(1)
else:
comment = 0
# 文章内容
content = response.xpath('//div[@class="entry"]').extract()[0]
# 文章标签
tag = response.xpath('//*[@id="post-114610"]/div[2]/p/a/text()').extract()
# 利用列表生成式过滤携带评论的文章标签
tag = [element for element in tag if not element.strip().endswith("评论")]
# 利用join方式将列表拼成一个字符串
tags = ",".join(tag)
pass
表达式 | 说明 |
---|---|
* | 选择所有节点 |
#container | 选择id为container的节点 |
.container | 选取所有class包含container的节点 |
li a | 选取所有li下的所有a节点 |
ul + p | 选择ul后面的第一个p元素 |
div#container>ul | 选取id为container的第一个ul子元素 |
ul ~ p | 选取与ul相邻的所有p元素 |
a[title] | 选取所有有title属性的a元素 |
a[href=“http://jobbole.com”] | 选取所有href属性为jobbole.com值的a元素 |
a[href*=“jobble”] | 选取所有href属性包含jobbole的a元素 |
a[href^=“http”] | 选取所有href属性以http开头的a元素 |
a[href$=".jpg"] | 选取所有href属性以jpg结尾的a元素 |
input[type=radio]:checked | 选择选中的radio元素 |
div:not(#container) | 选取所有id非container的div属性 |
li:nth-child(3) | 选取第三个li元素 |
tr:nth-child(2n) | 第偶数个tr |
::text | 利用伪类选择器获得选中的元素的内容 |
使用response.css("选择器内容")
通过css选择器获得内容
利用css选择器提取伯乐在线平台内容[jobbole.py]
# -*- coding: utf-8 -*-
import scrapy
import re
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/114610/'] #放入想爬取的所有url
def parse(self, response):
######################
## 利用css选择器获取内容
######################
title = response.css(".entry-header h1::text").extract()[0]
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·", "").strip()
# 点赞数
praise_nums = response.css(".vote-post-up h10::text").extract()[0]
# 收藏数
collect = response.css('span.bookmark-btn::text').extract()[0]
match_re = re.match(".*?(\d+).*", collect)
if match_re:
collect = match_re.group(1)
# 评论数
comment = response.css('a[href="#article-comment"] span::text').extract()[0]
match_re = re.match(".*?(\d+).*", comment)
if match_re:
comment = match_re.group(1)
# 获得文章内容
content = response.css("div.entry").extract()[0]
# 获得标签
tags = response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag = [element for element in tags if not element.strip().endswith("评论")]
tags = ",".join(tag)
pass
如果想使用extract将selector变成list后取得第一个除了使用extract()[0]
之后还可以使用extract_first()
,如果获取的是空的话,使用extract()[0]
会报错,而extract_first()
不会报错,还可以在参数设定默认值,如果没有值则返回对应的内容
实例代码[jobbole.py]
# -*- coding: utf-8 -*-
import scrapy
import re
from scrapy.http import Request
from urllib import parse
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/'] #放入想爬取的所有url
def parse(self, response):
"""
1.获取文章列表页中的文章url并交给scrapy下载后并进行解析
2.获取下一页url并交给scrapy进行下载,下载完成后交给parse
:param response:
:return:
"""
#解析列表页的所有文章url并交给scrapy下载后进行解析
#利用::attr()伪类选择器获得对应元素的属性值
post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract()
for post_url in post_urls:
#利用Request方法提交请求获取对应url内容
#url表示访问的url(parse.urljoin是借助urllib将当前url与请求url进行拼接,从而获取到真实的url),callback表示回调的函数
yield Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)
#提交下一页并交给scrapy进行下载
#两个类之间有空格则表示子元素,两个类中间没空格则表示同时满足两个类的元素
next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
if next_url:
yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
#提取文章的具体字段
def parse_detail(self,response):
######################
## 利用css选择器获取内容
######################
title = response.css(".entry-header h1::text").extract()[0]
print(title)
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·", "").strip()
# 点赞数
praise_nums = response.css(".vote-post-up h10::text").extract()[0]
# 收藏数
collect = response.css('span.bookmark-btn::text').extract()[0]
match_re = re.match(".*?(\d+).*", collect)
if match_re:
collect = match_re.group(1)
else:
collect = 0
# 评论数
comment = response.css('a[href="#article-comment"] span::text').extract()[0]
match_re = re.match(".*?(\d+).*", comment)
if match_re:
comment = match_re.group(1)
else:
comment = 0
# 获得文章内容
content = response.css("div.entry").extract()[0]
# 获得标签
tags = response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag = [element for element in tags if not element.strip().endswith("评论")]
tags = ",".join(tag)
开发流程
::attr()
伪类选择器获取对应属性的值数据爬取的目的在于将非结构的数据源提取成结构型的数据
如果想使用Request函数向callback函数中传递内容,则需要使用meta参数
需求说明:在列表页获取图片url连接传递到parse_detail函数进行解析
实现思路
response.meta.get("front_image_url","")
实例代码
# -*- coding: utf-8 -*-
import scrapy
import re
from scrapy.http import Request
from urllib import parse
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/'] #放入想爬取的所有url
def parse(self, response):
"""
1.获取文章列表页中的文章url并交给scrapy下载后并进行解析
2.获取下一页url并交给scrapy进行下载,下载完成后交给parse
:param response:
:return:
"""
#解析列表页的所有文章url并交给scrapy下载后进行解析
#利用::attr()伪类选择器获得对应元素的属性值
post_nodes = response.css("#archive .floated-thumb .post-thumb a")
for post_node in post_nodes:
image_url = post_node.css("img::attr(src)").extract_first("") #获得图片的src
post_url = post_node.css("::attr(href)").extract_first("") #获得连接的href
#利用Request方法提交请求获取对应url内容
#url表示访问的url(parse.urljoin是借助urllib将当前url与请求url进行拼接,从而获取到真实的url),callback表示回调的函数
yield Request(url=parse.urljoin(response.url,post_url),meta={
"front_image_url":image_url},callback=self.parse_detail)
#提交下一页并交给scrapy进行下载
#两个类之间有空格则表示子元素,两个类中间没空格则表示同时满足两个类的元素
next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
if next_url:
yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
#提取文章的具体字段
def parse_detail(self,response):
######################
## 利用css选择器获取内容
######################
# 获得meta中的front_image_url,文章封面图
front_image_url = response.meta.get("front_image_url","")
# 获得title
title = response.css(".entry-header h1::text").extract()[0]
print(title)
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·", "").strip()
# 点赞数
praise_nums = response.css(".vote-post-up h10::text").extract()[0]
# 收藏数
collect = response.css('span.bookmark-btn::text').extract()[0]
match_re = re.match(".*?(\d+).*", collect)
if match_re:
collect = match_re.group(1)
else:
collect = 0
# 评论数
comment = response.css('a[href="#article-comment"] span::text').extract()[0]
match_re = re.match(".*?(\d+).*", comment)
if match_re:
comment = match_re.group(1)
else:
comment = 0
# 获得文章内容
content = response.css("div.entry").extract()[0]
# 获得标签
tags = response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag = [element for element in tags if not element.strip().endswith("评论")]
tags = ",".join(tag)
相当于定义model
在items.py文件中定义item
class JobBoleArticleItem(scrapy.Item):
title = scrapy.Field() #scrapy可以使用Field表示任意类型
create_date = scrapy.Field()
url = scrapy.Field()
url_object_id = scrapy.Field() #对url进行编码
front_image_url = scrapy.Field()
front_image_path = scrapy.Field()
praise_nums = scrapy.Field()
comment_nums = scrapy.Field()
fav_nums = scrapy.Field()
tags = scrapy.Field()
content = scrapy.Field()
在jobbole.py文件中定义爬虫返回内容放到item中
# -*- coding: utf-8 -*-
import scrapy
import re
from scrapy.http import Request
from urllib import parse
#引入item
from ArticleSpider.items import JobBoleArticleItem
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/'] #放入想爬取的所有url
def parse(self, response):
"""
1.获取文章列表页中的文章url并交给scrapy下载后并进行解析
2.获取下一页url并交给scrapy进行下载,下载完成后交给parse
:param response:
:return:
"""
#解析列表页的所有文章url并交给scrapy下载后进行解析
#利用::attr()伪类选择器获得对应元素的属性值
post_nodes = response.css("#archive .floated-thumb .post-thumb a")
for post_node in post_nodes:
image_url = post_node.css("img::attr(src)").extract_first("") #获得图片的src
post_url = post_node.css("::attr(href)").extract_first("") #获得连接的href
#利用Request方法提交请求获取对应url内容
#url表示访问的url(parse.urljoin是借助urllib将当前url与请求url进行拼接,从而获取到真实的url),callback表示回调的函数
yield Request(url=parse.urljoin(response.url,post_url),meta={
"front_image_url":image_url},callback=self.parse_detail)
#提交下一页并交给scrapy进行下载
#两个类之间有空格则表示子元素,两个类中间没空格则表示同时满足两个类的元素
next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
if next_url:
yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
#提取文章的具体字段
def parse_detail(self,response):
######################
## 利用css选择器获取内容
######################
# 实例化item
article_item = JobBoleArticleItem()
# 获得meta中的front_image_url,文章封面图
front_image_url = response.meta.get("front_image_url","")
# 获得title
title = response.css(".entry-header h1::text").extract()[0]
print(title)
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·", "").strip()
# 点赞数
praise_nums = response.css(".vote-post-up h10::text").extract()[0]
# 收藏数
collect = response.css('span.bookmark-btn::text').extract()[0]
match_re = re.match(".*?(\d+).*", collect)
if match_re:
collect = match_re.group(1)
else:
collect = 0
# 评论数
comment = response.css('a[href="#article-comment"] span::text').extract()[0]
match_re = re.match(".*?(\d+).*", comment)
if match_re:
comment = match_re.group(1)
else:
comment = 0
# 获得文章内容
content = response.css("div.entry").extract()[0]
# 获得标签
tags = response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag = [element for element in tags if not element.strip().endswith("评论")]
tags = ",".join(tag)
#填充item
article_item['title'] = title
article_item['url'] = response.url
article_item['create_date'] = create_date
article_item['front_image_url'] = [front_image_url]
article_item['praise_nums'] = praise_nums
article_item['comment_nums'] = comment
article_item['fav_nums'] = collect
article_item['tags'] = tags
article_item['content'] = content
yield article_item #将item传递到pipeline中
pipeline主要用于做数据处理
item赋值之后就会传递到pipeline.py中,但需要将settings中的
ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, }
取消注释
在settings中设置下载图片的pipeline(scrapy已经给提供了,在site-package->scrapy->pipelines->images.py->ImagesPipeline),添加到配置的ITEM_PIPELINES中(为item流经的管道,后面的数字表示处理顺序,数字越小就越早进入pipeline)
#先进入ImagePipeline再进入ArticleSpiderPipeline ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, 'scrapy.pipelines.images.ImagesPipeline': 1, }
ImagePipeline主要用于存储图片,需要在setting中配置下载传递item中的哪个字段,想保存图片还需要安装PIL库
pip install pillow
import os #先进入ImagePipeline再进入ArticleSpiderPipeline ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, 'scrapy.pipelines.images.ImagesPipeline': 1, } #配置下载的是哪些字段,将指定的字段视作一个数组,故item中图片url应为数组格式 IMAGES_URLS_FIELD = "front_image_url" #设置图片保存路径[先创建一个images文件夹] project_dir = os.path.abspath(os.path.dirname(__file__)) #获得当前文件夹的绝对路径 IMAGES_STORE = os.path.join(project_dir,"images")
此时执行main.py会发现在images文件夹下会存储对应的网络图片
自定义存储图片pipeline
配置setting.py,设置过滤掉的图片(表示必须大于100*100)
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
'scrapy.pipelines.images.ImagesPipeline': 1,
}
IMAGES_URLS_FIELD = "front_image_url"
project_dir = os.path.abspath(os.path.dirname(__file__))
IMAGES_STORE = os.path.join(project_dir,"images")
# 表示下载的图片必须要是大于100*100的
#IMAGES_MIN_HEIGHT = 100 #在ImagesPipeline中使用到了此变量
#IMAGES_MIN_WIDTH = 100 #在ImagesPipeline中使用到了此变量
ImagesPipeline中的item_completed函数中可以获取到图片的实际下载地址(需要重载)
pipelines.py实例代码
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy.pipelines.images import ImagesPipeline
class ArticlespiderPipeline(object):
def process_item(self, item, spider):
return item
# 继承ImagePipeline
class ArticleImagePipeline(ImagesPipeline):
def item_completed(self, results, item, info):
# result中包含一个tuple,一个是状态值,一个是返回对象
for ok,value in results:
image_file_path = value["path"]
item["front_image_path"] = image_file_path
return item
自定义存储json的pipeline
setting.py配置调用pipeline
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2, #添加存储到json文件的pipeline
'ArticleSpider.pipelines.ArticleImagePipeline': 1,
# 'scrapy.pipelines.images.ImagesPipeline': 1,
}
pipelines.py配置存储json的pipeline
class JsonWithEncodingPipeline(object):
def __init__(self):
# 利用codecs方式打开文件,与open函数不同在于编码,可以省去很多编码方面的繁杂工作
self.file = codecs.open("article.json","w",encoding="utf-8")
def process_item(self, item, spider):
#利用json.dumps函数将item存储成json
lines = json.dumps(dict(item),ensure_ascii=False) + "\n"
#写入json文件中
self.file.write(lines)
#process_item函数一定要返回item,因为下一个pipeline还会使用此item
return item
#在spider close的时候关闭file
def spider_closed(self,spider):
self.file.close()
scrapy提供了方便的exporters机制,帮助我们很方便的将item导出成各种类型的文件[前提是setting.py中调用此pipeline,此处省略去写]
# 利用json exporter
class JsonExporterPipeline(object):
#调用scrapy提供的json exporter导出json文件
def __init__(self):
self.file = open('articleexporter.json','wb') # 二进制方式打开
self.exporter = JsonItemExporter(self.file,encoding = "utf-8",ensure_ascii=False)
self.exporter.start_exporting()
def process_item(self, item, spider):
self.exporter.export_item(item)
return item
def close_spider(self,spider):
self.exporter.finish_exporting()
self.file.close()
修改jobbole.py中的create_date为date类型(便于存储到mysql中的date类型)
安装mysql驱动pip install mysqlclient
定义数据存储pipeline,直接将item存储到mysql中
class MysqlPipeline(object):
def __init__(self):
#连接mysqldb,创建连接
self.conn = MySQLdb.connect('127.0.0.1','root','123456','scrapy',charset="utf8",use_unicode=True)
#创建cursor
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
insert_sql = """
insert into jobbole_article(title,create_date,url,url_object_id,front_image_url,front_image_path,comment_nums,fav_nums,praise_nums,tags,content)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
self.cursor.execute(insert_sql,(item["title"],item["create_date"],item["url"],item["url_object_id"],item["front_image_url"],item["front_image_path"],item["comment_nums"],item["fav_nums"],item["praise_nums"],item["tags"],item["content"]))
self.conn.commit()
上述方法(execute和commit操作是同步操作)在后期爬取加解析会快于数据存储到mysql,会导致阻塞。使用twisted框架提供的api可以完成数据的异步写入。
在setting.py中配置相关数据信息
MYSQL_HOST = "127.0.0.1"
MYSQL_DBNAME = "scrapy"
MYSQL_USER = "root"
MYSQL_PASSWORD = "123456"
使用twisted框架实现异步的数据写入
#异步mysql插入
class MysqlTwistedPipeline(object):
#setting中包含setting.py中的内容
@classmethod
def from_settings(cls,settings):
dbparms = dict(
host = settings["MYSQL_HOST"],
db = settings["MYSQL_DBNAME"],
user = settings["MYSQL_USER"],
passwd = settings["MYSQL_PASSWORD"],
charset = "utf8",
cursorclass = MySQLdb.cursors.DictQursor,
use_unicode = True
)
#创建连接池,参数1使用的dbapi的name,
dbpool = adbapi.ConnectionPool("MySQLdb",**dbparms)
return cls(dbpool)
def __init__(self,dbpool):
self.dbpool = dbpool
def process_item(self, item, spider):
#使用twisted将mysql插入变成异步执行,参数1表示要异步执行的函数,参数2表示执行的item
query= self.dbpool.runInteraction(self.do_insert,item)
query.addErrback(self.handle_error)#处理异常
def do_insert(self,cursor,item):
#执行具体的插入
insert_sql = """
insert into jobbole_article(title,create_date,url,url_object_id,front_image_url,front_image_path,comment_nums,fav_nums,praise_nums,tags,content)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
cursor.execute(insert_sql, (
item["title"], item["create_date"], item["url"], item["url_object_id"], item["front_image_url"],
item["front_image_path"], item["comment_nums"], item["fav_nums"], item["praise_nums"], item["tags"],
item["content"]))
#不需要commit,会自动进行commit
def handle_error(self,failure):
#处理异步插入的异常
print(failure)
设计思路
jobbole.py
# -*- coding: utf-8 -*-
import scrapy
import re
from scrapy.http import Request
from urllib import parse
import datetime
from scrapy.loader import ItemLoader
#引入item
from ArticleSpider.items import JobBoleArticleItem
#引入md5处理url
from ArticleSpider.utils.common import get_md5
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/'] #放入想爬取的所有url
def parse(self, response):
"""
1.获取文章列表页中的文章url并交给scrapy下载后并进行解析
2.获取下一页url并交给scrapy进行下载,下载完成后交给parse
:param response:
:return:
"""
#解析列表页的所有文章url并交给scrapy下载后进行解析
#利用::attr()伪类选择器获得对应元素的属性值
post_nodes = response.css("#archive .floated-thumb .post-thumb a")
for post_node in post_nodes:
image_url = post_node.css("img::attr(src)").extract_first("") #获得图片的src
post_url = post_node.css("::attr(href)").extract_first("") #获得连接的href
#利用Request方法提交请求获取对应url内容
#url表示访问的url(parse.urljoin是借助urllib将当前url与请求url进行拼接,从而获取到真实的url),callback表示回调的函数
yield Request(url=parse.urljoin(response.url,post_url),meta={
"front_image_url":image_url},callback=self.parse_detail)
#提交下一页并交给scrapy进行下载
#两个类之间有空格则表示子元素,两个类中间没空格则表示同时满足两个类的元素
next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
if next_url:
yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
#提取文章的具体字段
def parse_detail(self,response):
######################
## 利用css选择器获取内容
######################
# 实例化item
article_item = JobBoleArticleItem()
# 获得meta中的front_image_url,文章封面图
front_image_url = response.meta.get("front_image_url","")
# 获得title
title = response.css(".entry-header h1::text").extract()[0]
print(title)
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·", "").strip()
# 点赞数
praise_nums = response.css(".vote-post-up h10::text").extract()[0]
# 收藏数
collect = response.css('span.bookmark-btn::text').extract()[0]
match_re = re.match(".*?(\d+).*", collect)
if match_re:
collect = match_re.group(1)
else:
collect = 0
# 评论数
comment = response.css('a[href="#article-comment"] span::text').extract()[0]
match_re = re.match(".*?(\d+).*", comment)
if match_re:
comment = match_re.group(1)
else:
comment = 0
# 获得文章内容
content = response.css("div.entry").extract()[0]
# 获得标签
tags = response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag = [element for element in tags if not element.strip().endswith("评论")]
tags = ",".join(tag)
#填充item
article_item['title'] = title
#判断是否有日期,有的话则格式化成对应格式,没有的话则是当前日期
try:
create_date = datetime.datetime.strptime(create_date,"%Y%m%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()
article_item['create_date'] = create_date
article_item['url'] = response.url
article_item['url_object_id'] = get_md5(response.url)
article_item['front_image_url'] = [front_image_url]
article_item['praise_nums'] = praise_nums
article_item['comment_nums'] = comment
article_item['fav_nums'] = collect
article_item['tags'] = tags
article_item['content'] = content
#Item loader加载item
item_loader = ItemLoader(item=JobBoleArticleItem(),response=response)
#使用css方式向item loader中填充值
item_loader.add_css("title",".entry-header h1::text")
item_loader.add_css("create_date","p.entry-meta-hide-on-mobile::text")
item_loader.add_css("praise_nums",".vote-post-up h10::text")
item_loader.add_css("comment_nums","a[href='#article-comment'] span::text")
item_loader.add_css("fav_nums","span.bookmark-btn::text")
item_loader.add_css("tags","p.entry-meta-hide-on-mobile a::text")
item_loader.add_css("content","div.entry")
#使用value方式向item_loader中填充值
item_loader.add_value("url",response.url)
item_loader.add_value("url_object_id",get_md5(response.url))
item_loader.add_value("front_image_url",[front_image_url])
article_item = item_loader.load_item()
yield article_item #将item传递到pipeline中
items.py
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import scrapy
from scrapy.loader.processors import MapCompose
class ArticlespiderItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
#处理的函数,value表示input的值,即初始值
def add_jobbole(value):
return value+"-jobbole"
class JobBoleArticleItem(scrapy.Item):
title = scrapy.Field(
input_processor = MapCompose(add_jobbole) #Mapcompose表示可以对传入的内容调用多个函数进行预处理
) #scrapy可以使用Field表示任意类型
create_date = scrapy.Field()
url = scrapy.Field()
url_object_id = scrapy.Field() #对url进行编码
front_image_url = scrapy.Field()
front_image_path = scrapy.Field()
praise_nums = scrapy.Field()
comment_nums = scrapy.Field()
fav_nums = scrapy.Field()
tags = scrapy.Field()
content = scrapy.Field()
items.py添加对字段的处理内容
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import datetime
import scrapy
import re
from scrapy.loader.processors import MapCompose,TakeFirst,Join
from scrapy.loader import ItemLoader
class ArticlespiderItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
#处理的函数,value表示input的值,即初始值
def add_jobbole(value):
return value+"-jobbole"
#处理时间函数
def date_convert(value):
try:
create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()
return create_date
#获得字符串中数字
def get_nums(value):
match_re = re.match(".*?(\d+).*", value)
if match_re:
nums = int(match_re.group(1))
else:
nums = 0
return nums
#去掉含有评论的tag
def remove_comment_tags(value):
if "评论" in value:
return ""
else:
return value
#什么都不做的函数,专门用于覆盖default_output_processor
def return_value(value):
return value
class JobBoleArticleItem(scrapy.Item):
title = scrapy.Field(
input_processor = MapCompose(add_jobbole) #Mapcompose表示可以对传入的内容调用多个函数进行预处理
) #scrapy可以使用Field表示任意类型
create_date = scrapy.Field(
input_processor = MapCompose(date_convert),
output_processor = TakeFirst() #使用TakeFirst函数可以只获得数组中的第一个
)
url = scrapy.Field()
url_object_id = scrapy.Field() #对url进行编码
front_image_url = scrapy.Field(
output_processor = MapCompose(return_value)
)
front_image_path = scrapy.Field()
praise_nums = scrapy.Field(
input_processor =MapCompose(get_nums)
)
comment_nums = scrapy.Field(
input_processor=MapCompose(get_nums)
)
fav_nums = scrapy.Field(
input_processor=MapCompose(get_nums)
)
tags = scrapy.Field(
input_processor=MapCompose(remove_comment_tags), #tags中可能包含评论数,则需要定义一个函数去掉含评论的tag
output_processor=Join(",") #使用Join函数连接数组中的内容,使用output_processor可以覆盖掉default_output_processor
)
content = scrapy.Field()
#自定义itemloader
class ArticleItemLoader(ItemLoader):
#自定义itemloader
default_output_processor = TakeFirst()
pipelines.py调整内容,在ArticleImagePipeline中添加对item中是否有front_image_url的判断
# -*- coding: utf-8 -*-
#利用codecs打开文件
import codecs
import json
import MySQLdb
import MySQLdb.cursors
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy.pipelines.images import ImagesPipeline
from scrapy.exporters import JsonItemExporter
from twisted.enterprise import adbapi
class ArticlespiderPipeline(object):
def process_item(self, item, spider):
return item
# 将结果存储成json
class JsonWithEncodingPipeline(object):
def __init__(self):
# 利用codecs方式打开文件,与open函数不同在于编码,可以省去很多编码方面的繁杂工作
self.file = codecs.open("article.json","w",encoding="utf-8")
def process_item(self, item, spider):
#利用json.dumps函数将item存储成json
lines = json.dumps(dict(item),ensure_ascii=False) + "\n"
#写入json文件中
self.file.write(lines)
#process_item函数一定要返回item,因为下一个pipeline还会使用此item
return item
#在spider close的时候关闭file
def spider_closed(self,spider):
self.file.close()
# 利用json exporter
class JsonExporterPipeline(object):
#调用scrapy提供的json exporter导出json文件
def __init__(self):
self.file = open('articleexporter.json','wb') # 二进制方式打开
self.exporter = JsonItemExporter(self.file,encoding = "utf-8",ensure_ascii=False)
self.exporter.start_exporting()
def process_item(self, item, spider):
self.exporter.export_item(item)
return item
def close_spider(self,spider):
self.exporter.finish_exporting()
self.file.close()
# 继承ImagePipeline
class ArticleImagePipeline(ImagesPipeline):
def item_completed(self, results, item, info):
if "front_image_url" in item:
# result中包含一个tuple,一个是状态值,一个是返回对象
for ok,value in results:
image_file_path = value["path"]
item["front_image_path"] = image_file_path
return item
#Mysql pipeline
class MysqlPipeline(object):
def __init__(self):
#连接mysqldb,创建连接
self.conn = MySQLdb.connect('127.0.0.1','root','123456','scrapy',charset="utf8",use_unicode=True)
#创建cursor
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
insert_sql = """
insert into jobbole_article(title,create_date,url,url_object_id,front_image_url,front_image_path,comment_nums,fav_nums,praise_nums,tags,content)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
self.cursor.execute(insert_sql,(item["title"],item["create_date"],item["url"],item["url_object_id"],item["front_image_url"],item["front_image_path"],item["comment_nums"],item["fav_nums"],item["praise_nums"],item["tags"],item["content"]))
self.conn.commit()
#异步mysql插入
class MysqlTwistedPipeline(object):
#setting中包含setting.py中的内容
@classmethod
def from_settings(cls,settings):
dbparms = dict(
host = settings["MYSQL_HOST"],
db = settings["MYSQL_DBNAME"],
user = settings["MYSQL_USER"],
passwd = settings["MYSQL_PASSWORD"],
charset = "utf8",
cursorclass = MySQLdb.cursors.DictCursor,
use_unicode = True
)
#创建连接池,参数1使用的dbapi的name,
dbpool = adbapi.ConnectionPool("MySQLdb",**dbparms)
return cls(dbpool)
def __init__(self,dbpool):
self.dbpool = dbpool
def process_item(self, item, spider):
#使用twisted将mysql插入变成异步执行,参数1表示要异步执行的函数,参数2表示执行的item
query= self.dbpool.runInteraction(self.do_insert,item)
query.addErrback(self.handle_error)#处理异常
def do_insert(self,cursor,item):
#执行具体的插入
insert_sql = """
insert into jobbole_article(title,create_date,url,url_object_id,front_image_url,front_image_path,comment_nums,fav_nums,praise_nums,tags,content)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
cursor.execute(insert_sql, (
item["title"], item["create_date"], item["url"], item["url_object_id"], item["front_image_url"],
item["front_image_path"], item["comment_nums"], item["fav_nums"], item["praise_nums"], item["tags"],
item["content"]))
#不需要commit,会自动进行commit
def handle_error(self,failure):
#处理异步插入的异常
print(failure)
jobbole.py文件中通过itemloader处理item并进行传递
# -*- coding: utf-8 -*-
import scrapy
import re
from scrapy.http import Request
from urllib import parse
import datetime
#引入item
from ArticleSpider.items import JobBoleArticleItem, ArticleItemLoader
#引入md5处理url
from ArticleSpider.utils.common import get_md5
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/'] #放入想爬取的所有url
def parse(self, response):
"""
1.获取文章列表页中的文章url并交给scrapy下载后并进行解析
2.获取下一页url并交给scrapy进行下载,下载完成后交给parse
:param response:
:return:
"""
#解析列表页的所有文章url并交给scrapy下载后进行解析
#利用::attr()伪类选择器获得对应元素的属性值
post_nodes = response.css("#archive .floated-thumb .post-thumb a")
for post_node in post_nodes:
image_url = post_node.css("img::attr(src)").extract_first("") #获得图片的src
post_url = post_node.css("::attr(href)").extract_first("") #获得连接的href
#利用Request方法提交请求获取对应url内容
#url表示访问的url(parse.urljoin是借助urllib将当前url与请求url进行拼接,从而获取到真实的url),callback表示回调的函数
yield Request(url=parse.urljoin(response.url,post_url),meta={
"front_image_url":image_url},callback=self.parse_detail)
#提交下一页并交给scrapy进行下载
#两个类之间有空格则表示子元素,两个类中间没空格则表示同时满足两个类的元素
next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
if next_url:
yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
#提取文章的具体字段
def parse_detail(self,response):
######################
## 利用css选择器获取内容
######################
# 实例化item
article_item = JobBoleArticleItem()
# 获得meta中的front_image_url,文章封面图
front_image_url = response.meta.get("front_image_url","")
# 获得title
title = response.css(".entry-header h1::text").extract()[0]
print(title)
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·", "").strip()
# 点赞数
praise_nums = response.css(".vote-post-up h10::text").extract()[0]
# 收藏数
collect = response.css('span.bookmark-btn::text').extract()[0]
match_re = re.match(".*?(\d+).*", collect)
if match_re:
collect = match_re.group(1)
else:
collect = 0
# 评论数
comment = response.css('a[href="#article-comment"] span::text').extract()[0]
match_re = re.match(".*?(\d+).*", comment)
if match_re:
comment = match_re.group(1)
else:
comment = 0
# 获得文章内容
content = response.css("div.entry").extract()[0]
# 获得标签
tags = response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag = [element for element in tags if not element.strip().endswith("评论")]
tags = ",".join(tag)
#填充item
article_item['title'] = title
#判断是否有日期,有的话则格式化成对应格式,没有的话则是当前日期
try:
create_date = datetime.datetime.strptime(create_date,"%Y%m%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()
article_item['create_date'] = create_date
article_item['url'] = response.url
article_item['url_object_id'] = get_md5(response.url)
article_item['front_image_url'] = [front_image_url]
article_item['praise_nums'] = praise_nums
article_item['comment_nums'] = comment
article_item['fav_nums'] = collect
article_item['tags'] = tags
article_item['content'] = content
#Item loader加载item
item_loader = ArticleItemLoader(item=JobBoleArticleItem(),response=response)
#使用css方式向item loader中填充值
item_loader.add_css("title",".entry-header h1::text")
item_loader.add_css("create_date","p.entry-meta-hide-on-mobile::text")
item_loader.add_css("praise_nums",".vote-post-up h10::text")
item_loader.add_css("comment_nums","a[href='#article-comment'] span::text")
item_loader.add_css("fav_nums","span.bookmark-btn::text")
item_loader.add_css("tags","p.entry-meta-hide-on-mobile a::text")
item_loader.add_css("content","div.entry")
#使用value方式向item_loader中填充值
item_loader.add_value("url",response.url)
item_loader.add_value("url_object_id",get_md5(response.url))
item_loader.add_value("front_image_url",[front_image_url])
article_item = item_loader.load_item()
yield article_item #将item传递到pipeline中