阿布云

你所需要的,不仅仅是一个好用的代理。

scrapy简单爬取(二)

阿布云 发表于

p1.png

既然现在所有的 url 都能够获取到了,那么现在我们将 jobbole.py 中的 parse 函数修改一下

def parse(self, response):

    post_nodes = response.css("#archive .floated-thumb .post-thumb")    # a selector, 可以在这个基础上继续做 selector

 

    for post_node in post_nodes:

        post_url = post_node.css("a::attr(href)").extract_first("")

        yield Request(url=parse.urljoin(response.url, post_url),

                      callback=self.parse_detail)

 

    # 必须考虑到有前一页,当前页和下一页链接的影响,使用如下所示的方法

    next_url = response.css("span.page-numbers.current+a::attr(href)").extract_first("")

     if next_url:

         yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)

def parse_detail(self, response):"""作为回调函数,在上面调用"""

title = response.css(".entry-header h1::text").extract()[0]

    match_date = re.match("([0-9/]*).*",

                          response.css(".entry-meta-hide-on-mobile::text").extract()[0].strip())

    if match_date:

        create_date = match_date.group(1)

 

    votes_css = response.css(".vote-post-up h10::text").extract_first()

    if votes_css:

        vote_nums = int(votes_css)

    else:

        vote_nums = 0

 

    ma_fav_css = re.match(".*?(\d+).*",

                          response.css(".bookmark-btn::text").extract_first())

    if ma_fav_css:

        fav_nums = int(ma_fav_css.group(1))

    else:

        fav_nums = 0

 

    ma_comments_css = re.match(".*?(\d+).*",

                               response.css("a[href='#article-comment'] span::text").extract_first())

    if ma_comments_css:

        comment_nums = int(ma_comments_css.group(1))

    else:

        comment_nums = 0

 

    tag_lists_css = response.css(".entry-meta-hide-on-mobile a::text").extract()

    tag_lists_css = [ele for ele in tag_lists_css if not ele.strip().endswith('评论')]

    tags = ','.join(tag_lists_css)

 

    # cpyrights = response.css(".copyright-area").extract()

    content = response.css(".entry *::text").extract()

1. 获取文章列表页中的文章url,交给 scrapy 下载后并进行解析,即调用 parse 函数解析

2. 然后获取下一页的文章 url,按照1 2 循环

对于 parse 函数,一般做三种事情

a. 解析返回的数据 response data

b. 提取数据,生成 ITEM

c. 生成需要进一步处理 URL 的 Request 对象

某些网站中,url 仅仅只是一个后缀,需要将当前页面的url+后缀进行拼接,使用的是 parse.urljoin(base, url) ,如果 urljoin 中的 url 没有域名,将使用base进行拼接,如果有域名,将不会进行拼接,此函数在 python3 的 urllib 库中。Request(meta参数):meta参数是一个字典{},作为回调函数的参数

这样,我们就获得了所有的文章

扩展二:使用item,并保存图片到本地

上一小节提到了, parse 函数提取数据之后,生成 item,scrapy 会通过 http 将 item 传到 pipeline 进行处理,那么这一小节,我们使用 item 来接收 parse 提取的数据。在 items.py 文件中,定义一个我们自己的数据类 JobBoleArticleItem,并继承 scrapy.item 类

class JobBoleArticleItem(scrapy.Item):

    title = scrapy.Field()          # Field()能够接收和传递任何类型的值,类似于字典的形式

    create_date = scrapy.Field()    # 创建时间

    url = scrapy.Field()            # 文章路径

    front_img_url_download = scrapy.Field()

    fav_nums = scrapy.Field()       # 收藏数

    comment_nums = scrapy.Field()   # 评论数

    vote_nums = scrapy.Field()      # 点赞数

    tags = scrapy.Field()           # 标签分类 label

    content = scrapy.Field()        # 文章内容

    object_id = scrapy.Field()      # 文章内容的md5的哈希值,能够将长度不定的 url 转换成定长的序列

Field() 对象,能够接收和传递任何类型的值,看源代码,就能发现,Field() 类继承自 dict 对象,具有字典的所有属性。

注意,在上面定义的类中,我们增加了一个新的成员变量 front_img_url_download ,这是保存的是文章列表中,每一个文章的图片链接。我们需要将这个图片下载到本地环境中。既然使用了 item 接收我们提取的数据,那么 parse 函数就需要做相应的改动

