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'])
main.py

执行前需要修改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()')
View Code

由于如果经常这样调试,会反复进行网站请求,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返回
View Code

我们可以通过命令进行本地调试:

>>> 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()
['

 2018/08/29 · ', '
 
 

 
 · ', '
 
']
# 过滤后的内容中还有一个‘.‘存在
>>> 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'
View Code

 随后我们希望获取文章的点赞数量,我们查看了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
parse_detail

在学习了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()
View Code

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
parse_detail

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(","),
    )           # 文章标签
items.py

获取全部文章

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)
View Code

再看一下列表页,我们还有可能希望获取每条文章前面的图片,并增加到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()           # 文章标签
View Code

然后再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
View Code

建立一个文件处理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()
View Code

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
View Code

还可以对下载图片进行配置,这里我们主要是希望能获得图片文件的保存路径,并赋值给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    
View Code

自定义将结果保存为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) + "
"
        self.file.write(lines)
        return item

    # 定义一个关闭文件的函数
    def spider_closed(self,spider):
        self.file.close()
View Code

使用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
View Code

将结果保存在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()    
View Code
原文地址:https://www.cnblogs.com/trunkslisa/p/9555927.html