文章目录

  • 第7章 使用Exporter导出数据
  • 7.1 指定如何导出数据
  • 7.1.2 配置文件
  • 7.2 添加导出数据格式
  • 7.2.1 源码参考
  • 7.2.2 实现Exporter
  • 7.3 本章小结


第7章 使用Exporter导出数据

  之前章节的学习了Scrapy中爬取数据、封装数据、处理数据的相关技术,本章学习如何将爬取到的数据以某种数据格式保存到文件中,即导出数据。
  在Scrapy中,负责导出数据的组件被称为Exporter(导出器),Scrapy内部实现了多个Exporter,每个Exporter实现一种数据格式的导出,支持的数据格式如下(括号中为相应的Exporter):
(1)JSON (JsonItemExporter)
(2)JSON lines (JsonLinesItemExporter)
(3)CSV (CsvItemExporter)
(4)XML (XmlItemExporter)

(5)Pickle (PickleItemExporter)
(6)Marshal (MarshalItemExporter)
  其中,前4种是极为常用的文本数据格式,而后两种是Python特有的。在大多数情况下,使用Scrapy内部提供的Exporter就足够了,需要以其他数据格式(上述6种以外)导出数据时,可以自行实现Exporter

7.1 指定如何导出数据

  在导出数据时,需向Scrapy爬虫提供以下信息:
  ● 导出文件路径。
  ● 导出数据格式(即选用哪个Exporter)。

  可以通过以下两种方式指定爬虫如何导出数据:
  (1)通过命令行参数指定。
  (2)通过配置文件指定。

  在运行scrapy crawl命令时,可以分别使用:
   -o参数:指定导出文件路径
   -t参数:指定导出数据格式。

  在example项目中,使用以下命令运行爬虫:

$ scrapy crawl books -o books.csv
...
$ head -10 books.csv # 查看文件开头的10 行
name,price
A Light in the Attic,£51.77
Tipping the Velvet,£53.74
Soumission,£50.10
Sharp Objects,£47.82
Sapiens: A Brief History of Humankind,£54.23
The Requiem Red,£22.65
The Dirty Little Secrets of Getting Your Dream Job,£33.34
"The Coming Woman: A Novel Based on the Life of the Infamous Feminist, Woodhull",£17.93
The Boys in the Boat: Nine Americans and Their Epic Quest for Gold at Olympics,£22.60

  其中,-o books.csv指定了导出文件的路径,在这里虽然没有使用-t参数指定导出数据格式,但Scrapy爬虫通过文件后缀名推断出我们想以csv作为导出数据格式。同样的道理,如果将参数改为-o books.json,Scrapy爬虫就会以json作为导出数据格式。
  需要明确地指定导出数据格式时,使用-t参数,例如:

$ scrapy crawl books -t csv -o books1.data
...
$ scrapy crawl books -t json -o books2.data
...
$ scrapy crawl books -t xml -o books3.data
...

  运行以上命令后,Scrapy爬虫会以-t参数中的数据格式字符串(如csv、json、xml)为键,在配置字典FEED_EXPORTERS中搜索Exporter,FEED_EXPORTERS的内容由以下两个字典的内容合并而成:
● 默认配置文件中的FEED_EXPORTERS_BASE
● 用户配置文件中的FEED_EXPORTERS

  前者包含内部支持的导出数据格式,后者包含用户自定义的导出数据格式。以下是Scrapy源码中定义的FEED_EXPORTERS_BASE,它位于scrapy.settings.default_settings模块

FEED_EXPORTERS_BASE = {
'json': 'scrapy.exporters.JsonItemExporter',
'jsonlines': 'scrapy.exporters.JsonLinesItemExporter',
'jl': 'scrapy.exporters.JsonLinesItemExporter',
'csv': 'scrapy.exporters.CsvItemExporter',
'xml': 'scrapy.exporters.XmlItemExporter',
'marshal': 'scrapy.exporters.MarshalItemExporter',
'pickle': 'scrapy.exporters.PickleItemExporter',
}

  如果用户添加了新的导出数据格式(即实现了新的Exporter),可在配置文件settings.py中定义FEED_EXPORTERS,例如:

FEED_EXPORTERS = {'excel': 'my_project.my_exporters.ExcelItemExporter'}

  另外,指定导出文件路径时,还可以使用%(name)s和%(time)s两个特殊变量:
  ● %(name)s:会被替换为Spider的名字。
  ● %(time)s:会被替换为文件创建时间。
  请看一个例子,假设一个项目中有爬取书籍信息、游戏信息、新闻信息的3个Spider,分别名为 ‘books’、‘games’、‘news’。对于任意Spider的任意一次爬取,都可以使用’export_data/%(name)s/%(time)s.csv’作为导出路径,Scrapy爬虫会依据Spider的名字和爬取的时间点创建导出文件:

$ scrapy crawl books1 -o export_data/%(name)s/%(time)s -t csv
...
$ scrapy crawl books1 -o export_data/%(name)s/%(time)s -t csv
...
$ scrapy crawl books1 -o export_data/%(name)s/%(time)s.json
...
$ cd export_data
...
$ tree /F
文件夹 PATH 列表
卷序列号为 00000200 5CD4:FD02
F:.
└─books1
        2021-11-10T06-27-40
        2021-11-10T06-39-46
        2021-11-10T06-41-55.json

  使用命令行参数指定如何导出数据很方便,但命令行参数只能指定导出文件路径以及导出数据格式,并且每次都在命令行里输入很长的参数让人很烦躁,使用配置文件可以弥补这些不足。

7.1.2 配置文件

  接下来,学习下在配置文件中指定如何导出数据。下面依次说明几个常用选项:
● FEED_URI 导出文件路径

FEED_URI = 'export_data/%(name)s.data'

● FEED_FORMAT 导出数据格式

FEED_FORMAT = 'csv'

● FEED_EXPORT_ENCODING 导出文件编码(默认情况下json文件使用数字编码,其他使用utf-8编码)。

FEED_EXPORT_ENCODING = 'gbk'

● FEED_EXPORT_FIELDS 导出数据包含的字段(默认情况下导出所有字段),并指定次序。

FEED_EXPORT_FIELDS = ['name', 'author', 'price']

● FEED_EXPORTERS 用户自定义Exporter字典,添加新的导出数据格式时使用。

FEED_EXPORTERS = {'excel':'my_project.my_exporters.ExcelItemExporter'}

7.2 添加导出数据格式

  在某些需求下,我们想要添加新的导出数据格式,此时需要实现新的Exporter类。下面先参考Scrapy内部的Exporter类是如何实现的,然后自行实现一个Exporter。

7.2.1 源码参考

   Scrapy内部的Exporter类在scrapy.exporters模块中实现,以下是其中的代码片段:

class BaseItemExporter(object):
	def __init__(self, **kwargs):
		self._configure(kwargs)
	def _configure(self, options, dont_fail=False):
		self.encoding = options.pop('encoding', None)
		self.fields_to_export = options.pop('fields_to_export', None)
		self.export_empty_fields = options.pop('export_empty_fields', False)
		self.indent = options.pop('indent', None)
		if not dont_fail and options:
			raise TypeError(f"Unexpected options: {', '.join(options.keys())}")
	def export_item(self, item):
		raise NotImplementedError
	def serialize_field(self, field, name, value):
		serializer = field.get('serializer', lambda x: x)
		return serializer(value)
	def start_exporting(self):
		pass
	def finish_exporting(self):
		pass
	def _get_serialized_fields(self, item, default_value=None, include_empty=None):
		"""Return the fields to export as an iterable of tuples
		(name, serialized_value)
		"""
		# item = ItemAdapter(item)
		
		if include_empty is None:
			include_empty = self.export_empty_fields
		if self.fields_to_export is None:
			if include_empty and not isinstance(item, dict):
				field_iter = six.iterkeys(item.fields)
			else:
				field_iter = six.iterkeys(item)
		else:
			if include_empty:
				field_iter = self.fields_to_export
			else:
				field_iter = (x for x in self.fields_to_export if x in item)
		for field_name in field_iter:
			if field_name in item:
				field = {} if isinstance(item, dict) else item.fields[field_name]
				value = self.serialize_field(field, field_name, item[field_name])
			else:
				value = default_value
			yield field_name, value