def parse(self, response):

    post_nodes = response.css("#archive .floated-thumb .post-thumb")    # a selector, 可以在这个基础上继续做 selector

 

    for post_node in post_nodes:

        post_url = post_node.css("a::attr(href)").extract_first("")

        img_url = post_node.css("a img::attr(src)").extract_first("")

        yield Request(url=parse.urljoin(response.url, post_url),

                      meta={"front-image-url":img_url}, callback=self.parse_detail)

 

    # 必须考虑到有前一页,当前页和下一页链接的影响,使用如下所示的方法

    next_url = response.css("span.page-numbers.current+a::attr(href)").extract_first("")

     if next_url:

         yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)

同时,解析函数 parse_detail 也需要修改,将数据保存到我们的item中,只需要添加下面的部分就可

front_img_url = response.meta.get("front-image-url", "")

    article_item = JobBoleArticleItem() # 实例化 item 对象

    # 赋值 item 对象

    article_item["title"] = title

    article_item["create_date"] = create_date

    article_item["url"] = response.url

    article_item["front_img_url_download"] = [front_img_url] # 这里传递的需要是列表的形式,否则后面保存图片的时候,会出现类型错误,必须是可迭代对象

    article_item["fav_nums"] = fav_nums

    article_item["comment_nums"] = comment_nums

    article_item["vote_nums"] = vote_nums

    article_item["tags"] = tags

    # article_item["cpyrights"] = cpyrights

    article_item["content"] = ''.join(content)      # 取出的 content 是一个 list ,存入数据库的时候,需要转换成字符串

    article_item["object_id"] = gen_md5(response.url)

    yield article_item

这里,parse 函数成功生成了我们定义的 item 对象,将数据传递到 pipeline。那么,图片链接已经获取到了,我们如下将图片下载下来呢。

解释一下上面代码中的 front-img-url,这个是在 parse 函数中作为参数 meta 传递给 Request() 函数,回调函数调用 parse_detail,返回的 response 对象中的 meta 成员,将包含这个元素, meta 就是一个字典, response.meta.get(“front-image-url”) 将获取到我们传递过来的图片url

scrapy 提供了一个 ImagesPipeline 类,可直接用于图片操作,只需要我们在 settings.py 文件中进行配置即可。

在 settings.py 中,有一个配置参数为 ITEM_PIPELINE ,这其实就是一个字典,当需要用到 pipeline 时,就需要在这个字典中进行配置,字典中存在的, scrapy 才会使用。字典中的 key 就是 pipeline 的类名,后面的数字表示优先级,数字越小表示越先调用,越大越靠后。既然我们现在需要使用到 scrapy 提供的图片下载功能,那么需要在这个字典中配置 ImagesPipeline

ITEM_PIPELINES = {

   'scrapy.pipelines.images.ImagesPipeline': 1,

}

同时,还需要在 settings.py 中配置,item 中哪一个字段是图片 url,以及图片需要存放什么位置

IMAGES_URLS_FIELD = "front_img_url_download"     # ITEM 中的图片 URL,用于下载PROJECT_IMAGE_PATH = os.path.abspath(os.path.dirname(__file__))   # 获取当前文件所在目录IMAGES_STORE = os.path.join(PROJECT_IMAGE_PATH, "images")         # 下载图片的保存位置

这些参数,可以在 ImagesPipeline 类的源代码中查看到

注意:上面配置好后,上面的代码是在工程路径下面创建一个 images 的目录,用于保存图片,运行 main.py,可能会出现如下错误: no module named PIL,这是因为图片操作需要 pillow 库,只需要安装即可

pip install pillow ,快速安装,就按照我上面说的豆瓣源的方法。

还可能出现”ValueError: Missing scheme in request url: h”的错误,这是因为图片操作,要求 front_img_url_download 的值为 list 或者可以迭代的对象,所以我们在 parse 函数中给 item 赋值的时候, front_img_url_download 就是赋值的 list 对象

好了,这些注意了之后,应该能够下载图片了。

扩展三:使用 itemloader

相信大家已经发现,虽然使用了 item,但是使用 css selecotor,我们的 parse 函数显得很长,而且,当数据量越来越大之后,一大堆的 css 表达式是很难维护的。在加上正则表达式的提取,代码会显得很臃肿。这里,给大家推荐是用 itemloader。itemloader 可以看成是一个容器。

首先,在 items.py 中,我们需要定义一个继承自 ItemLoader 的类

class ArticleItemLoader(ItemLoader):

    """

    自定义 ItemLoader, 就相当于一个容器

    """

    # 这里表示,输出获取的 ArticleItemLoader 提取到的值,都是 list 中的第一个值

    # 如果有的默认不是取第一个值,就在 Field() 中进行修改

    default_output_processor = TakeFirst()

