Scrapy 学习记录
首先,安装virtualenv虚拟环境
启动虚拟环境,进入希望保存项目的目录
使用下面的命令新建一个scrapy的项目(由于pycharm中没有内置scrapy的项目,只能手动创建)
scrapy startproject ArticleSpider(项目名称)
系统返回表示成功
New Scrapy project 'ArticleSpider', using template directory '/Users/qiuyang/virtualenv/scrapy/lib/python3.6/site-packages/scrapy/templates/project', created in: /Users/qiuyang/Documents/python/ArticleSpider You can start your first spider with: cd ArticleSpider scrapy genspider example example.com
在pycharm中打开项目文件,设置解释器为虚拟环境,然后就可以进行代码的编写工作了
在命令行,在项目目录中创建爬虫文件(指定爬虫文件名称,爬取起始地址):
$ scrapy genspider jobbole blog.jobbole.com
这时在spides目录中就新建了一个jobbole.py的文件
由于pycharm没有默认的scrapy的debug模式,需要自行创建
在项目的根目录下创建main.py文件
from scrapy.cmdline import execute import sys,os # 获取项目路径,将项目路径添加到系统path路径中 sys.path.append(os.path.dirname(os.path.abspath(__file__))) # print(os.path.dirname(os.path.abspath(__file__))) # 执行命令,在命令行中是$ scrapy crawl jobbole execute(['scrapy','crawl','jobbole'])
执行前需要修改settings文件
# Obey robots.txt rules ROBOTSTXT_OBEY = False #需要修改为False,这个配置默认读取每个网站上的robots协议,爬取时须关闭
使用命令行在项目目录中执行命令:
$ scrapy crawl jobbole
可以看到程序正常执行
此时可以使用pycharm的debug模式,在jobbole.py文件中设置断点
点击debug运行程序后,在设置断点的位置将停止程序
选择request时,会浮动一个框,可以点击+号查看详情
这时可以看到返回的类型、url、body等内容,在最右边的view还可以看到详情
开始爬取
首先我们先选择一个页面:http://blog.jobbole.com/114329/
打开页面之后,检查,这时我们就能看到网页上的代码
注意:此时检查的代码与查看源文件的代码有可能不一致,是由于有些html代码是通过js渲染出来的,此时我们就需要针对源文件的代码进行查找,否则有可能出现找不到的情况。
我们在jobbole.py文件中进行编写xpath查找代码:
def parse(self, response): # 通过XPATH查找这个‘h1’标签 re_selector = response.xpath('//*[@class="entry-header"]/h1') # 可以通过text()的方法获取内部文本信息 re_selector_data = response.xpath('//*[@id="post-114329"]/div[1]/h1/text()') # 获取title的标签的文本信息 title = response.xpath('//*[@class="entry-header"]/h1/text()')
由于如果经常这样调试,会反复进行网站请求,scrapy提供了一个shell的功能,可以在终端通过命令进行调试:
# 在终端中,可以通过这个命令,对网址进行请求 scrapy shell http://blog.jobbole.com/114329/ #然后在终端中会显示如下: [s] Available Scrapy objects: [s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc) [s] crawler <scrapy.crawler.Crawler object at 0x10e40a6d8> [s] item {} [s] request <GET http://blog.jobbole.com/114329/> [s] response <200 http://blog.jobbole.com/114329/> [s] settings <scrapy.settings.Settings object at 0x10f1c76d8> [s] spider <DefaultSpider 'default' at 0x10f487898> [s] Useful shortcuts: [s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed) [s] fetch(req) Fetch a scrapy.Request and update local objects [s] shelp() Shell help (print this help) [s] view(response) View response in a browser # 这其中,response对象就是我们获取的html返回
我们可以通过命令进行本地调试:
>>> title = response.xpath('//*[@class="entry-header"]/h1') >>> title [<Selector xpath='//*[@class="entry-header"]/h1' data='<h1>如何在 Linux Shell 编程中定义和使用函数</h1>'>] >>> title = response.xpath('//*[@class="entry-header"]/h1/text()') # 我们可以用extract()命令将selector对象转换为列表对象 >>> title.extract() ['如何在 Linux Shell 编程中定义和使用函数'] # 从列表对象中取出的第一个值,就是我们希望得到的字符串 >>> title.extract()[0] '如何在 Linux Shell 编程中定义和使用函数' # 获取p标签的内容 >>> create_date = response.xpath('//*[@id="post-114329"]/div[2]/p/text()') # 此时我们看到,标签内容中有很多的换行符,我们可以通过strip()命令进行过滤 >>> create_date.extract() ['\r\n\r\n 2018/08/29 · ', '\r\n \r\n \r\n\r\n \r\n · ', '\r\n \r\n'] # 过滤后的内容中还有一个‘.‘存在 >>> create_date.extract()[0].strip() '2018/08/29 ·' # 我们可以将这个.进行替换操作 >>> create_date.extract()[0].strip().replace('·','') '2018/08/29 ' # 随后再进行一个strip()操作,即可得到一个理想的值 >>> create_date.extract()[0].strip().replace('·','').strip() '2018/08/29'
随后我们希望获取文章的点赞数量,我们查看了HTML代码,发现点赞数量的html代码如下:
我们使用如下代码,发现获取的列表为空,是因为xpath的class中,除了我们搜索的内容外,还有其他的内容
# 这个代码无法获取内容 response.xpath('//span[@class="vote-post-up"]')
我们用contains语法进行获取,含义是:获取class中包含vote-post-up的标签
response.xpath('//span[contains(@class="vote-post-up")]')
随后我们再获取这个标签下面的h10标签的内容,进行extract,并获取列表的第一个值,此时获取到的是一个str类型的,由于希望获得的是数字类型的,我们再转换为数字类型的
vote = int(response.xpath('//span[contains(@class,"vote-post-up")]/h10/text()').extract()[0])
获取点赞数量等
# 首先我们获取点赞的标签文本,文本内容如:【1 点赞】 faver_num = response.xpath('//span[contains(@class,"bookmark-btn")]/text()').extract()[0] #我们需要将数字信息提取出来,用正则表达式进行提取 match_fav_re = re.match(".*(\d+).*",faver_num) #如果没人点赞的情况,我们默认设为0,如果有则取出,并转换为数字 if match_fav_re: faver_num = int(match_fav_re.group(1)) else: faver_num = 0
最后获取一下标签信息
tag_list = response.css('.entry-meta-hide-on-mobile a::text').extract() # 有可能获取到的列表中包含了不需要的项目如:【it技术,3评论,微世界】 # 列表去重,去掉以“评论”字符为结尾的所有项 tag_list = [element for element in tag_list if not element.strip().endswith("评论")]
除了通过xpath方式,还可以通过css方式进行标签获取
此时我们就针对每一个页面的内容获取完毕了,我们定义一个获取详情的函数
def parse_detail(self,response): # 提取文章的具体字段 # re_selector_data = response.xpath('//*[@id="post-114329"]/div[1]/h1/text()') title = response.xpath('//*[@class="entry-header"]/h1/text()').extract()[0] create_date = response.css('.entry-meta .entry-meta-hide-on-mobile::text').extract()[0].strip().replace('·','').strip() vote_num = int(response.xpath('//span[contains(@class,"vote-post-up")]/h10/text()').extract()[0]) faver_num = response.xpath('//span[contains(@class,"bookmark-btn")]/text()').extract()[0] match_fav_re = re.match(".*(\d+).*",faver_num) if match_fav_re: faver_num = int(match_fav_re.group(1)) else: faver_num = 0 comment_num = response.xpath('//a[@href="#article-comment"]/span/text()').extract()[0] match_com_re = re.match(".*(\d+).*",comment_num) if match_com_re: comment_num = int(match_com_re.group(1)) else: comment_num = 0 cotent = response.xpath('//div[@class="entry"]').extract()[0] tag_list = response.css('.entry-meta-hide-on-mobile a::text').extract() # 列表去重,去掉以评论字符为结尾的所有项 tag_list = [element for element in tag_list if not element.strip().endswith("评论")] pass
在学习了itemload以后,可以对这里进行大幅修改,将获取的方式留下,处理方式则放在item中处理
1、首先在item.py文件中新建一个类继承ItemLoader类,就可以修改其中的配置
from scrapy.loader.processors import TakeFirst from scrapy.loader import ItemLoader # 自定义一个类,继承ItemLoader类,就可以修改其中的配置 class ArticleItemLoader(ItemLoader): # 自定义itemloader,修改默认取第一个值,全部变为str default_output_processor = TakeFirst()
2、在jobbole.py文件中的类中新建
class JobboleSpider(scrapy.Spider): name = 'jobbole' allowed_domains = ['blog.jobbole.com'] start_urls = ['http://blog.jobbole.com/all-posts/']
def parse_detail(self, response): # 提取文章的具体字段 # 将原来代码中的meta传来的url获取一下 front_image_url = response.meta.get('front_image_url', '') # 通过item loader加载item,通过自定义的ArticleItemLoader类进行实例对象,传入两个参数,一个是item对象,一个是获取的response item_loader = ArticleItemLoader(item=JobboleArticleItem(), response=response) # item_loader.add_xpath() item_loader.add_css('title', '.entry-header h1::text') item_loader.add_css('create_date', '.entry-meta .entry-meta-hide-on-mobile::text') item_loader.add_xpath('vote_num', '//span[contains(@class,"vote-post-up")]/h10/text()') item_loader.add_xpath('faver_num', '//span[contains(@class,"bookmark-btn")]/text()') item_loader.add_xpath('comment_num', '//a[@href="#article-comment"]/span/text()') item_loader.add_css('tag_list', '.entry-meta-hide-on-mobile a::text') item_loader.add_xpath('cotent', '//div[@class="entry"]') item_loader.add_value('front_image_url', [front_image_url]) item_loader.add_value('url', response.url) item_loader.add_value('url_object_id', get_md5(response.url)) # 调用默认的item方法以后,会将所有的值全部变为list形式 article_item = item_loader.load_item() # yield之后,会传递到pipelines.py中,需要在settings中将pipelines生效,将系统已经生成好的ITEM_PIPELINES打开注释 yield article_item
3、设置itme.py,新建了很多的方法,将方法传入item中
# 测试函数,可以将值后面增加字符串 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_num(value): match_re = re.match(".*(\d+).*", value) if match_re: num = int(match_re.group(1)) else: num = 0 return num # 专门去掉标签中的'评论字样' def remove_comment_tags(value): if '评论' in value: return '' else: return value # 空函数,用于将值变为列表格式 def return_value(value): return value # 自定义一个类,继承ItemLoader类,就可以修改其中的配置 class ArticleItemLoader(ItemLoader): # 自定义itemloader,修改默认取第一个值,全部变为str default_output_processor = TakeFirst() class JobboleArticleItem(scrapy.Item): # scrapy只有一个Field类型 # title = scrapy.Field() # 文章名称 # create_date = scrapy.Field() # 创建日期 # url = scrapy.Field() # 文章url # url_object_id = scrapy.Field() # url->md5 # front_image_url = scrapy.Field() # 封面图片 # front_image_path = scrapy.Field() # 封面图片存放路径 # vote_num = scrapy.Field() # 点赞数 # faver_num = scrapy.Field() # 收藏数 # comment_num = scrapy.Field() # 评论数 # cotent = scrapy.Field() # 文章正文html # tag_list = scrapy.Field() # 文章标签 title = scrapy.Field( # 给title全部增加一个jobbole的结尾,甚至可以调用多个函数 #input_processor=MapCompose(add_jobbole, lambda x: x+'-Trunkslisa') ) # 文章名称 create_date = scrapy.Field( # 给时间格式进行转换 input_processor=MapCompose(date_convert), # 将获取的值取第一个 # output_processor=TakeFirst() ) # 创建日期 url = scrapy.Field() # 文章url url_object_id = scrapy.Field() # url->md5 front_image_url = scrapy.Field( # 下载文件时,系统需要数组形式的数据,调用一个空函数,重新赋值 output_processor=MapCompose(return_value), ) # 封面图片 front_image_path = scrapy.Field() # 封面图片存放路径 vote_num = scrapy.Field( input_processor=MapCompose(get_num) ) # 点赞数 faver_num = scrapy.Field( input_processor=MapCompose(get_num) ) # 收藏数 comment_num = scrapy.Field( input_processor=MapCompose(get_num) ) # 评论数 cotent = scrapy.Field() # 文章正文html tag_list = scrapy.Field( # 由于获取的内容本身就是list,这时候使用默认的TakeFirst()就不是太合适了 # 这时使用scrapy提供的Join类 output_processor=Join(","), ) # 文章标签
获取全部文章
1、获取文章列表页中的文章url 并scrapy下载后交给解析函数进行具体字段的获取
2、获取下一页的url 并交给scrapy进行下载,下载完成后再交给parse函数
定义parse函数
from scrapy.http import Request def parse(self, response): """ 1、获取文章列表页中的文章url 并scrapy下载后交给解析函数进行具体字段的获取 2、获取下一页的url 并交给scrapy进行下载,下载完成后再交给parse函数 """ post_list = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract() for plist in post_list: # 如果页面的url没有全部,只能提取到url的部分,需要针对url做拼接操作 # Request(url=parse.urljoin(response.url, plist), callback=self.parse_detail) yield Request(url=plist, callback=self.parse_detail) # print(post_list) # 提取下一页的url,并交给scrapy进行下载 # 同一个节点2个class标签,可以通过没有空格的方式指定 next_urls = response.css(".next.page-numbers::attr(href)").extract()[0] if next_urls: yield Request(url=next_urls, callback=self.parse)
再看一下列表页,我们还有可能希望获取每条文章前面的图片,并增加到request中一起发给detail中进行保存
图中可以看到图片的url与文章的url分别处于两个不同的div之中,scrapy可以进行再次筛选,并将image_url通过meta参数,按照字典的方式传入
post_node = response.css("#archive .floated-thumb") for plist in post_node: image_url = plist.css('.post-thumb a img::attr(src)').extract()[0] post_url = plist.css('.post-thumb a::attr(href)').extract()[0] # 可以将image_url通过meta参数,按照字典的方式传入 # 如果页面的url没有全部,只能提取到url的部分,scrapy可以将response.url直接拼接进入;若已经有域名,则不生效 yield Request(url=post_url, callback=self.parse_detail, meta={'front_image_url': image_url})
通过debug方式可以看到,response中的meta里面,已经有front_image_url的值,现在我们在detail中按照字典方式将其取出
# 通过字典方式将meta中的front_image_url取出,通过get方式防止异常,并赋一个空值 front_image_url = response.meta.get('front_image_url', '')
items.py
定义一个数据类
class JobboleArticleItem(scrapy.Item): # scrapy只有一个Field类型 title = scrapy.Field() # 文章名称 create_date = scrapy.Field() # 创建日期 url = scrapy.Field() # 文章url url_object_id = scrapy.Field() # url->md5 front_image_url = scrapy.Field() # 封面图片 front_image_path = scrapy.Field() # 封面图片存放路径 vote_num = scrapy.Field() # 点赞数 faver_num = scrapy.Field() # 收藏数 comment_num = scrapy.Field() # 评论数 cotent = scrapy.Field() # 文章正文html tag_list = scrapy.Field() # 文章标签
然后再jobbole.py中引入
from ArticleSpider.ArticleSpider.items import JobboleArticleItem
在parse_detail中实例化item
# 实例化Item类 article_item = JobboleArticleItem() # 给item设置的字段赋值 article_item['title'] = title article_item['create_date'] = create_date article_item['url'] = response.url article_item['front_image_url'] = [front_image_url] article_item['vote_num'] = vote_num article_item['faver_num'] = faver_num article_item['comment_num'] = comment_num article_item['cotent'] = cotent article_item['tag_list'] = tag_list # 这是将获取到的url进行了md5的编码 article_item['url_object_id'] = get_md5(response.url) # yield之后,会传递到pipelines.py中,需要在settings中将pipelines生效,将系统已经生成好的ITEM_PIPELINES打开注释 yield article_item
建立一个文件处理md5编码的情况
import hashlib def get_md5(url): # 判断传入的url是否是str格式的(str格式的默认都是unicode编码),并用utf8进行编码,用来确保可以进行md5运算 if isinstance(url,str): url = url.encode('utf8') m = hashlib.md5() m.update(url) return m.hexdigest()
pipeline想要生效还需要在setting文件中进行设置
# 默认路径是注释掉的,打开注释 ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticleSpiderPipeline': 300, # 如果希望使用系统的功能对图片进行下载,需要加入这个pipelines,并设置一下pipylines的执行顺序,数字越小,越早执行 '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, 'image') # 配置下载100*100以上的图片 IMAGES_MIN_HEIGHT = 100 IMAGES_MIN_WIDTH = 100
还可以对下载图片进行配置,这里我们主要是希望能获得图片文件的保存路径,并赋值给item中我们设定的字段
# 引入类 from scrapy.pipelines.images import ImagesPipeline # 新建一个类,并继承系统的ImagesPipeline类 class ArticleImagePipeline(ImagesPipeline): """ 自定义编写这个函数,可以通过results获取文件保存的路径 通过debug可以看到results是一个元组形式的,元组第一个值是True,第二个值是一个字典。 字典中有一个path的key,value是保存的地址 通过一个for循环,将value取出,这个value是一个列表形式的 """ def item_completed(self, results, item, info): for ok, value in results: img_file_path = value['path'] # 此时我们就可以将文件保存的路径保存在front_image_path中 item['front_image_path'] = img_file_path # pipeline类必须要返回item return item
自定义将结果保存为json文件
import codecs # codecs与open类似,但是减少了很多的编码工作 import json # 自定义保存json的pipeline class JsonWithEncodingPipeline(object): def __init__(self): self.file = codecs.open('artcle.json','w',encoding='utf-8') # 处理item的写入文件的主要函数 def process_item(self,item,spider): lines = json.dumps(dict(item), ensure_ascii=False) + "\n" self.file.write(lines) return item # 定义一个关闭文件的函数 def spider_closed(self,spider): self.file.close()
使用scrapy自带的json文件保存模块
# 调用scrapy提供的JsonExporter导出json文件 class JsonExporterPipeline(object): def __init__(self): self.file = open('articleexport.json','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
将结果保存在mysql中,首先需要在mysql中新建一个表,并设置好字段
# 需要在虚拟环境安装 mysqlclient # pip3 install mysqlclient import MySQLdb class MysqlPipeline(object): def __init__(self): # self.conn = MySQLdb.connect('host','user','password','dbname',charset="utf8",use_unicode=True) self.conn = MySQLdb.connect('127.0.0.1','root','123','article_spider',charset="utf8",use_unicode=True) self.cursor = self.conn.cursor() def process_item(self,item,spider): insert_sql = "insert into jobbole(title,create_date,url,url_object_id,front_image_url,front_image_path,vote_num,faver_num,comment_num,cotent) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)" sql = 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['vote_num'], item['faver_num'], item['comment_num'], item['cotent'], # 这里本来希望把标签也进行导入,但是实际操作过程中报错了,就先注释掉了 # item['tag_list'] )) self.conn.commit()