文章目录
第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
如上所示,我们成功地使用ExcelItemExporter将爬取到的数据存入了Excel文件中。
7.3 本章小结
本章学习了在Scrapy中如何使用Exporter将爬取到的数据导出到文件,首先介绍使用命令行参数以及配置文件指定如何导出数据的方法,然后参考Scrapy内部Exporter的源码实现了一个能将数据导出到Excel文件的Exporter。