将默认输出函数定为 TakeFirst(),即取结果 list 中的第一个值,定义了 ItemLoader 类之后,需要修改 jobbole.py 中的 parse_detail 函数了,现在就不再直接使用 css selector 了,使用 itemloader 中的 css 进行数据提取,新的 parse_detail 如下所示:

def parse_detail(self, response):

    front_img_url = response.meta.get("front-image-url", "")

    item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response)

    article_item_loader = JobBoleArticleItem()

    item_loader.add_css("title", ".entry-header h1::text")  # 通过 css 选择器获取值

    item_loader.add_value("url", response.url)

    item_loader.add_css("create_date", ".entry-meta-hide-on-mobile::text")

    item_loader.add_value("front_img_url_download", [front_img_url])

    item_loader.add_css("fav_nums", ".bookmark-btn::text")

    item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text")

    item_loader.add_css("vote_nums", ".vote-post-up h10::text")

    item_loader.add_css("tags", ".entry-meta-hide-on-mobile a::text")

    item_loader.add_css("content", ".entry *::text")

    item_loader.add_value("object_id", gen_md5(response.url))

    # item_loader.add_xpath()

    # item_loader.add_value()

    article_item_loader = item_loader.load_item()

    yield article_item_loader

这样,数据提取就全部交给了 itemloader 来执行了。代码整体都简洁和工整了很多。ItemLoader 有三个方法用于提取数据,分别是 add_css() , add_xpath() , add_value() ,前两个分别是 css 选择器和 xpath 选择器,如果是值,就直接使用 add_value() 即可。

最后 load_item() 函数,将根据上面提供的规则进行数据解析,每一个解析的值都是以 list 结果的形式呈现,同时,将结果赋值 item。

但是,大家应该已经发现,之前我们直接使用 css selector 提取数据的时候,对于某些数据,需要使用正则表达式进行匹配才能获取所需的值,这里什么都没做,仅仅是通过 itemloader 提取了数据而已。所以,我们还需要重新定义我们的 item 类,这些操作在 item 中进行处理。修改 items.py 中的 JobBoleArticleItem 类,具体如下:

class JobBoleArticleItem(scrapy.item):

    title = scrapy.Field()

    create_date = scrapy.Field(     # 创建时间

        input_processor = MapCompose(get_date),

        output_processor = Join("")

    )

    url = scrapy.Field()            # 文章路径

    front_img_url_download = scrapy.Field(    # 文章封面图片路径,用于下载,赋值时必须为数组形式

        # 默认 output_processor 是 TakeFirst(),这样返回的是一个字符串,不是 list,此处必须是 list

        # 修改 output_processor

        output_processor = MapCompose(return_value)

    )

    front_img_url = scrapy.Field()

    fav_nums = scrapy.Field(        # 收藏数

        input_processor=MapCompose(get_nums)

    )

    comment_nums = scrapy.Field(    # 评论数

        input_processor=MapCompose(get_nums)

    )

    vote_nums = scrapy.Field(       # 点赞数

        input_processor=MapCompose(get_nums)

    )

    tags = scrapy.Field(           # 标签分类 label

        # 本身就是一个list, 输出时,将 list 以 commas 逗号连接

        input_processor = MapCompose(remove_comment_tag),

        output_processor = Join(",")

    )

    content = scrapy.Field(        # 文章内容

        # content 我们不是取最后一个,是全部都要,所以不用 TakeFirst()

        output_processor=Join("")

    )

    object_id = scrapy.Field()      # 文章内容的md5的哈希值,能够将长度不定的 url 转换成定长的序列

input_processor 对传入的值进行预处理, output_processor 对处理后的值按照规则进行处理和提取,比如 TakeFirst() 就是对处理的结果取第一个值。

input_processor = MapCompose(func1, func2, func3, ...) 这行代码,说明的是, Item 传入的这个字段的值,将会分别调用 MapCompose 中的所有传入的方法进行逐个处理,这个方法也是可以是 lambda 的匿名函数。

因为上面定义 ArticleItemLoader 类的时候,使用了默认的 default_output_processor ,如果不想使用默认的这个方法,就在 Field() 中,使用 output_processor 参数覆盖默认的方法,哪怕什么都不做,也不会使用默认方法获取数据了。对上面那些方法定义如下:

def get_nums(value):

    """

    通过正则表达式获取 评论数,点赞数和收藏数

    """

    re_match = re.match(".*?(\d+).*", value)

    if re_match:

        nums = (int)(re_match.group(1))

    else:

        nums = 0

 

    return nums

 