# json
class JsonItemExporter(BaseItemExporter):
	def __init__(self, file, **kwargs):
		self._configure(kwargs, dont_fail=True)
		self.file = file
		kwargs.setdefault('ensure_ascii', not self.encoding)
		self.encoder = ScrapyJSONEncoder(**kwargs)
		self.first_item = True
	def start_exporting(self):
		self.file.write(b"[\n")
	def finish_exporting(self):
		self.file.write(b"\n]")
	def export_item(self, item):
		if self.first_item:
			self.first_item = False
		else:
			self.file.write(b',\n')
			itemdict = dict(self._get_serialized_fields(item))
			data = self.encoder.encode(itemdict)
			self.file.write(to_bytes(data, self.encoding))
# json lines
class JsonLinesItemExporter(BaseItemExporter):
	...
# xml
class XmlItemExporter(BaseItemExporter):
	...
# csv
class CsvItemExporter(BaseItemExporter):
	...
...

  其中的每一个Exporter都是BaseItemExporter的一个子类,BaseItemExporter定义了一些抽象接口待子类实现:
● export_item(self, item)
  负责导出爬取到的每一项数据,参数item为一项爬取到的数据,每个子类必须实现该方法。
● start_exporting(self)
  在导出开始时被调用,可在该方法中执行某些初始化工作。
● finish_exporting(self)
  在导出完成时被调用,可在该方法中执行某些清理工作。

以JsonItemExporter为例,其实现非常简单:
● 为了使最终导出结果是一个json中的列表,在start_exporting和finish_exporting方法中分别向文件写入b"[\n, b"\n]"。
● 在export_item方法中,调用self.encoder.encode方法将一项数据转换成json串(具体细节不再赘述),然后写入文件。

7.2.2 实现Exporter

  接下来,我们参照JsonItemExporter的源码,在example项目中实现一个能将数据以Excel格式导出的Exporter。
  在项目中创建一个my_exporters.py(与settings.py同级目录),在其中实现ExcelItemExporter,代码如下:

# my_exporters.py
from scrapy.exporters import BaseItemExporter
import xlwt

class ExcelItemExporter(BaseItemExporter):
    def __init__(self, file, **kwargs):
        self._configure(kwargs)
        self.file = file
        self.wbook = xlwt.Workbook()
        self.wsheet = self.wbook.add_sheet('scrapy')
        self.row = 0

    def finish_exporting(self):
        self.wbook.save(self.file)

    def export_item(self, item):
        fields = self._get_serialized_fields(item)
        for col, v in enumerate(x for _, x in fields):
            self.wsheet.write(self.row, col, v)
        self.row += 1

解释上述代码如下:
● 这里使用 第三方库xlwt 将数据写入Excel文件。
● 在构造器方法中 创建Workbook对象和Worksheet对象 ,并初始化用来记录写入 行坐标的self.row
● 在export_item方法中调用基类的 _get_serialized_fields方法,获得item所有字段的迭代器,然后调用self.wsheet.write方法将各字段写入Excel表格。
● finish_exporting方法在所有数据都被写入Excel表格后被调用,在该方法中调用self.wbook.save方法将Excel表格写入Excel文件。
    完成ExcelItemExporter后,在配置文件settings.py中添加如下代码:

FEED_EXPORTERS = {'excel': 'example.my_exporters.ExcelItemExporter'}

    现在,可以使用ExcelItemExporter导出数据了,以-t excel为参数重新运行爬虫:

$ scrapy crawl books -t excel -o books.xls


图7-1所示为爬取完成后在Excel文件中观察到的结果。 ![图7-1]() 图7-1

   如上所示,我们成功地使用ExcelItemExporter将爬取到的数据存入了Excel文件中。

7.3 本章小结

   本章学习了在Scrapy中如何使用Exporter将爬取到的数据导出到文件,首先介绍使用命令行参数以及配置文件指定如何导出数据的方法,然后参考Scrapy内部Exporter的源码实现了一个能将数据导出到Excel文件的Exporter。

    本文参照《精通Scrapy网络爬虫+(刘硕著)》PDF,并自己跑相关代码,代码内容稍作修改,仅做参考和笔记复习使用