目录
准备
这里以爬取一个普通的文章网站来学习爬虫。以爬取网址为http://blog.jobbole.com/all-posts/的网址所有文章的目标为例。按照之前的文章搭建好爬虫环境,这里,因为我要爬取文章,所以我创建的爬虫项目名为ArticleSpider,因为是对这个网站(http://blog.jobbole.com/all-posts/)进行文章爬取,所以创建了针对这个网站爬虫的模板(也就是爬虫逻辑处理编写),创建后的目录结构以及模板的初始状态如下:
创建的步骤
一、创建一个virtualenv虚拟空间
二、(安装scrapy框架)。进入这个虚拟空间,pip install requests
安装request,pip install scrapy
安装scrapy爬虫框架。
pip install 下载好的Twisted文件路径
安装Twisted即可,再从新安装scrapy。三、(使用scrapy创建爬虫项目与生成爬虫模板文件)。安装好scrapy框架后,使用
scrapy startproject 项目名称
,创建爬虫项目,按照后面提示,进入项目,并运行命令scrapy genspider jobbole blog.jobbole.com
生成对应网站的爬虫模板文件。其中,jobbole是爬虫文件名,是唯一的,会在spiders文件夹中生成同名py文件;blog.jobbole.com是爬取的网站域名。四、(创建一个入口文件)。scrapy创建的jobbole.py,是针对blog.jobbole.com的爬虫逻辑文件,但这个文件需要通过命令
scrapy crawl jobbole
在scrapy框架里才能运行,直接运行文件是不行的。所以,需要通过scrapy提供的命令行方法,写在一个文件中,去管理运行这些文件。这个文件我命名为main,作为入口文件,其中代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#!/usr/bin/env python # -*- coding: utf-8 -*- #scrapy提供的命令行执行库 from scrapy.cmdline import execute import os import sys #把项目根目录路径加入到python文件路径查找列表中 sys.path.append(os.path.abspath(os.path.dirname(__file__))) #拼写命令,交给scrapy.cmdline的execute去转化为cmd命令执行 execute(['scrapy','crawl','jobbole']) |
运行这个main.py文件,会报错:No module ‘win32api’,这需要命令:pip install pypiwin32
安装即可解决。
重新运行main.py文件,控制台没报错就成功了。
至此,基本的准备已完成。
编写爬取代码
打开 项目ArticleSpider >> spiders >>jobbole.py文件。可以看见scrapy为我们创建了一个简单爬虫代码模板,其中parse方法scrapy框架会首先调用,response 则接收爬取到网页的数据。以后,没有特别的说明,运行文件指的是运行main.py文件。
使用选择器提取文字
选择器有xpath与css两种,按个人习惯使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# -*- coding: utf-8 -*- import scrapy #爬虫文件需要继承scrapy框架的 scrapy.Spider class JobboleSpider(scrapy.Spider): name = 'jobbole' #爬虫文件名,唯一 allowed_domains = ['blog.jobbole.com'] #允许爬取的域名 start_urls = ['http://blog.jobbole.com/114424/'] #从哪里开始爬 #scrapy 首先会调用parse这个方法 # response 接收爬取到的网页 def parse(self, response): # response 带有css与xpath两种选择器 # extract_first(value) 转为数组,如果第一个数组异常,返回value title = response.css('.entry-header h1::text').extract_first('') print(title) pass |
运行main.py,可以输出获取到的文章标题
爬取所有文章
爬取所有文章的流程:
第一:获取一页列表的文章URL并交给scrapy下载并解释(就是爬取每篇文章的数据)
第二:获取下一页的URL,并交给scrapy下载,下载完后交个parse(就是获取每一页的文章列表,交给第一步,提取文章数据)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# -*- coding: utf-8 -*- import scrapy 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/'] #入口文件 def parse(self, response): # 第一步:获取列表中所有文章的URL,交给scrapy下载并解析 # 获取需要提取的目标元素的共同父元素 post_nodes = response.css('#archive .floated-thumb .post-thumb a') for post_node in post_nodes: post_url = post_node.css('::attr(href)').extract_first("")#文章的url地址 #yield 自动把URL交给scrapy下载 # callback=方法不要带括号,写名字即可 yield Request(url=parse.urljoin(response.url, post_url), callback=self.post_detail) #第二步:提取下一页,交给scrapy下载 #.next.page-numbers 两个class不分开等于class="next page-numbers",分开表示上下级 #extract_first("") 如果数组第一个不存在,返回设定的值,这里我设定的值是"" next_url = response.css('#archive .next.page-numbers::attr("href")').extract_first("") if next_url: # yield 把下一页的URL交给scrapy下载,得到下一页列表又交给parse处理,提取文章详情 yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse) pass #文章详情爬取逻辑 def post_detail(self, response): title = response.css('.entry-header h1::text').extract_first('') print(title) pass |
Request–根据URL获取网页内容
scrapy的下载,需要借助scrapy提供的Request(url=‘’, callback=function) 类。其中url为要下载的网址,callback为回调函数。引入scrapy的Request:from scrapy.http import Request
URL域名缺失的问题
爬取的URL中,有的是完整的网址(带域名):href=”http://blog.jobbole.com/114420/”,有的不是完整的网址(缺少域名): href=”/114420/”。这可以借助urllib提供的parse里的urljoin方法(from urllib import parse),去把没有域名的网址自动补全。 parse.urljoin(base, url),其中,base是域名,url是需要处理的url。 如果是url缺少域名,就会把base域名+url形成新的url;如果url是完整的就不处理。如何获取域名呢?response.url可以返回域名。
scrapy之item
什么是item?
item在项目的items文件中定义。 item是一个类似字典的数据结构,但比字典好,把有用的字段都在item中声明, 最后实例化这个item,把数据与item中的字段对应绑定即可,这样对字段集中管理,可以避免写错字段。 还有一点重要的是,scrapy会把定义的item数据返回到pipelines中,而pipelines是用于数据的处理与保存, 所以,如果要把爬取到的数据传到pipelines中,就要定义item。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# -*- 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 class ArticlespiderItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass #定义自己的item #item是类似字典,但比字典更强大,定义数据字段 class JobBoleArticleItem(scrapy.Item): title = scrapy.Field() #实现了scrapy提供的Field,scrapy会自动寻找数据类型 |
使用item
在jobbole.py中,引入在item.py中自定义的item。然后实例化自定义的item,把数据与对应的item绑定,最后yield 绑定好数据的item,scrapy就会把item传递到pipelines.py中,通过process_item(self, item, spider)
接收到item,其中参数中的item就是接收到的item。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# -*- coding: utf-8 -*- import re import scrapy from scrapy.http import Request from urllib import parse from ArticleSpider.items import JobBoleArticleItem #引入item class JobboleSpider(scrapy.Spider): name = 'jobbole' allowed_domains = ['blog.jobbole.com'] start_urls = ['http://blog.jobbole.com/all-posts/'] #入口文件 def parse(self, response): # 第一步:获取列表中所有文章的URL,交给scrapy下载并解析 # 获取需要提取的目标元素的共同父元素 post_nodes = response.css('#archive .floated-thumb .post-thumb a') for post_node in post_nodes: post_url = post_node.css('::attr(href)').extract_first("")#文章的url地址 #yield 自动把URL交给scrapy下载 yield Request(url=parse.urljoin(response.url, post_url), callback=self.post_detail) #第二步:提取下一页,交给scrapy下载 #.next.page-numbers 两个class不分开等于class="next page-numbers",分开表示上下级 #extract_first("") 如果数组第一个不存在,返回设定的值,这里我设定的值是"" next_url = response.css('#archive .next.page-numbers::attr("href")').extract_first("") if next_url: # yield 把下一页的URL交给scrapy下载,得到下一页列表又交给parse处理,提取文章详情 yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse) pass #文章详情爬取逻辑 def post_detail(self, response): # 需要引入items article_item = JobBoleArticleItem() # 实例化item title = response.css('.entry-header h1::text').extract()[0] # 接收到的值与item进行绑定 article_item['title'] = title article_item['url'] = response.url yield article_item # 把item传到pipelines去 pass |
定义自己的函数库
这时,我需要把文章的URL地址MD5一下,作为url_id存储起来,这时就需要一个MD5的处理方法。这样,我可以在ArticleSpider项目下建立一个名为utils的文件夹,并在里面创建名为common.py的文件,在里面定义自己的函数库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#!/usr/bin/env python # -*- coding: utf-8 -*- # md5在hashlib里 import hashlib def get_md5(url): #判断url是否Unicode编码,是的话,转为utf-8编码 if isinstance(url, str): url = url.encode("utf-8") md5 = hashlib.md5()#实例MD5 md5.update(url)#对URL进行MD5计算 return md5.hexdigest() |
使用自定义函数库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# -*- coding: utf-8 -*- import scrapy from scrapy.http import Request from urllib import parse from ArticleSpider.items import JobBoleArticleItem from ArticleSpider.utils.common import get_md5 #引入公用方法(get_md5) class JobboleSpider(scrapy.Spider): name = 'jobbole' allowed_domains = ['blog.jobbole.com'] start_urls = ['http://blog.jobbole.com/all-posts/'] def parse(self, response): post_nodes = response.css('#archive .floated-thumb .post-thumb a') for post_node in post_nodes: post_url = post_node.css('::attr(href)').extract_first("") yield Request(url=parse.urljoin(response.url, post_url), callback=self.post_detail) next_url = response.css('#archive .next.page-numbers::attr("href")').extract_first("") if next_url: yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse) pass def post_detail(self, response): article_item = JobBoleArticleItem() title = response.css('.entry-header h1::text').extract()[0] article_item['title'] = title article_item['url'] = response.url article_item['url_id'] = get_md5(response.url)# 使用函数库 yield article_item pass |
Request获取携带额外的参数
提取的目标数据是文章详情,如果想把列表的文章封面图也提取,但是这个封面图不属于文章详情页的,如何提取呢?可不可以在获取文章列表的时候获取封面图,当做参数,传进下一次response。 其实这是可以的,Request(url, meta, callback)
, Request有个meta参数,meta定义数据是:meta={键:值}
,这样就可以自己定义数据, 在callback函数中,会存在response的meta中,使用response.meta.get(键名, "")
即可取出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# -*- coding: utf-8 -*- import scrapy from scrapy.http import Request from urllib import parse from ArticleSpider.items import JobBoleArticleItem 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/'] def parse(self, response): 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("") post_url = post_node.css('::attr(href)').extract_first("") #补充meta参数 yield Request(url=parse.urljoin(response.url, post_url), meta={"font_image_url": image_url}, callback=self.post_detail) next_url = response.css('#archive .next.page-numbers::attr("href")').extract_first("") if next_url: yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse) pass def post_detail(self, response): article_item = JobBoleArticleItem() # 上面自定义的meta中的font_image_url的值,存在于response的meta中, # 通过response.meta.get(键名, 出错返回的值) 获取对应键的值 font_image_url = response.meta.get("font_image_url", "") # 封面图片获取,参数一:获取的字段 title = response.css('.entry-header h1::text').extract()[0] article_item['title'] = title article_item['url'] = response.url article_item['url_id'] = get_md5(response.url) article_item['font_image_url'] = font_image_url # item中定义font_image_url yield article_item pass |
scrapy之pipelines
pipelines用于接收item数据并处理数据的存储,比如把数据存储为json或者存储到MySQL数据库。默认的pipelines的示例如下:
1 2 3 |
class ArticlespiderPipeline(object): def process_item(self, item, spider): return item |
其中,class ArticlespiderPipeline(object):
是你定义的pipeline名字,继承自object; def process_item(self, item, spider):
process_item是scrapy来到pipelines首先调用的方法,这个方法会接收item数据,其中的item参数,就是接收item数据的。
scrapy之settings
settings.py存放着许多关于scrapy的设定。这里说一下ITEM_PIPELINES={'ArticleSpider.pipelines.ArticlespiderPipeline': 300,}
这个设定。ITEM_PIPELINES,每当item数据在传输到pipelines之前,都会经过这个设置。这个设置是用来注册pipelines中定义的类,item只会传输到已注册的pipelines中定义的类,并且按权重来调用pipelines中的类。pipelines是一个管道,item是数据流。所以,在默认的设置中,item会传输到pipelines中的ArticlespiderPipeline类中,它的权重为300,数值越小,越优先。
如何保存图片
图片的URL已经有了,如何把图片保存到本地呢?那就是使用scrapy提供的ImagesPipeline。只需要在setting中设置一下就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import os #ITEM_PIPELINES 是用来 配置通过pipelines的传输item的时候,按权重调用类 #pipelines是一个管道,item是数据流,ITEM_PIPELINES相当于拦截器 ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, #scrapy的images库,有很多对图片处理的类,其中ImagesPipeline是通过管道下载图片的类 #图片下载需要安装pillow库,否则报 No module named 'PIL' 'scrapy.pipelines.images.ImagesPipeline': 1, } #告诉ImagesPipeline,图片的下载地址为item的font_image_url字段 #IMAGES_URLS_FIELD接收的是一个数组,不是字符串,否则报ValueError IMAGES_URLS_FIELD = "font_image_url" project_dir = os.path.abspath(os.path.dirname(__file__)) #告诉ImagesPipeline,下载图片的保存地址 IMAGES_STORE = os.path.join(project_dir,'images') |
图片的下载,需要pillow库的支持,使用命令:pip install pillow
安装即可。
还有一点注意的,传给scrapy的ImagesPipeline的图片路径参数类型是一个dict字典,之前在jobbole.py传的是一个字符串,所以,要把图片的URL转换成字典:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# -*- coding: utf-8 -*- import scrapy from scrapy.http import Request from urllib import parse from ArticleSpider.items import JobBoleArticleItem 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/'] def parse(self, response): 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("") post_url = post_node.css('::attr(href)').extract_first("") yield Request(url=parse.urljoin(response.url, post_url), meta={"font_image_url": image_url}, callback=self.post_detail) next_url = response.css('#archive .next.page-numbers::attr("href")').extract_first("") if next_url: yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse) pass def post_detail(self, response): article_item = JobBoleArticleItem() font_image_url = response.meta.get("font_image_url", "") title = response.css('.entry-header h1::text').extract()[0] article_item['title'] = title article_item['url'] = response.url article_item['url_id'] = get_md5(response.url) # font_image_url转换成列表,否则是会影响图片的下载 article_item['font_image_url'] = [font_image_url] yield article_item pass |
获取保存到本地的图片路径
这个需要重写scrapy的ImagesPipeline中的 item_completed(self, results, item, info)方法。保存路径存在results的path当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# -*- 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 #获取图片保存地址,需要重写ImagesPipeline类中的item_completed方法 #保存路径在results对象中 #ITEM_PIPELINES 需要把执行ImagesPipeline,改为执行这个类 class ArticleImagePipeline(ImagesPipeline): def item_completed(self, results, item, info): for ok, value in results: image_file_path = value['path'] item['font_image_path'] = image_file_path return item #记得要把item返回,否则一直会在这个方法里 |
最后,把这个注册到settings.py中的ITEM_PIPELINES并把权重调为最优先。
1 2 3 4 5 6 7 8 |
import os ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, 'ArticleSpider.pipelines.ArticleImagePipeline': 1,#注册自定义类,权重为1,最优先 } IMAGES_URLS_FIELD = "font_image_url" project_dir = os.path.abspath(os.path.dirname(__file__)) IMAGES_STORE = os.path.join(project_dir,'images') |
保存数据
保存数据,都是在pipelines.py里进行编写的,然后,在settings里注册一下就好了。在pipelines.py自定义的一般都会继承自object
,并且重写def process_item(self, item, spider):
这个方法,毕竟这个方法是用来接收item数据的。
自定义保存json
保存为json,需要依赖json类库,使用json.dumps()
来,把item数据转换成json数据;还有需要依赖codecs类库,使用codecs.open()
来创建文件,用于存储json数据。其实也可以不依赖任何类库,直接使用open()
来创建文件,之所以使用codecs.open()
是因为,codecs创建的文件可以避免编码的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#自定义保存为json文件 class JobboleEncodingJson(object): def __init__(self): #使用codecs创建json文件 self.file = codecs.open('article.json','w',encoding='utf-8') def process_item(self, item, spider): #使用 json.dumps()把item转为json数据 # ensure_ascii参数很重要,False表示不使用Unicode编码, # 否则中文会被Unicode编码,从而乱码 lines = json.dumps(dict(item),ensure_ascii=False) + '\n' #把json数据写入到文件中 self.file.write(lines) return item # spider的生命周期,结束的时候调用 def spider_close(self): #关闭文件资源 self.file.close() |
转换为json数据,需要经过三个阶段:
(一):初始化阶段
在初始化阶段,就需要创建好用于保存json数据的文件。codecs.open('article.json','w',encoding='utf-8')
,其中参数“article.json”为创建的文件名,“w”为数据写入文件的方式,“utf-8”为写入的文字编码。
(二):数据处理阶段
数据处理,就需要获取到数据,这就避不开process_item(self, item, spider)
方法,通过它,能接收item数据。item数据转换成json使用json.dumps(dict(item),ensure_ascii=False)
,其中,item数据需要先换成dict字典,ensure_ascii参数非常重要,False表示不使用Unicode编码, 否则中文会被Unicode编码,从而乱码。最后把转换的json数据写入到文件中:self.file.write(lines)
。最后的最后,记得要把item,return回去。
(三):结束阶段
spider有个生命周期函数def spider_close(self):
,在爬虫结束的时候把文件资源关闭释放:self.file.close()
。
最后到settings.py注册一下就好了。
1 2 3 4 5 6 7 8 9 10 |
import os ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, 'ArticleSpider.pipelines.ArticleImagePipeline': 1, 'ArticleSpider.pipelines.JobboleEncodingJson': 2,#注册函数 } IMAGES_URLS_FIELD = "font_image_url" project_dir = os.path.abspath(os.path.dirname(__file__)) IMAGES_STORE = os.path.join(project_dir,'images') |
使用scrapy保存为json文件
scrapy提供了exporter,来把item保存为各种数据,除了json,还可以保存为xml、cvs等等。使用scrapy保存为json数据,需要依赖exporter的JsonItemExporter类库:from scrapy.exporters import JsonItemExporter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
from scrapy.exporters import JsonItemExporter # 导入scrapy转换json的类库 #调用scrapy提供的exporter,导出json文件 class JobboleExporterJson(object): def __init__(self): #创建一个文件 self.file = open('article_exporter.json','wb') #实例化JsonItemExporter self.exporter = JsonItemExporter(self.file, encoding='utf-8', ensure_ascii=False) #JsonItemExporter 开始 self.exporter.start_exporting() def process_item(self, item, spider): # export_item(item) 把item交给scrapy, # scrapy会把item转换成json并存入文件 self.exporter.export_item(item) return item #记得把item return回去 def close_spider(self): self.file.close() #关闭文件资源 self.exporter.finish_exporting() #结束JsonItemExporter |
scrapy转换为json数据,需要经过三个阶段:
(一):初始化阶段
在初始化阶段,就需要创建好用于保存json数据的文件。open('article_exporter.json','wb')
,其中参数“article_exporter.json”为创建的文件名,“wb”为数据写入文件的方式。实例化JsonItemExporter:JsonItemExporter(self.file, encoding='utf-8', ensure_ascii=False)
,其中,参数“self.file”:创建的文件句柄;“encoding”:数据的文字编码;“ensure_ascii”:是否使用Unicode存储数据。
(二):数据处理阶段
数据处理,就需要获取到数据,这就避不开process_item(self, item, spider)
方法,通过它,能接收item数据。只需要把item通过self.exporter.export_item(item)
交给scrapy,scrapy会把item转换成json并存入文件。最后的最后,记得要把item,return回去。
(三):结束阶段
scrapy exporter有个生命周期钩子函数:def close_spider(self):
,在这里把文件资源关闭释放:self.file.close()
;关闭JsonItemExporter:self.exporter.finish_exporting()
最后到settings.py注册一下就好了。
1 2 3 4 5 |
ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, 'ArticleSpider.pipelines.ArticleImagePipeline': 1, 'ArticleSpider.pipelines.JobboleExporterJson': 2,#注册函数 } |
数据保存到MySQL数据库
python操作MySQL数据库,除了已经安装好MySQL数据库外,还需要安装mysqlclient:pip install -i https://pypi.douban.com/simple/ mysqlclient
,这里使用了豆瓣源,加快下载速度。如果安装不了到这个网站(https://www.lfd.uci.edu/~gohlke/pythonlibs/)上,查找Mysqlclient,并下载对应python版本的文件,进行安装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import MySQLdb # 导入MySQL类库 class JobboleMysqlPipeline(object): def __init__(self): # 打开数据库连接 self.conn = MySQLdb.connect( host='127.0.0.1', user='root', password='root', db='benzadmin', charset='utf8', use_unicode=True ) # 使用cursor()方法获取操作游标 self.cursor = self.conn.cursor() def process_item(self, item, spider): # SQL 插入语句 insert_sql = """ insert into spider_jobbole (title,font_image_url,font_image_path,url,url_id) values (%s,%s,%s,%s,%s) """ try: # 执行sql语句 self.cursor.execute(insert_sql, ( item['title'], item['font_image_url'], item['font_image_path'], item['url'], item['url_id'])) # 提交到数据库执行 self.conn.commit() except: # 发生错误时回滚 self.conn.rollback() return item def spider_close(self): # 关闭数据库连接 self.conn.close() |
最后到settings.py注册一下就好了。
MySQL异步存储数据
scrapy框架爬虫速度非常快,以至于MySQL的数据插入速度跟不上,这时应该考虑到使用MySQL的异步存储,而Twisted框架为我们提供了一个异步容器,并不提供数据库的链接,我们在这个异步容器中结合MySQL的类库,使MySQL具备异步的能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
import MySQLdb # 导入MySQL类库 # 导入cursor,cursorclass = MySQLdb.cursors.DictCursor才会生效 import MySQLdb.cursors # adbapi可以使MySQL的操作变成异步的操作 from twisted.enterprise import adbapi class MysqlTwistedPipeline(object): # cls 代表 MysqlTwistedPipeline() # from_settings 方法名固定,scrapy通过这个方法读取settings配置 @classmethod def from_settings(cls, settings): dbparms = dict( host = settings['MYSQL_HOST'], db = settings['MYSQL_DBNAME'], user = settings['MYSQL_USER'], password = settings['MYSQL_PASSWORD'], charset = 'utf8', use_unicode = True, cursorclass = MySQLdb.cursors.DictCursor,# 指定cursor ) # adbapi提供的连接池,能让MySQL异步操作 # MySQLdb 为数据库模块 # **dbparms 为链接数据库的参数 dbpool = adbapi.ConnectionPool('MySQLdb',**dbparms) # 实例化自身类MysqlTwistedPipeline(), # 并把创建好的连接池dbpool作为参数传递过去 return cls(dbpool) # 接收连接池dbpool def __init__(self, dbpool): self.dbpool = dbpool def process_item(self, item, spider): # 使用Twisted 将MySQL插入变成异步执行 query = self.dbpool.runInteraction(self.do_insert, item) query.addErrback(self.handle_MysqlErr) # 异常处理 # 具体的异常处理 def handle_MysqlErr(self, failure, item, spider): print(failure) pass # 具体的插入操作 def do_insert(self, cursor, item): # 插入操作 insert_sql = """ insert into spider_jobbole (title,font_image_url,font_image_path,url,url_id) values (%s,%s,%s,%s,%s) """ # 执行sql语句 cursor.execute(insert_sql, ( item['title'], item['font_image_url'], item['font_image_path'], item['url'], item['url_id'])) # 不需要commit,会自动commit |
在settings.py设置数据库链接参数:
1 2 3 4 5 |
#MySQL配置参数 MYSQL_HOST = '127.0.0.1' MYSQL_DBNAME = 'benzadmin' MYSQL_USER = 'root' MYSQL_PASSWORD = 'root' |
使用item_loader管理item
之前写的item比较混乱,不好管理与维护,所以,借助scrapy的ItemLoader来管理item。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
from scrapy.loader import ItemLoader # 导入 ItemLoader from ArticleSpider.items import JobBoleArticleItem # 导入自定义的item class JobboleSpider(scrapy.Spider): name = 'jobbole' allowed_domains = ['blog.jobbole.com'] start_urls = ['http://blog.jobbole.com/all-posts/'] def post_detail(self, response): # 实例化ItemLoader,item参数为实例化的自定义item item_loader = ItemLoader(item=JobBoleArticleItem(), response=response) # ItemLoader提供 # .add_css(field_name,css) # .add_xpath(field_name, xpath) # .add_value(field_name, value) # field_name 为字段名,css为css表达式,xpath为xpath表达式,value为值 item_loader.add_css('title','.entry-header h1::text') item_loader.add_value('url', response.url) item_loader.add_value('url_id', get_md5(response.url)) item_loader.add_value('font_image_url', [response.meta.get("font_image_url", "")]) # 加载item article_item = item_loader.load_item() yield article_item pass |
解决某些item数据需要特殊处理
某些item数据需要经过进一步的处理,才能得到目标数据。这就需要在items.py文件中,通过导入scrapy的MapCompose:from scrapy.loader.processors import MapCompose
,在MapCompose里传入方法名,就会从左到右依次调用执行方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import scrapy # scrapy的item预处理 # MapCompose 从左到右依次执行方法 from scrapy.loader.processors import MapCompose # 处理方法 # 记得要有个参数接收item # value接收item def titleappend(value): return '标题:'+value class JobBoleArticleItem(scrapy.Item): title = scrapy.Field( # MapCompose依次调用处理方法,对item数据进行数据处理 #input_processor 为输入前的处理 input_processor = MapCompose(titleappend) ) url = scrapy.Field() url_id = scrapy.Field() font_image_url = scrapy.Field() font_image_path = scrapy.Field() |
如何解决item为list,但只取第一个
这个需要在items.py中,借助scrapy的TakeFirst:from scrapy.loader.processors import TakeFirst
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import scrapy # scrapy的item预处理 # MapCompose 从左到右依次执行方法 # TakeFirst 获取列表的第一个值 from scrapy.loader.processors import MapCompose, TakeFirst class ArticlespiderItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass def titleappend(value): return '标题:'+value class JobBoleArticleItem(scrapy.Item): title = scrapy.Field( input_processor = MapCompose(titleappend), #不需要传参数,如果当前item有多个值,只会取第一个 # output_processor 为输出前的处理 output_processor = TakeFirst() ) url = scrapy.Field() url_id = scrapy.Field() font_image_url = scrapy.Field() font_image_path = scrapy.Field() |
如果每个item都需要TakeFirst,那么,可以通过自定义ItemLoader来实现全局的调用。
在items.py文件中,我们需要引入ItemLoader:from scrapy.loader import ItemLoader
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import scrapy from scrapy.loader.processors import MapCompose, TakeFirst # 引入ItemLoader from scrapy.loader import ItemLoader class ArticlespiderItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass def titleappend(value): return '标题:'+value # 自定义ItemLoader class ArticleItemLoader(ItemLoader): # default_output_processor 默认的输出处理 default_output_processor = TakeFirst() class JobBoleArticleItem(scrapy.Item): title = scrapy.Field( input_processor = MapCompose(titleappend) ) url = scrapy.Field() url_id = scrapy.Field() font_image_url = scrapy.Field() font_image_path = scrapy.Field() |
然后,jobbole.py 就不能再用ItemLoader,改用自定义的ItemLoader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 自定义的item还是需要的 from ArticleSpider.items import JobBoleArticleItem # 引入自定义ItemLoader from ArticleSpider.items import ArticleItemLoader class JobboleSpider(scrapy.Spider): name = 'jobbole' allowed_domains = ['blog.jobbole.com'] start_urls = ['http://blog.jobbole.com/all-posts/'] def post_detail(self, response): # 实例化自定义的ArticleItemLoader,传入的参数不变 item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response) item_loader.add_css('title','.entry-header h1::text') item_loader.add_value('url', response.url) item_loader.add_value('url_id', get_md5(response.url)) item_loader.add_value('font_image_url', [response.meta.get("font_image_url", "")]) article_item = item_loader.load_item() yield article_item pass |
在全局TakeFirst下,某些item不需要TakeFirst,保持原有数据
在全局TakeFirst下,某些item不需要TakeFirst。在全局的TakeFirst下,我们可以看见,它定义的是:default_output_processor = TakeFirst()
,如果某些item不需要TakeFirst,可以在Field()里指定:output_processor = 自定义方法
,覆盖掉default_output_processor =TakeFirst()
。其中,自定义方法可以固定这样写:
1 2 3 |
# 接收数据并原样返回 def return_value(value): return value |
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import scrapy from scrapy.loader.processors import MapCompose, TakeFirst from scrapy.loader import ItemLoader class ArticlespiderItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass def titleappend(value): return '标题:'+value #接收数据并原样返回 def return_value(value): return value class ArticleItemLoader(ItemLoader): default_output_processor = TakeFirst() class JobBoleArticleItem(scrapy.Item): title = scrapy.Field( # 覆盖掉全局的default_output_processor = TakeFirst() # 维持原有的数据格式 output_processor = MapCompose(return_value) ) url = scrapy.Field() url_id = scrapy.Field() font_image_url = scrapy.Field() font_image_path = scrapy.Field() |
item的list变成带分隔符的字符串
这个借助scrapy的Join即可完成:from scrapy.loader.processors import Join
:
1 2 3 4 5 6 7 8 9 10 11 12 |
from scrapy.loader.processors import Join # 引入Join class JobBoleArticleItem(scrapy.Item): title = scrapy.Field( # 如果这是一个list,这个Join会以“,”为分割符, # 把list变成字符串 output_processor = Join(',') ) url = scrapy.Field() url_id = scrapy.Field() font_image_url = scrapy.Field() font_image_path = scrapy.Field() |