Python爬虫之Scrapy框架的使用(二)
一:创建爬虫
scrapy startproject ArticleSpider
cd ArticleSpider
scrapy genspider cnblogs cnblogs.com
二:settings配置文件
ROBOTSTXT_OBEY = False
DOWNLOAD_DELAY = 1
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11'
}
LOG_LEVEL = "WARNING" # 增加此行配置
三:创建启动文件
from scrapy.cmdline import execute
import sys, os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
execute(['scrapy', 'crawl', 'cnblogs'])
四:爬虫文件内容
import scrapy
from urllib import parse
import json
from ArticleSpider.items import ArticlespiderItem
from ArticleSpider.utils.common import get_url_md5
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs_bak'
allowed_domains = ['cnblogs.com']
start_urls = ['https://news.cnblogs.com/']
def parse(self, response):
# news_list = response.xpath('//div[@id="news_list"]//div[@class="news_block"]')
news_list = response.css('div#news_list div.news_block')
for div in news_list:
detail_url = div.xpath('.//h2/a/@href').get()
thumbnail_url_data = div.xpath('.//img[@class="topic_img"]/@src').get()
if not thumbnail_url_data:
thumbnail_url = []
elif thumbnail_url_data.startswith('/'):
thumbnail_url = ['https:' + thumbnail_url_data]
else:
thumbnail_url = [thumbnail_url_data]
yield scrapy.Request(url=parse.urljoin(response.url, detail_url), callback=self.parse_detail,meta={'thumbnail_url': thumbnail_url})
# 提取下一页
next_url = response.xpath('//div[@class="pager"]/a[contains(text(), "Next")]/@href').get()
yield scrapy.Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
def parse_detail(self, response):
thumbnail_url = response.meta.get('thumbnail_url', '')
title = response.xpath('//div[@id="news_title"]/a/text()').get()
author = response.xpath('//div[@id="news_info"]/span[@class="news_poster"]/a/text()').get()
pubdate = response.xpath('//div[@id="news_info"]/span[@class="time"]/text()').get()
pubdate = pubdate.replace('发布于', '').lstrip()
content = ''.join(response.xpath('//div[@id="news_body"]//*').getall())
tag_list = response.xpath('//div[@class="news_tags"]//a/text()').getall()
tags = ','.join(tag_list)
origin_url = response.url
url_hash_id = get_url_md5(origin_url)
content_id = response.xpath('.//input[@id="lbContentID"]/@value').get()
item = ArticlespiderItem(title=title, author=author, pubdate=pubdate, content=content, tags=tags,thumbnail_url=thumbnail_url, origin_url=origin_url, url_hash_id=url_hash_id)
# 不建议将再有的url请求放在该方法中,建议将url请求 yield 出去
yield scrapy.Request(parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(content_id)),callback=self.parse_json_data, meta={'item': item})
def parse_json_data(self, response):
item = response.meta.get('item')
json_data = json.loads(response.text)
item['dig_count'] = json_data.get('DiggCount')
item['comment_count'] = json_data.get('CommentCount')
item['view_count'] = json_data.get('TotalView')
yield item
五:item文件
class ArticlespiderItem(scrapy.Item):
title = scrapy.Field()
author = scrapy.Field()
pubdate = scrapy.Field()
content = scrapy.Field()
tags = scrapy.Field()
dig_count = scrapy.Field()
comment_count = scrapy.Field()
view_count = scrapy.Field()
thumbnail_url = scrapy.Field()
thumbnail_path = scrapy.Field()
origin_url = scrapy.Field()
url_hash_id = scrapy.Field()
六:pipelines文件
import codecs
import json
import pymysql
from scrapy.exporters import JsonItemExporter
from scrapy.pipelines.images import ImagesPipeline
from twisted.enterprise import adbapi
class ArticlespiderPipeline:
def process_item(self, item, spider):
return item
class ArticleImagesPipeline(ImagesPipeline):
# 图片下载的pipeline
def item_completed(self, results, item, info):
if 'thumbnail_url' in item:
thumbnail_path = ''
for ok, value in results:
thumbnail_path = value['path'] or ''
item['thumbnail_path'] = thumbnail_path
# thumbnail_path保存格式为:full/xxxx.jpg
return item
class JsonWithEncodingPipeline(object):
'''自定义导出为json文件pipeline,一行一条数据'''
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'
self.file.write(lines)
return item
def spider_close(self, spider):
self.file.close()
class JsonExporterPipeline:
'''
自定义导出为json文件pipeline,保存后的数据为一个列表
该类中默认会执行三个方法:open_spider, close_spider,process_item。
'''
def __init__(self):
self.file = open('article_export.json', 'wb')
self.exporter = JsonItemExporter(self.file, ensure_ascii=False, encoding='utf-8')
self.exporter.start_exporting()
def open_spider(self, spider):
print('start...')
def process_item(self, item, spider):
self.exporter.export_item(item)
return item
def close_spider(self, spider):
self.exporter.finish_exporting()
self.file.close()
print('finish...')
class MysqlPipeline:
'''保存数据到mysql数据库pipeline'''
def __init__(self):
self.conn = pymysql.connect(host='172.17.2.36', port=3306, user='root', password='xxxxxx',database='spider', charset='utf8')
self.cursor = self.conn.cursor()
def open_spider(self, spider):
print('start...')
def process_item(self, item, spider):
insert_sql = 'insert into cnblogs values(%s, %s, %s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'
self.cursor.execute(insert_sql, (
item.get('title', ''),
item.get('author', ''),
item.get('pubdate', '1970-07-01'),
item.get('content', ''),
item.get('origin_url', ''),
item.get('url_hash_id', ''),
item.get('tags', ''),
item.get('dig_count', 0),
item.get('comment_count', 0),
item.get('view_count', 0),
''.join(item.get('thumbnail_url', [])),
item.get('thumbnail_path', '')
))
self.conn.commit()
return item
def close_spider(self, spider):
self.cursor.close()
self.conn.close()
print('finish...')
class MysqlTwistedPipeline:
# 自定义异步保存数据到mysql数据库类
def __init__(self, dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls, settings):
# 从settings配置文件读取参数
db_params = dict(
host=settings['MYSQL_HOST'],
port=settings['MYSQL_PORT'],
database=settings['MYSQL_DATABASE'],
user=settings['MYSQL_USER'],
password=settings['MYSQL_PASSWORD'],
charset='utf8',
cursorclass=pymysql.cursors.DictCursor
)
db_pool = adbapi.ConnectionPool('pymysql', **db_params)
return cls(db_pool)
def process_item(self, item, spider):
query = self.dbpool.runInteraction(self.insert_item, item)
query.addErrback(self.handle_error, item, spider)
return item
def insert_item(self, cursor, item):
insert_sql = 'insert into cnblogs values(%s, %s, %s,%s,%s,%s,%s,%s,%s,%s,%s,%s) on duplicate key update view_count=values(view_count)'
cursor.execute(insert_sql, (
item.get('title', ''),
item.get('author', ''),
item.get('pubdate', '1970-07-01'),
item.get('content', ''),
item.get('origin_url', ''),
item.get('url_hash_id', ''),
item.get('tags', ''),
item.get('dig_count', 0),
item.get('comment_count', 0),
item.get('view_count', 0),
''.join(item.get('thumbnail_url', [])),
item.get('thumbnail_path', '')
))
def handle_error(self, error, item, spider):
print('=' * 10 + 'error' + '=' * 10)
print(error)
settings文件配置:
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
# 'scrapy.pipelines.images.ImagesPipeline': 1,
'ArticleSpider.pipelines.ArticleImagesPipeline': 1,
# 'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
# 'ArticleSpider.pipelines.JsonExporterPipeline': 3,
# 'ArticleSpider.pipelines.MysqlPipeline': 4,
'ArticleSpider.pipelines.MysqlTwistedPipeline': 5,
}
# 图片下载pipeline相关配置
IMAGES_URLS_FIELD = 'thumbnail_url'
IMAGES_STORE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'images')
# mysql数据库信息配置
MYSQL_HOST = '172.17.2.36'
MYSQL_PORT = 3306
MYSQL_DATABASE = 'spider'
MYSQL_USER = 'root'
MYSQL_PASSWORD = 'xxxxxx'
到此,运行爬虫启动文件即可爬取数据。
七:使用scrapy中的ItemLoader提取数据
当项目很大,提取的字段数以百计,那么各种提取规则会越来越多,按照这种方式来做,维护的工作将会是一场噩梦!所以scrapy就提供了ItemLoader这样一个容器,在这个容器里面可以配置item中各个字段的提取规则。可以通过函数分析原始数据,并对Item字段进行赋值,非常的便捷。
可以这么来看 Item 和 Itemloader:Item提供保存抓取到数据的容器,而 Itemloader提供的是填充容器的机制。
Itemloader提供的是一种灵活,高效的机制,可以更方便的被spider或source format (HTML, XML, etc)扩展并重写,更易于维护,尤其是分析规则特别复杂繁多的时候。
7.1 爬虫文件内容
import scrapy
from urllib import parse
import json
from scrapy.loader import ItemLoader
from ArticleSpider.items import ArticlespiderItem
from ArticleSpider.utils.common import get_url_md5
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['cnblogs.com']
start_urls = ['https://news.cnblogs.com/']
def parse(self, response):
# news_list = response.xpath('//div[@id="news_list"]//div[@class="news_block"]')
news_list = response.css('div#news_list div.news_block')
for div in news_list:
detail_url = div.xpath('.//h2/a/@href').get()
# 因为ImagesPipeline要求图片url是一个列表,所以上面的案例对该字段进行了处理,但使用itemload,这里传递一个空字符串,itemload不会报错会转换成一个空列表
thumbnail_url = div.xpath('.//img[@class="topic_img"]/@src').get()
yield scrapy.Request(url=parse.urljoin(response.url, detail_url), callback=self.parse_detail,meta={'thumbnail_url': thumbnail_url})
# 提取下一页
# next_url = response.xpath('//div[@class="pager"]/a[contains(text(), "Next")]/@href').get()
# yield scrapy.Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
def parse_detail(self, response):
# 使用item_loader提取数据。下面使用css选择器提取数据,当然也可以使用
item_loader = ItemLoader(item = ArticlespiderItem(), response = response)
# 使用item_loader提取的数据都是list类型
item_loader.add_css('title', '#news_title a::text')
item_loader.add_css('author', '.news_poster a::text')
item_loader.add_css('content', '#news_content')
item_loader.add_css('tags', '.news_tags a::text')
item_loader.add_css('pubdate', '#news_info .time::text')
item_loader.add_value('origin_url', response.url)
item_loader.add_value('thumbnail_url', response.meta.get('thumbnail_url', ''))
item_loader.add_value('url_hash_id', get_url_md5(response.url))
# 当所有数据被收集起来之后,还需要调用 ItemLoader.load_item() 方法, 实际上填充并且返回了之前通过调用 add_xpath(),add_css(),and add_value() 所提取和收集到的数据。
item = item_loader.load_item()
content_id = response.xpath('.//input[@id="lbContentID"]/@value').get()
# 不建议将url请求放在该方法中,建议将url请求 yield 出去
yield scrapy.Request(parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(content_id)),callback=self.parse_json_data, meta={'item': item})
def parse_json_data(self, response):
item = response.meta.get('item')
json_data = json.loads(response.text)
item['dig_count'] = json_data.get('DiggCount')
item['comment_count'] = json_data.get('CommentCount')
item['view_count'] = json_data.get('TotalView')
yield item
7.2 input_processor和output_processor
使用输入处理器input_processor和输出处理器output_processor对提取到的数据进行加工处理。
从上面的示例中,可以看到,存在两个问题:
第一,提取的数据,填充进去的对象都是List类型。而我们大部分的需求是要取第一个数值,取List中的第一个非空元素,那么如何实现取第一个呢?
第二,在做item字段解析时,经常需要再进一步解析,过滤出我们想要的数值,例如用正则表达式将 $10 price中的数字10提取出来。那么又如何对字段加一些处理函数呢?
input_processor and output_processor在item文件定义字段时对数据进行处理,下面举例说明用法:
from scrapy.loader.processors import MapCompose, TakeFirst
def date_convert(value):
# 这里的value传递过来的就是列表中的元素
if value:
value = re.sub('[\u4e00-\u9fa5]', '', value).strip() # 去除字符串中的中文字符
try:
create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()
return create_date
class ArticlespiderItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
author = scrapy.Field()
pubdate = scrapy.Field(input_processor=MapCompose(date_convert), output_processor=TakeFirst())
# 可以定义多个函数,按顺序写到MapCompose中
......................
TakeFirst:返回第一个非空(non-null/ non-empty)值,常用于单值字段的输出处理器,无参数。
MapCompose:与Compose处理器类似,区别在于各个函数结果在内部传递的方式(会涉及到list对象解包的步骤):
- 输入值是被迭代的处理的,List对象中的每一个元素被单独传入,第一个函数进行处理,然后处理的结果被连接起来形成一个新的迭代器,并被传入第二个函数,以此类推,直到最后一个函数。最后一个函数的输出被连接起来形成处理器的输出。
- 每个函数能返回一个值或者一个值列表,也能返回None(会被下一个函数所忽略)
- 这个处理器提供了很方便的方式来组合多个处理单值的函数。因此它常用于输入处理器,因为传递过来的是一个List对象。
Compose:用给定的多个函数的组合,来构造的处理器。list对象(注意不是指list中的元素),依次被传递到第一个函数,然后输出,再传递到第二个函数,一个接着一个,直到最后一个函数返回整个处理器的输出。默认情况下,当遇到None值(list中有None值)的时候停止处理。可以通过传递参数stop_on_none = False改变这种行为。
Identity:最简单的处理器,不进行任何处理,直接返回原来的数据。无参数。
Join:返回用分隔符连接后的值。分隔符默认为空格。不接受Loader contexts。当使用默认分隔符的时候,这个处理器等同于:u’ '.join
7.3 重用和扩展ItemLoaders
从上面的信息来看,ItemLoaders是非常灵活的,但是假设有个需求,所有的字段,我们都要去取第一个,那么如果有300个字段,我们就要添加300次,每个都要写,就会觉得很麻烦。那么有没有办法统一设置呢,答案是有的,如下:
如果想要实现每个字段都只取第一个,那么可以定义一个自己的ItemLoader类:ArticleItemLoader(继承自ItemLoader类)
我们首先可以看一下原始的 ItemLoader 的定义:
class ItemLoader:
# 可以看到是有默认的输入/输出处理器的,而且默认是什么都不做
default_item_class = Item
default_input_processor = Identity()
default_output_processor = Identity()
default_selector_class = Selector
在item文件中自定义一个ItemLoader类:
def date_convert(value):
# 这里的value传递过来的就是列表中的元素
if value:
value = re.sub('[\u4e00-\u9fa5]', '', value).strip() # 去除字符串中的中文字符
try:
pubdate = datetime.datetime.strptime(value, "%Y/%m/%d").date()
except Exception as e:
pubdate = datetime.datetime.now().date()
return pubdate
class ArticleItemLoad(ItemLoader):
default_output_processor = TakeFirst()
class ArticlespiderItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
author = scrapy.Field()
pubdate = scrapy.Field(input_processor=MapCompose(date_convert))
content = scrapy.Field()
tags = scrapy.Field(output_processor=Join(separator=','))
dig_count = scrapy.Field()
comment_count = scrapy.Field()
view_count = scrapy.Field()
thumbnail_url = scrapy.Field(output_processor=Identity())
thumbnail_path = scrapy.Field()
origin_url = scrapy.Field()
url_hash_id = scrapy.Field()
下面把爬虫文件修改全部使用自定义的ItemLoader类:
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['cnblogs.com']
start_urls = ['https://news.cnblogs.com/']
def parse(self, response):
# news_list = response.xpath('//div[@id="news_list"]//div[@class="news_block"]')
news_list = response.css('div#news_list div.news_block')
for div in news_list:
detail_url = div.xpath('.//h2/a/@href').get()
thumbnail_url_data = div.xpath('.//img[@class="topic_img"]/@src').get()
if not thumbnail_url_data:
thumbnail_url = []
elif thumbnail_url_data.startswith('/'):
thumbnail_url = ['https:' + thumbnail_url_data]
else:
thumbnail_url = [thumbnail_url_data]
yield scrapy.Request(url=parse.urljoin(response.url, detail_url), callback=self.parse_detail,meta={'thumbnail_url': thumbnail_url})
# 提取下一页
# next_url = response.xpath('//div[@class="pager"]/a[contains(text(), "Next")]/@href').get()
# yield scrapy.Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
def parse_detail(self, response):
# 使用item_loader提取数据
item_loader = ArticleItemLoad(item=ArticlespiderItem(), response=response)
item_loader.add_css('title', '#news_title a::text')
item_loader.add_css('author', '.news_poster a::text')
item_loader.add_css('content', '#news_content')
item_loader.add_css('tags', '.news_tags a::text')
item_loader.add_css('pubdate', '#news_info .time::text')
item_loader.add_value('origin_url', response.url)
item_loader.add_value('thumbnail_url', response.meta.get('thumbnail_url', ''))
item_loader.add_value('url_hash_id', get_url_md5(response.url))
# 下面的parse_json_data方法可以不使用ItemLoader
# 如果需要使用ItemLoader那么延缓使用item_loader.load_item()方法
# item = item_loader.load_item()
content_id = response.xpath('.//input[@id="lbContentID"]/@value').get()
# 不建议将url请求放在该方法中,建议将url请求 yield 出去
yield scrapy.Request(parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(content_id)), callback=self.parse_json_data, meta={'item_loader': item_loader})
def parse_json_data(self, response):
item_loader = response.meta.get('item_loader')
json_data = json.loads(response.text)
item_loader.add_value('dig_count', json_data.get('DiggCount'))
item_loader.add_value('comment_count', json_data.get('CommentCount'))
item_loader.add_value('view_count', json_data.get('TotalView'))
item = item_loader.load_item()
yield item