def get_date(value):

    re_match = re.match("([0-9/]*).*?", value.strip())

    if re_match:

        create_date = re_match.group(1)

    else:

        create_date = ""

    return create_date

def remove_comment_tag(value):

    """

    去掉 tag 中的 “评论” 标签

    """

    if "评论" in value:

        return ""

    else:

        return value

 

def return_value(value):

    """

    do nothing, 只是为了覆盖 ItemLoader 中的 default_processor

    """

    return value

千万注意:这些方法,每一个最后,都必须有 return,否则程序到后面将获取不到这个字段的数据,再次访问这个字段的时候,就会报错。

扩展四:将数据导出到 json 文件中

好了,既然已经将数据通过 ItemLoader 获取到了,那么我们现在就将数据从 pipeline 输出到 json 文件中。

将数据以 json 格式输出,可以通过 json 库的方法,也可以使用 scrapy.exporters 的方法。

json 库

我们已经知道,对数据的处理,scrapy 是在 pipeline 中进行的,所以,我们需要在 pipelines.py 中定义我们对数据的导出操作。创建一个新类

class JsonWithEncodingPipeline(object):

    """

    处理 item 数据,保存为json格式的文件中

    """

    def __init__(self):

        self.file = codecs.open('article.json', 'w', encoding='utf-8')

 

    def process_item(self, item, spider):

        lines = json.dumps(dict(item), ensure_ascii=False) + '\n'   # False,才能够在处理非acsii编码的时候,不会出错,尤其

        #中文

        self.file.write(lines)

        return item     # 必须 return

 

    def spider_close(self, spider):

        """

        把文件关闭

        """

        self.file.close()

__init__() 构造对象的时候,就打开文件,scrapy 会调用 process_item() 函数对数据进行处理,在这个函数中,将数据以 json 的格式写入文件中。操作完成之后,将文件关闭。思路很简单。

scrapy.exporters 的方式

class JsonExporterPipeline(object):

 

    def __init__(self):

        """

        先打开文件,传递一个文件

        """

        self.file = open('articleexporter.json', 'wb')

        #调用 scrapy 提供的 JsonItemExporter导出json文件

        self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)

        self.exporter.start_exporting()

 

    def spider_close(self, spider):

        self.exporter.finish_exporting()

        self.file.close()

 

    def process_item(self, item, spider):

        self.exporter.export_item(item)

        return item

scrapy.exporters 提供了几种不同格式的文件支持,能够将数据输出到这些不同格式的文件中,查看 JsonItemExporter 源码即可获知

__all__ = ['BaseItemExporter', 'PprintItemExporter', 'PickleItemExporter',

           'CsvItemExporter', 'XmlItemExporter', 'JsonLinesItemExporter',

           'JsonItemExporter', 'MarshalItemExporter']

这些就是 scrapy 支持的文件。方法名称都差不多,这算是 scrapy 运行 pipeline 的模式,只需要将逻辑处理放在 process_item() ,scrapy 就会根据规则对数据进行处理。

当然,要想使我们写的数据操作有效,别忘记了,在 settings.py 中进行配置

ITEM_PIPELINES = {

 

  'scrapy.pipelines.images.ImagesPipeline': 1,

  'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,

  'ArticleSpider.pipelines.JsonExporterPipeline':3,

}

扩展五:将数据存储到 MySQL 数据库

前面介绍了将数据以 json 格式导出到文件,那么将数据保存到 MySQL 中,如何操作,相信大家已经差不多了然于胸了。这里也介绍两种方法,一种是通过 MySQLdb 的API来实现的数据库存取操作,这种方法简单,适合用与数据量不大的场合,如果数据量大,数据库操作的速度跟不上数据解析的速度,就会造成数据拥堵。那么使用第二种方法就更好,使用 twisted 框架提供的异步操作方法,不会造成拥堵,速度更快。

既然是入 MySQL 数据库,首先肯定是需要创建数据库表了。表结构如下图所示:

3.png

上图中有一个字段的值,我没有讲述怎么取,就是 front_img_path 这个值,大家在数据库入库的时候,直接用空置或者空字符串填充即可。这个字段是保存图片在本地保存的路径,这个需要在 ImagesPipe 的 item_completed(self, results, item, info) 方法中的 results 参数中获取。

好了,数据库表创建成功之后,下面就来将数据入库了。

MySQLdb 的方法入库

