Python分布式爬虫打造搜索引擎-scrapy爬取知名技术文章网站
一、项目基础环境
python3.6.0
pycharm2018.2
mysql+navicat
二、scrapy爬取知名技术文章网站
1、使用虚拟环境 ,创建虚拟环境:
mkvirtualenv --python=C:\python3.6\python.exe articlespider #python3.6版本
2、进入articlespider目录,安装scrapy:
pip install -i https://pypi.douban.com/simple/ scrapy #豆瓣源安装速度快
安装时报错:
放到 articlespider目录下 ,安装:
pip install Twisted-18.7.0-cp36-cp36m-win_amd64.whl
3、创建爬虫项目:
1)创建项目:
scrapy startproject ArticleSpider
创建成功:
2)项目中创建spider模块:
进入ArticleSpider ,创建spider:
#jobbole:spider名称 , blog.jobbole.com:要爬取数据的url
scrapy genspider jobbole blog.jobbole.com
创建成功:
在pycharm中打开我们的ArticleSpider项目,可以看到spiders下多了一个py文件:jobbole.py
scrapy目录结构:
scrapy.cfg:配置文件
setings.py:基本设置
SPIDER_MODULES = ['ArticleSpider.spiders'] #存放spider的路径
NEWSPIDER_MODULE = 'ArticleSpider.spiders'
pipelines.py:做跟数据存储相关的东西
middilewares.py:自己定义的middlewares 定义方法,处理响应的IO操作
init.py:项目的初始化文件
items.py:定义我们所要爬取的信息的相关属性。Item对象是种类似于表单,用来保存获取到的数据
此时,pycharm下articlespiders项目还没完全配置好:
①、首先需要在pycharm的setting中的project interpreter下配置python:使用虚拟环境articlespider下的python3.6
②、接着,爬虫项目不像Django项目,没有自动配置好调试或运行相关,需要我们手动生成一个main.py文件,作为启动文件:
1)在articlespiders项目下新建main.py文件:
#ArticleSpiders/main
from scrapy.cmdline importexecuteimportsys,os
sys.path.append(os.path.dirname(os.path.abspath(__file__))) #父路径
execute(['scrapy','crawl','jobbole']) #执行指令:scrapy crawl jobbole ,执行后会跳到jobbole.py中执行JobboleSpider类
2)settings.py下设置不遵守reboots协议 :
ROBOTSTXT_OBEY 设为False
ROBOTSTXT_OBEY = False
执行main.py文件,报错:ModuleNotFoundError: No module named 'win32api',在虚拟环境下安装 pypiwin32 :
pip install -i https://pypi.douban.com/simple/ pypiwin32
再debug运行main.py文件,此时运行成功:
4、 xpath的使用
1)为什么要使用xpath?
xpath使用路径表达式在xml和html中进行导航
xpath包含有一个标准函数库
xpath是一个w3c的标准
xpath速度要远远超beautifulsoup
2)xpath节点关系
父节点 *上一层节点*
子节点
兄弟节点 *同胞节点*
先辈节点 *父节点,爷爷节点*
后代节点 *儿子,孙子*
3)xpath语法:
简单使用xpath基本语法爬取数据:
推荐使用class型,因为后期循环爬取可扩展通用性强
importscrapyclassJobboleSpider(scrapy.Spider):
name= 'jobbole'allowed_domains= ['blog.jobbole.com']
start_urls= ['http://blog.jobbole.com/']defparse(self, response):
re_selector= response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1/text()") # 不支持这种绝对路径,容易出错
re2_selector= response.xpath('//*[@id="post-110287"]/div[1]/h1/text()')
re3_selector= response.xpath('//div[@class="entry-header"]/h1/text()')
完整的xpath提取伯乐在线字段代码:
importscrapyimportreclassJobboleSpider(scrapy.Spider):
name= "jobbole"allowed_domains= ["blog.jobbole.com"]
start_urls= ['http://blog.jobbole.com/110287/']defparse(self, response):#提取文章的具体字段
title = response.xpath('//div[@class="entry-header"]/h1/text()').extract_first("")
create_date= response.xpath("//p[@class='entry-meta-hide-on-mobile']/text()").extract()[0].strip().replace("·","").strip()
praise_nums= response.xpath("//span[contains(@class, 'vote-post-up')]/h10/text()").extract()[0]
fav_nums= response.xpath("//span[contains(@class, 'bookmark-btn')]/text()").extract()[0]
match_re= re.match(".*?(\d+).*", fav_nums)ifmatch_re:
fav_nums= match_re.group(1)
comment_nums= response.xpath("//a[@href='#article-comment']/span/text()").extract()[0]
match_re= re.match(".*?(\d+).*", comment_nums)ifmatch_re:
comment_nums= match_re.group(1)
content= response.xpath("//div[@class='entry']").extract()[0]
tag_list= response.xpath("//p[@class='entry-meta-hide-on-mobile']/a/text()").extract()
tag_list= [element for element in tag_list if not element.strip().endswith("评论")]
tags= ",".join(tag_list)
小demo:
tag_list=['职场','2 评论','今昔']
[element for element in tag_list if not element.strip().endswith('评论')]
# 结果['职场', '今昔']
以上都是在pycharm中debug调试进行的,如觉麻烦可在终端开启调试模式:
scrapy shell http://blog.jobbole.com/110287/ #开启控制台调试
5、CSS选择器
CSS选择器的使用:
#通过css选择器提取字段
#front_image_url = response.meta.get("front_image_url", "") #文章封面图
title = response.css(".entry-header h1::text").extract_first()
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]
fav_nums= response.css(".bookmark-btn::text").extract()[0]
match_re= re.match(".*?(\d+).*", fav_nums)ifmatch_re:
fav_nums= int(match_re.group(1))else:
fav_nums=0
comment_nums= response.css("a[href='#article-comment'] span::text").extract()[0]
match_re= re.match(".*?(\d+).*", comment_nums)ifmatch_re:
comment_nums= int(match_re.group(1))else:
comment_nums=0
content= response.css("div.entry").extract()[0]
tag_list= response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag_list= [element for element in tag_list if not element.strip().endswith("评论")]
tags= ",".join(tag_list)
爬取 http://blog.jobbole.com/all-posts/ 所有文章 :
yield关键字:
#yield Request:会将url交给scrapy引擎进行下载数据,,下载完成后会调用回调方法parse_detail()提取文章内容中的字段
yield Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)
scrapy下的request下载网页,并执行回调函数:
from scrapy.http importRequest
Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)
urllib模块下的parse.urljoin函数能拼接网址,原因是爬取到的网址可能是个相对路径,不是绝对路径,使用parse.urlkoin可以避免一些错误(具体看源码):
urljoin(参数1,参数2):如果没有参数1,url=参数二,反过来,url=参数1.如果参数1/2都存在,会提取出参数1域名,与参数2比较,再合并成一个完整url
from urllib importparse
url=parse.urljoin(response.url,post_url)
parse.urljoin("http://blog.jobbole.com/all-posts/","http://blog.jobbole.com/111535/")#结果为http://blog.jobbole.com/111535/
爬取所有文章完整代码:
importscrapyfrom scrapy.http importRequestfrom urllib importparseclassJobboleSpider(scrapy.Spider):
name= 'jobbole'allowed_domains= ['blog.jobbole.com']
start_urls= ['http://blog.jobbole.com/all-posts/']defparse(self, response):"""1. 获取文章列表页中的文章url并交给scrapy下载后并进行解析
2. 获取下一页的url并交给scrapy进行下载, 下载完成后交给parse
流程:执行parse函数 → request下载,回调parse_detail函数 → 提取下一页交给scrapy进行下载,回调parse函数,
则又执行request下载,回调parse_detail函数,如此循环,直到获取不到下一页"""
#解析列表页中的所有文章url并交给scrapy下载后并进行解析
post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract()for post_url inpost_urls:#request下载完成之后,回调parse_detail进行文章详情页的解析
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("")ifnext_url:yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse)defparse_detail(self, response):#提取文章的具体字段
#title = response.xpath('//div[@class="entry-header"]/h1/text()').extract_first("")
#create_date = response.xpath("//p[@class='entry-meta-hide-on-mobile']/text()").extract()[0].strip().replace("·","").strip()
#praise_nums = response.xpath("//span[contains(@class, 'vote-post-up')]/h10/text()").extract()[0]
#fav_nums = response.xpath("//span[contains(@class, 'bookmark-btn')]/text()").extract()[0]
#match_re = re.match(".*?(\d+).*", fav_nums)
#if match_re:
#fav_nums = match_re.group(1)
# #comment_nums = response.xpath("//a[@href='#article-comment']/span/text()").extract()[0]
#match_re = re.match(".*?(\d+).*", comment_nums)
#if match_re:
#comment_nums = match_re.group(1)
# #content = response.xpath("//div[@class='entry']").extract()[0]
# #tag_list = response.xpath("//p[@class='entry-meta-hide-on-mobile']/a/text()").extract()
#tag_list = [element for element in tag_list if not element.strip().endswith("评论")]
#tags = ",".join(tag_list)
#通过css选择器提取字段
front_image_url = response.meta.get("front_image_url", "") #文章封面图
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]
fav_nums= response.css(".bookmark-btn::text").extract()[0]
match_re= re.match(".*?(\d+).*", fav_nums)ifmatch_re:
fav_nums= int(match_re.group(1))else:
fav_nums=0
comment_nums= response.css("a[href='#article-comment'] span::text").extract()[0]
match_re= re.match(".*?(\d+).*", comment_nums)ifmatch_re:
comment_nums= int(match_re.group(1))else:
comment_nums=0
content= response.css("div.entry").extract()[0]
tag_list= response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag_list= [element for element in tag_list if not element.strip().endswith("评论")]
tags= ",".join(tag_list)
View Code
6、items设计
数据爬取的任务就是从非结构的数据中提取出结构性的数据。
items 可以让我们自定义自己的字段(类似于字典,但比字典的功能更齐全)
6.1、当我们爬取数据,获取数据后需要将数据传给下个函数调用,则可以利用scrapy request下的meta属性,meta接收的是字典类型,当使用request爬取数据时,将需要传递给下个函数的数据放到meta中,meta中的数据会传给下个函数的response中去,可以被下个函数使用
1)关于scrapy request的meta:
Request中meta参数的作用是传递信息给下一个函数,使用过程可以理解成:
把需要传递的信息赋值给这个叫meta的变量,
但meta只接受字典类型的赋值,因此
要把待传递的信息改成“字典”的形式,即:
meta={'key1':value1,'key2':value2}
如果想在下一个函数中取出value1,
只需得到上一个函数的meta['key1']即可,
因为meta是随着Request产生时传递的,
下一个函数得到的Response对象中就会有meta,
即response.meta,
meta是一个dict,主要是用解析函数之间传递值,一种常见的情况:在parse中给item某些字段提取了值,但是另外一些值需要在parse_item中提取,这时候需要将parse中的item传到parse_item方法中处理,显然无法直接给parse_item设置而外参数。 Request对象接受一个meta参数,一个字典对象,同时Response对象有一个meta属性可以取到相应request传过来的meta
request meta
2)关于urllib parse.urljoin(url1,url2)
parse.url:能将相对路径,自动补充域名补全路径,前提主域名response能获取域名路径
一般爬取网上数据时,存在网页某些url是相对路径形式展示的,通过js代码等操作补全,此时我们要爬取完整url路径,就需要用到parse.urljoin方法了
如果你是相对路径,没有补全域名,我就从response里取出来补全,如果你有域名我则不起作用
获取文章url及封面图url,爬取下载页面相应数据:
调用callback函数,meta里的数据可以传递到parse_detail函数中调用:
项目创建成功,在项目ArticleSpider/ArticleSpider目录下有个items.py文件,如上述所说,我们能将爬取到的数据通过自定义自己的字段(类似字典),保存起来,之后可以通过pipelines.py文件,将数据保存到数据库等地方去。
6.2、items.py创建类 JobBoleArticleItem ,继承于scrapy.Itme:
classJobBoleArticleItem(scrapy.Item):
title=scrapy.Field()
create_date=scrapy.Field()
url=scrapy.Field()
url_object_id=scrapy.Field()
front_image_url=scrapy.Field()
front_image_path=scrapy.Field()
praise_nums=scrapy.Field()
comment_nums=scrapy.Field()
fav_nums=scrapy.Field()
content=scrapy.Field()
tags= scrapy.Field()
6.3、在jobbole.py parse_detail 中实例化 JobBoleArticleItem , 并将爬取到的数据放入 JobBoleArticleItem 对象中:
from ArticleSpider.items importJobBoleArticleItem
def parse_detail(self, response):
article_item =JobBoleArticleItem() # 实例化 article_item["title"] =title # 保存获取到的数据到Item 中
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_nums
article_item["fav_nums"] =fav_nums
article_item["tags"] =tags
article_item["content"] = content
接着在 parse_detail 中添加代码:
article_item数据会传到pipelines当中去
yield article_item
同时,setting中添加代码:
#setting.py
ITEM_PIPELINES={'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
}
ITEM_PIPELINES:item的管道,配置好ArticlespiderPipeline路径,当执行代码 yield article_item 时,程序会转到pipelines.py文件中执行 ArticlespiderPipeline 类,
也就是通过 yield article_item 将article_item数据传给pipelines调用。并执行pipelines中的相关类,如: ArticlespiderPipeline ,在 ArticlespiderPipeline 类中,我们可以通过代码编写,将数据存到数据库中去
ArticlespiderPipeline 代码:
classArticlespiderPipeline(object):defprocess_item(self, item, spider):return item
item中有个key:_value , _value中存放着我们爬取到的所有数据:
所以目前完整爬虫执行顺序是:创建main.py,执行main函数 → 跳到spiders目录下的jobbole.py 执行 JobboleSpider 类 ,类中有parse 、parse_detail等函数 → JobboleSpider类中获取到数据,实例化 JobBoleArticleItem ,将数据保存到Items 中 ,执行 yield article_item → 跳转到pipelines.py中执行ArticlespiderPipeline 类
6.4、图片下载保存
有时候我们爬取到图片或文件等,希望把它保存到本地或数据库中
首先进入setting中 ITEM_PIPELINES 配置图片下载pipeline:
ITEM_PIPELINES={'scrapy.pipelines.images.ImagesPipeline': 1,
}
Scrapy中的pipelines提供了默认的文件、图片、媒体等下载保存方式,如上所述,将路径配置上去就会相应的执行scrapy/pipelines的相关函数 ,
另外,ITEM_PIPELINES是一个数据管道的登记表,每一项后面的具体数字代表它的优先级,数字越小,越早进入管道执行
接下来我们新建一个images文件夹用于保存图片,在setting中配置images文件夹的路径:
project_dir = os.path.abspath(os.path.dirname(__file__))
IMAGES_STORE= os.path.join(project_dir, 'images') #图片文件路径
指定某字段(我们获取图片路径的字段)为我们的图片文件处理
IMAGES_URLS_FIELD = "front_image_url" #指定爬取的某(图片)字段作为图片处理
此时执行会报错:
原因是因为图片保存等需要pillow库来操作,解决方法就是在虚拟环境中安装pillow:pip install pillow
重新执行又报错:
原因是'scrapy.pipelines.images.ImagesPipeline' 中对图片的要求是数组格式,而我们目前传递的图片类型(front_image_url)是字符串格式,因此我们需要将爬取的图片类型改成数组类型获取
解决方法:
article_item["front_image_url"] =front_image_url#↓改成数组形式
article_item["front_image_url"] = [front_image_url]
接着,重新运行项目,成功爬取到数据及下载图片成功保存:
额外知识:
scrapy下的pipelines目录下有files.py、images.py、media.py ,用于处理文件、图片、用户上传下载媒体等数据
关于imager.py目前用到参数(常用参数可看源码):
IMAGES_URLS_FIELD = "front_image_url" #指定该字段作为图片处理对象
IMAGES_STORE= os.path.join(project_dir, 'images') #图片存放路径
IMAGES_MIN_HEIGHT= 100 #图片最小高度
IMAGES_MIN_WIDTH = 100 #图片最小宽度
使用scrapy自带的image.py处理图片,只需要按要求命名一致,以及在ITEM_PIPELINES 配置好所使用的类即可:ImagesPipeline ,
使用files.py、media.py也是一样的道理。
6.5、上面我们已经爬取到图片并保存到我们的本地中,现在我们想办法将保存到服务器或本地的图片重新绑定个路径,方便调用
1)在pipelines.py中新建自定义 ArticleImagePipeline 类,继承于ImagePipeline ,重载Scrapy下的image.py中的 item_completed方法,
item_completed方法参数中有一个名为results的参数,该参数内部数据是个list,list内部是元组的类型,每个元组有两个子数据,第一个是状态,成功则返回True,另一个子数据是个字典类型,字典内部有个path参数,path参数存放的就是我们服务端图片保存的路径。如下:
result = {(True, {'url':'http://......'} ) }
#pipelines.py
from scrapy.pipelines.images import ImagesPipeline
classArticleImagePipeline(ImagesPipeline): # 继承ImagePipelinedefitem_completed(self, results, item, info):if "front_image_url" in item: #
for ok, value in results: #results[0] = ‘ok’
image_file_path = value["path"]
item["front_image_path"] =image_file_pathreturn item #需返回item
2)items.py中JobBoleArticleItem 类,添加字段:
classJobBoleArticleItem(scrapy.Item):
title=scrapy.Field()
create_date=scrapy.Field()
url=scrapy.Field()
url_object_id=scrapy.Field()
front_image_url=scrapy.Field()
front_image_path= scrapy.Field() #新增
praise_nums =scrapy.Field()
comment_nums=scrapy.Field()
fav_nums=scrapy.Field()
content=scrapy.Field()
tags= scrapy.Field()
3)在setting.py中配置我们自定义的pipelines类路径:
ITEM_PIPELINES ={'ArticleSpider.pipelines.ArticlespiderPipeline': 300,'scrapy.pipelines.images.ImagesPipeline': 1, #scrapy自带图片处理
'ArticleSpider.pipelines.ArticleImagePipeline': 2, #新增 , 自定义ArticleImagePipeline ,需配置好管道路径
}
通过上述三步操作,就已经把下载后的图片绝对路径保存到我们的item中去了
6.6、我们每爬取一个文章,或者说一个url,我们就应该给该url进行去重处理,这样可以避免我们在爬取数据时遇到同一个url时会重新爬一次数据。所有爬取成功的url都逐个存于response.url中,
我们现在要做的是将response中的url进行md5加密处理,之后存于数据库
1)首先,新建utils包,用于处理一些额外的函数等,在utils中新建common.py,编写get_md5函数:
关于md5加密:python3中数据默认都是Unicode类型,进行md5加密时,需要将数据转行成utf8才能进行加密,否则报错:
#utils/common.py
importhashlibdefget_md5(url):if isinstance(url, str): #python3中数据都是Unicode类型,需要转换成utf8才能被hash加密,否则会报错,py3中str即为Unicode
url = url.encode("utf-8") #如果是Unicode类型则encode成utf8
m =hashlib.md5()
m.update(url)return m.hexdigest()
2)在jobbole.py中parse_detail函数中,将response.url 进行md5加密处理并保存到item中:
from ArticleSpider.utils.common importget_md5defparse_detail(self, response):
article_item["url_object_id"] = get_md5(response.url) #新增
article_item["title"] =title
article_item["url"] =response.url
.
.
.
3)同时,在items.py下的 JobBoleArticleItem 中新增字段:
url_object_id = scrapy.Field()
6.7数据保存相关
1)保存到本地json文件中
方式一:
在pipelines.py文件中新建类:JsonWithEncodingPipeline
打开文件时不直接使用 open ,用python自带的codecs包,可以避免一些编码方面的问题出现
使用json.dumps时,不使用ensure_ascii=False ,默认输出中文是ASCII字符码,要想正确输出中文,需要指定ensure_ascii=False
pipelines.py/JsonWithEncodingPipelines:
importcodecsclassJsonWithEncodingPipeline(object):#自定义json文件的导出
def __init__(self):
self.file= codecs.open('article.json', 'w', encoding="utf-8")def process_item(self, item, spider): #yield item后,跳到pipelines.py中执行对应类时,默认会执行此方法
#将item转换为dict,然后生成json对象,false避免中文出错
lines = json.dumps(dict(item), ensure_ascii=False) + "\n"self.file.write(lines)returnitem#scrapy.signals中的信号量状态,spider_closed:当spider关闭爬虫时调用
defspider_closed(self, spider):
self.file.close()
接着在setting中的ITEM_PIPELINES 配置好:
ITEM_PIPELINES ={'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
}
当爬虫爬取数据,保存到 Item 中,经过yield Item ,转到pipelines中执行里面的类
方式二:
使用scrapy中自带的类保存文件:
#scrapy/exporters
['BaseItemExporter',
'PprintItemExporter',
'PickleItemExporter',
'CsvItemExporter',
'XmlItemExporter',
'JsonLinesItemExporter',
'JsonItemExporter', # 保存json文件
'MarshalItemExporter']
from scrapy.exporters importJsonItemExporterclassJsonExporterPipleline(object):#调用scrapy提供的json export导出json文件
def __init__(self):
self.file= open('articleexport.json', 'wb')
self.exporter= JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)
self.exporter.start_exporting()defclose_spider(self, spider):
self.exporter.finish_exporting()
self.file.close()defprocess_item(self, item, spider):
self.exporter.export_item(item)return item
接着在setting中配置 ITEM_PIPELINES:
'ArticleSpider.pipelines.JsonExporterPipleline': 6
2)将 数据保存到mysql中
首先,在Navicat中新建数据库article_spider ,再新建表,之后填充数据类型:
其中,
1、create_date ,在数据库中是datetime类型,我们在爬取数据时是以字符串形式保存,因此需要更改下item['create_date']类型:
importdatetimetry:
create_date= datetime.datetime.strptime(create_date, "%Y/%m/%d").date()exceptException as e:
create_date= datetime.datetime.now().date()
注:
datetime.datetime.strptime():将字符串转换成日期格式
datetime.datetime.strftime():将日期格式转换成字符串
datetime.datetime.strftime().date():日期
datetime.datetime.strftime().time():时间
datetime.datetime.now():当前日期时间
datetime.datetime.now().date():当前日期
2、需要设定一个主键,我们用url_object_id 来做主键
准备工作弄好,需要在虚拟环境下安装:mysqlclient
pip install mysqlclient
安装成功即可以开始我们的数据保存到mysql操作了
方式一: 利用pipelines保存数据到数据库(同步)
importMySQLdbclassMysqlPipeline(object):#采用同步的机制写入mysql
def __init__(self):
self.conn= MySQLdb.connect('127.0.0.1', 'root', 'password', 'article_spider', charset="utf8", use_unicode=True)
self.cursor=self.conn.cursor()defprocess_item(self, item, spider):
insert_sql= """insert into jobbole_article(title, url, create_date, fav_nums)
VALUES (%s, %s, %s, %s)"""self.cursor.execute(insert_sql, (item["title"], item["url"], item["create_date"], item["fav_nums"]))
self.conn.commit()
useUnicode=true&characterEncoding=UTF-8 作用:
添加的作用是:指定字符的编码、解码格式。
例如:mysql数据库用的是gbk编码,而项目数据库用的是utf-8编码。这时候如果添加了useUnicode=true&characterEncoding=UTF-8,那么作用有如下两个方面:1. 存数据时:
数据库在存放项目数据的时候会先用UTF-8格式将数据解码成字节码,然后再将解码后的字节码重新使用GBK编码存放到数据库中。2.取数据时:
在从数据库中取数据的时候,数据库会先将数据库中的数据按GBK格式解码成字节码,然后再将解码后的字节码重新按UTF-8格式编码数据,最后再将数据返回给客户端
View Code
接着,在setting中的 ITEM_PIPELINES 配置:
'ArticleSpider.pipelines.MysqlPipeline': 7
方式二:利用pipelines保存数据到数据库(twisted异步)
因为我们的爬取速度可能大于数据库存储的速度,最好是异步操作(异步化操作mysql)
首先,我们将数据库连接的相关配置参数转到setting.py中配置:
MYSQL_HOST = "127.0.0.1"MYSQL_DBNAME= "article_spider"MYSQL_USER= "root"MYSQL_PASSWORD= "123456"
接着在pipelines.py中新建类:MysqlTwistedPipline ,编写代码如下:
执行顺序:from_settings → init → pross_item
importMySQLdbimportMySQLdb.cursorsfrom twisted.enterprise importadbapiclassMysqlTwistedPipeline(object):def __init__(self,dbpool):
self.dbpool=dbpool
@classmethoddeffrom_settings(cls,settings):#读取配置文件信息,在类初始化(实例)时调用
dbparm =dict(
host= settings["MYSQL_HOST"],
db= settings["MYSQL_DBNAME"],
user= settings["MYSQL_USER"],
passwd= settings["MYSQL_PASSWORD"],
charset= 'utf8',
cursorclass=MySQLdb.cursors.DictCursor, # 字典类型,还有一种json类型
use_unicode=True,
)dbpool= adbapi.ConnectionPool("MySQLdb",**dbparm) #tadbapi.ConnectionPool:wisted提供的一个用于异步化操作的连接处(容器)。将数据库模块,及连接数据库的参数等传入即可连接mysql
return cls(dbpool) #实例化 MysqlTwistedPipeline
defprocess_item(self,item,spider):#操作数据时调用
query = self.dbpool.runInteraction(self.sql_insert,item) #在连接池执行mysql语句相应操作 ,异步操作
query.addErrback(self.handle_error,item,spider) #异常处理
defhandle_error(self,failure,item,spider):#处理异常
print("异常:",failure)defsql_insert(self,cursor,item):#数据插入操作
insert_sql = """insert into article_spider(title, url, create_date, fav_nums,url_object_id)
VALUES (%s, %s, %s, %s,%s)"""cursor.execute(insert_sql,(item["title"],item["url"],item["create_date"],item["fav_nums"],item["url_object_id"]))
最后,切记一定要在setting中的ITEM_PIPELINES 配置好路径:
ITEM_PIPELINES ={'ArticleSpider.pipelines.MysqlTwistedPipeline': 3,
}
关于mysql数据增删改查,也可以使用django ORM模式,需下载相关依赖包,具体参考:
7、scrapy item loader机制
使用scrapy的itemloader来维护提取代码,
itemloadr提供了一个容器,让我们配置某一个字段该使用哪种规则
from scrapy.loader importItemLoader#三种方法
add_css
add_value # 与其他两种不同,这种是直接获取value。如获取request爬取的url:add_value(“url”,response.url)
add_xpath
使用scrapy itemloader的方法,获取到的数据都是列表类型,如果我们要拿的是具体value数据,就需要做进一步处理。
item.py中定义的类中的字段可以带两个参数,我们可以通过scrapy自带的MapCompose来处理函数,再把结果以参数形式传给item下类的各字段:
MapCompose:用来处理函数
scrapy.Field:可接收两个参数(也可不填,按默认):
input_processor:预处理,当数据传进来时可以将数据进一步处理
output_processor:输出,经预处理后的数据再输出
from scrapy.loader.processors importMapComposedef add_mtianyan(value): #自定义函数,用于MapCompose调用。被调用时参数value即为调用方传入的value,如下面函数中调用,参数value即为title中的数据,接着对value进一步处理后再将处理好数据返回给title
return value+"-mtianyan"
classJobBoleArticleItem(scrapy.Item):
title=scrapy.Field(
input_processor=MapCompose(lambda x:x+"mtianyan",add_mtianyan), #使用MapCompose处理函数,每个数据传入时都会调用一次MapCompose,再经过MapCompose调用设定的函数,有几个函数就调用几个函数
output_processor=TakeFirst() # 该字段返回列表类型,此行代码表示:取第一个数据
)
)
itemloader使用
1)在item.py中自定义 ArticleItemLoader 类,继承于 ItemLoader :
classArticleItemLoader(ItemLoader):#自定义itemloader中output_processor默认方法,实现默认提取第一个数据
default_output_processor = TakeFirst()
2)在jobbole.py中的parse_detail 方法中,实例化ArticleItemLoader ,并使用ItemLoader自带方法爬取数据,并放置到item中:
#from scrapy.loader import ItemLoader
from pipelines importArticleItemLoader#通过item loader加载item
front_image_url = response.meta.get("front_image_url", "") #文章封面图
#item_loader = ItemLoader(item=JobBoleArticleItem(), response=response)
item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response) #使用自定义ItemLoader实例化JobBoleArticleItem
item_loader.add_css("title", ".entry-header h1::text")
item_loader.add_value("url", response.url)
item_loader.add_value("url_object_id", get_md5(response.url))
item_loader.add_css("create_date", "p.entry-meta-hide-on-mobile::text")
item_loader.add_value("front_image_url", [front_image_url])
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", ".bookmark-btn::text")
item_loader.add_css("tags", "p.entry-meta-hide-on-mobile a::text")
item_loader.add_css("content", "div.entry")#最后,一定要调用这个方法来对规则进行解析生成item对象
article_item = item_loader.load_item()
yield article_item
自定义ArticleItemLoader的好处:
爬虫爬取数据时,使用ItemLoader机制获取到的数据都是列表类型,如果每个字段都加output_processor参数,显得冗余,通过自定义ArticleItemLoader类,实现默认output_processor方法,之后每个字段的output_processor都会默认使用ArticleItemLoader的默认output方法
scrapy.Field自带的两个参数,除了output_processor外,另外一个参数input_processor可以对字段进行一些另外的处理,再将数据返回给字段。比如说,要将爬取到的图片路径(字符串格式)转换成日期格式:
def get_date(value):#value为爬取到的url(字符串格式)
try:
create_date= datetime.datetime.strptime(value,'%Y/%m/%d').date() #将字符串格式的url转换成日期格式,再传回给该字段
exceptException as e:
create_date=datetime.date.now().date()return create_date
接着,在item.py下的 JobBoleArticleItem类中处理:
scrapy 自带的MapCompose模块,可以处理函数相关
from scrapy.loader.processors importMapComposeclassJobBoleArticleItem(scrapy.Item):
create_date=scrapy.Field(
input_processor=MapCompose(get_date), #此时获取到的数据即为日期格式
)
这样就可以注释掉一堆爬虫爬取数据的冗余代码了:
爬取伯乐技术网站所有文章完整代码
1、setting.py:
importos
BOT_NAME= 'ArticleSpider'SPIDER_MODULES= ['ArticleSpider.spiders']
NEWSPIDER_MODULE= 'ArticleSpider.spiders'ROBOTSTXT_OBEY=False
ITEM_PIPELINES={'ArticleSpider.pipelines.ArticlespiderPipeline': 300,'ArticleSpider.pipelines.ArticleImagePipeline': 2, #用于图片下载时调用
#'ArticleSpider.pipelines.JsonWithEncodingPipeline': 3, # 方式一:用于保存item 数据 ,在图片下载之后再调用
'ArticleSpider.pipelines.MysqlTwistedPipeline': 4, #方式三:异步数据库保存item数据
#'ArticleSpider.pipelines.JsonExporterPipleline': 3, # 方式二:使用scrapy提供的JsonItemExporter保存json文件,用于保存item 数据
#'scrapy.pipelines.images.ImagesPipeline':1 # scrapy中的pipelines自带的ImagesPipeline,用于图片下载,另外还有图片、媒体下载
}
BASE_DIR= os.path.dirname(os.path.abspath(__file__))
IMAGES_STORE= os.path.join(BASE_DIR,'images') #名称是固定写法,文件保存路径
IMAGES_URLS_FIELD = "acticle_image_url" #名称是固定写法。设定acticle_image_url字段为图片url,下载图片时找此字段对应的数据
ITEM_DATA_DIR= os.path.join(BASE_DIR,"item_data") #item数据保存到当地item_data文件夹
#mysql配置
MYSQL_HOST = "127.0.0.1"MYSQL_DBNAME= "article_spider"MYSQL_USER= "root"MYSQL_PASSWORD= "******"
setting.py
2、jobbole.py:
importscrapyfrom scrapy.http importRequestfrom urllib importparsefrom ArticleSpider.items importJobBoleActicleItem,ArticleItemLoaderfrom ArticleSpider.util.common importget_md5classJobboleSpider(scrapy.Spider):
name= 'jobbole'allowed_domains= ['blog.jobbole.com']#start_urls = ['http://blog.jobbole.com/']
start_urls = ['http://blog.jobbole.com/all-posts/']defparse(self, response):#爬取伯乐网所有的文章目标信息
#1、获取文章列表页的所有url,并交由scrapy进行逐个下载,通过回调函数:parse_detail解析并获取目标数据
post_nodes = response.css('#archive .floated-thumb .post-thumb a') #获取到的是个list,包含urls、imgs
for post_node inpost_nodes:
post_url= post_node.css('::attr(href)').extract_first('')
post_img= post_node.css('img::attr(src)').extract_first('') #通过request内部meta,将img_url传给下个函数调用
yield Request(url=parse.urljoin(response.url,post_url),meta={"acticle_image_url":post_img},callback=self.parse_detail)#2、每下载并获取完一页的文章后,接着获取下一页的url,让parse方法接着处理下一页所有文章的目标数据
next_url = response.css('.nex.page-numbers::attr(href)').extract_first('')print("nexturl",next_url)ifnext_url:yield Request(url=parse.urljoin(response.url,next_url),callback=parse)defparse_detail(self,response):
acticle_image_url= response.meta.get("acticle_image_url", "") #文章封面图
item_loader = ArticleItemLoader(item=JobBoleActicleItem(), response=response) #ArticleItemLoader:自定义item loader 。JobBoleActicleItem:实例化item对象
item_loader.add_css("title", "div.entry-header h1::text")
item_loader.add_value("url", response.url)
item_loader.add_value("url_object_id", get_md5(response.url))
item_loader.add_css("create_date", ".entry-meta-hide-on-mobile::text")
item_loader.add_value("acticle_image_url", [acticle_image_url])
item_loader.add_css("praise_nums", ".vote-post-up h10::text")
item_loader.add_css("comment_nums", ".btn-bluet-bigger.href-style.hide-on-480::text")
item_loader.add_css("fav_nums", "span.btn-bluet-bigger.href-style.bookmark-btn.register-user-only::text")
item_loader.add_css("tags", ".entry-meta-hide-on-mobile a::text")
item_loader.add_css("content", ".category-it-tech")#最后,一定要调用这个方法来对规则进行解析生成item对象
article_item =item_loader.load_item()yield article_item
jobbole.py
3、items.py:
from scrapy.loader importItemLoaderfrom scrapy.loader.processors importMapCompose, TakeFirst, JoinimportscrapyimportdatetimeimportreclassArticleItemLoader(ItemLoader):#自定义itemloader
default_output_processor =TakeFirst()defreturn_value(value):returnvaluedefdate_convert(value):#将str 转date格式
try:
create_date= datetime.datetime.strptime(value, "%Y/%m/%d").date()exceptException as e:
create_date=datetime.datetime.now().date()returncreate_datedefget_nums(value):#匹配评论/点赞/收藏数
match_re = re.match(".*?(\d+).*", value)ifmatch_re:
nums= int(match_re.group(1))else:
nums=0returnnumsclassJobBoleActicleItem(scrapy.Item):
title= scrapy.Field() #标题
create_date = scrapy.Field( #日期,date类型
input_processor=MapCompose(date_convert),
)
url= scrapy.Field() #爬取的url
url_object_id = scrapy.Field() #已经md5加密的爬取过的url
acticle_image_url = scrapy.Field( #封面图路径,下载时需确定为list
output_processor=MapCompose(return_value) #直接返回value(列表类型),不做任何处理(默认获取第一个值)
)
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( #标签,list类型
output_processor=MapCompose(return_value)
)
content= scrapy.Field() #内容
article_image_path = scrapy.Field() #图片保存在服务端的地址
items.py
4、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 importImagesPipelinefrom ArticleSpider.settings importITEM_DATA_DIRfrom scrapy.exporters importJsonItemExporterimportcodecsimportosimportjsonclassArticlespiderPipeline(object):defprocess_item(self, item, spider):returnitemclass ArticleImagePipeline(ImagesPipeline): #继承ImagesPipeline,用于下载图片时进行一些处理
def item_completed(self, results, item, info): #重载ImagesPipeline中的item_completed,下载完成时调用
if "acticle_image_url" in item: #如果存在,表示item中有这个字段
for complete_status,value in results: #results是一个list,里面是元组类型,每个元组有两个数据,一个是状态true or false,另一个数据是个字典
image_file_path = value["path"] #value取出元组中的第二个数据(字典),该数据内的path保存中图片下载到服务器后的路径
item["article_image_path"] = image_file_path #将图片存放路径存到item中
returnitem#--------------- item 数据保存 ----------------- ##class JsonWithEncodingPipeline(object):##方式一:使用json文件的方式,保存item数据#def __init__(self):#item_article_jsonfile = os.path.join(ITEM_DATA_DIR,'item_article.json')#self.file = codecs.open(item_article_jsonfile, 'w', encoding="utf-8") # 使用python自带的codecs打开文件,可以避免一些编码错误#def process_item(self, item, spider): #yield item后,跳到pipelines.py中执行对应类时,默认会执行此方法##item是JobBoleActicleItem类型,将item转换为dict,然后生成json对象,ensure_ascii=false避免中文出错#lines = json.dumps(dict(item), ensure_ascii=False) + "\n"#self.file.write(lines)#return item##scrapy.signals中的信号量状态,spider_closed:当spider关闭爬虫时调用#def spider_closed(self, spider):#self.file.close()
## item 数据保存,方式二:使用scrapy提供的JsonItemExporter保存json文件#class JsonExporterPipleline(object):##调用scrapy提供的json export导出json文件#def __init__(self):#item_article_jsonfile = os.path.join(ITEM_DATA_DIR, 'item_article_export.json')#self.file = open(item_article_jsonfile, 'wb')#self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)#self.exporter.start_exporting()#
#def close_spider(self, spider):#self.exporter.finish_exporting()#self.file.close()#
#def process_item(self, item, spider):#self.exporter.export_item(item) # 写入数据#return item
#item 数据保存,方式三:以异步方式写入数据库
importMySQLdbimportMySQLdb.cursorsfrom twisted.enterprise importadbapiclassMysqlTwistedPipeline(object):def __init__(self,dbpool):
self.dbpool=dbpool
@classmethoddef from_settings(cls,settings): #用于读取配置文件信息,先于process_item调用
dbparm =dict(
host=settings["MYSQL_HOST"],
db=settings["MYSQL_DBNAME"],
user=settings["MYSQL_USER"],
passwd=settings["MYSQL_PASSWORD"],
charset='utf8',
cursorclass=MySQLdb.cursors.DictCursor, #字典类型,还有一种json类型
use_unicode=True,
)
dbpool= adbapi.ConnectionPool("MySQLdb",**dbparm) #tadbapi.ConnectionPool:wisted提供的一个用于异步化操作的连接处(容器)。将数据库模块,及连接数据库的参数等传入即可连接mysql
return cls(dbpool) #实例化 MysqlTwistedPipeline
defprocess_item(self, item, spider):#操作数据时调用
query = self.dbpool.runInteraction(self.sql_insert, item) #执行mysql语句相应操作 ,异步操作
query.addErrback(self.handle_error, item, spider) #异常处理
defhandle_error(self, failure, item, spider):#处理异常
print("异常:", failure)defsql_insert(self, cursor, item):#数据插入操作
insert_sql = """insert into article_spider(title, url, create_date, fav_nums,url_object_id)
VALUES (%s, %s, %s, %s,%s)"""cursor.execute(insert_sql,
(item["title"], item["url"], item["create_date"], item["fav_nums"], item["url_object_id"]))
pipelines.py
5、main.py:运行主程序
from scrapy.cmdline importexecuteimportos,sys
sys.path.append(os.path.dirname(os.path.abspath(__file__))) #将父路径添加至sys path中
execute(['scrapy','crawl','jobbole',]) #执行:scrapy crawl jobbole 。 其中'jobbole'是jobbole.py中JobboleSpider类的name字段数据#execute(['scrapy','crawl','jobbole','--nolog']) # --nolog:表示不打印日志
main.py