class MysqlPipeline(object):

    def __init__(self):

        # 连接数据库

        self.conn = MySQLdb.connect('192.168.0.101', 'spider', 'wuzhenyu', 'article_spider', charset="utf8", use_unicode=True)

        self.cursor = self.conn.cursor()

 

    def process_item(self, item, spider):

        insert_sql = """

            insert into article(title, create_date, url, url_object_id, front_img_url, front_img_path, comment_nums,

            fav_nums, vote_nums, tags, content) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', %d, %d, %d, '%s', '%s')

        """ % (item["title"], item["create_date"], item["url"], item["object_id"],item["front_img_url"],

               item["front_img_path"], item["comment_nums"], item["fav_nums"], item["vote_nums"], item["tags"],

               item["content"])

 

        self.cursor.execute(insert_sql)

        self.conn.commit()

 

    def spider_close(self, spider):

        self.cursor.close()

        self.conn.close()

如果对 API 想了解的更多,就去阅读 python MySQLdb 的相关API文档说明,当然,要想这个生效,首先得在 settings.py 文件中将这个 pipeline 类加入 ITEM_PIPELINE 字典中

ITEM_PIPELINES = {

 

  'scrapy.pipelines.images.ImagesPipeline': 1,

  'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,

  'ArticleSpider.pipelines.JsonExporterPipeline':3,

  'ArticleSpider.pipelines.MysqlPipeline': 4,

}

通过 Twisted 框架提供的异步方法入库

class MysqlTwistedPipeline(object):

    """

    利用 Twisted API 实现异步入库 MySQL 的功能

    Twisted 提供的是一个异步的容器,MySQL 的操作还是使用的MySQLDB 的库

    """

    def __init__(self, dbpool):

        self.dbpool = dbpool

    @classmethod

    def from_settings(cls, settings):

        """

        被 spider 调用,将 settings.py 传递进来,读取我们配置的参数

        模仿 images.py 源代码中的 from_settings 函数的写法

        """

        # 字典中的参数,要与 MySQLdb 中的connect 的参数相同

        dbparams = 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

        )

 

        # twisted 中的 adbapi 能够将sql操作转变成异步操作

        dbpool = adbapi.ConnectionPool("MySQLdb", **dbparams)

        return cls(dbpool)

 

    def process_item(self, item, spider):

        """

        使用 twisted 将 mysql 操作编程异步执行

        """

        query = self.dbpool.runInteraction(self.do_insert, item)

        query.addErrback(self.handle_error) # handle exceptions

 

    def handle_error(self, failure):

        """

        处理异步操作的异常

        """

        print(failure)

 

    def do_insert(self, cursor, item):

        """

        执行具体的操作,能够自动 commit

        """

        print(item["create_date"])

        insert_sql = """

                    insert into article(title, create_date, url, url_object_id, front_img_url, front_img_path, comment_nums,

                    fav_nums, vote_nums, tags, content) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', %d, %d, %d, '%s', '%s');

                """ % (item["title"], item["create_date"], item["url"], item["object_id"], item["front_img_url"],

                       item["front_img_path"], item["comment_nums"], item["fav_nums"], item["vote_nums"], item["tags"],

                       item["content"])

 

        # self.cursor.execute(insert_sql, (item["title"], item["create_date"], item["url"], item["object_id"],

        #                                 item["front_img_url"], item["front_img_path"], item["comment_nums"],

        #                                 item["fav_nums"], item["vote_nums"], item["tags"], item["content"]))

        print(insert_sql)

        cursor.execute(insert_sql)

博主也是近一个多星期开始学习爬虫的 scrapy 框架,对 Twisted 框架也不怎么熟悉,上面的代码是一个例子,大家可以看下注释,等以后了解更多会补充更多相关知识。

需要提到的是,上面定义的 from_settings(cls. settings) 这个类方法, scrapy 会从 settings.py

文件中读取配置进行加载,这里将 MySQL 的一些配置信息放在了 settings.py 文件中,然后使用 from_settings 方法直接获取,在 settings.py 中需要添加如下代码:

# MySQL paramsMYSQL_HOST = ""MYSQL_DBNAME = "article_spider"MYSQL_USER = "spider"MYSQL_PASSWORD = ""

本篇文章,主要以 scrapy 框架爬取伯乐在线文章为例,简要介绍了 scrapy 爬取数据的一些方法,博主也是最近才开始学习爬虫,有不对的地方还请大家能够指正。

windows 中安装环境与 Ubuntu 会有一些不一样,而且如果使用的是 python3.x 版本,会要求 vc++ 的版本比较高,最好安装的是 visual studio 2015 以上的版本。否则会很麻烦。

对于 python2.7版本,在windows中,可以安装 VSForPython27.msi ,依赖的那些库应该就不会再出错了。