明确爬取网站
搭建虚拟环境
豆瓣源:https://pypi.doubanio.com/simple/
可以通过如下命令安装 Python 库,指定国内豆瓣源(加上 -i 参数和豆瓣源网址)
pip install -i https://pypi.doubanio.com/simple/ scrapy
创建项目
- 通过如下命令创建项目
scrapy startproject ArticleSpider
image.png
- 用 PyCharm 打开项目
- 创建爬虫(记得一定要先 cd 到 ArticleSpider 目录)
scrapy genspider jobbole blog.jobbole.com
image.png
注意:这地方创建爬虫时允许爬虫爬取的域设置为 blog.jobbole.com,后面爬取 Python 板块所有文章的时候改成了 jobbole.com,因为 Python 板块所有文章列表页 URL 是 http://python.jobbole.com/all-posts/,不然会被 Scrapy 过滤掉,不能爬取
- 在 PyCharm 中查看新建的爬虫
- 为了能在 PyCharm 中断点调试 Scrapy 爬虫项目,在 scrapy.cfg 同级目录(也就是项目根目录 ArticleSpider目录)下创建 main.py 文件用于启动 scrapy 项目
# main.py
import os
import sys
from scrapy.cmdline import execute
# 将当前项目目录追加到 Python 搜索模块路径
# os.path.abspath(__file__) 是当前文件
# os.path.dirname(os.path.abspath(__file__)) 是获取当前文件所在目录名
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 执行命令启动爬虫
execute(["scrapy", "crawl", 'jobbole'])
image.png
- settings.py 中关闭 robots 协议
- 现在在爬虫文件 jobbole.py 中 parse 方法内打一个断点,Debug 运行 main.py 文件,看下 response 接收到了什么
- 至此,项目创建以及初步配置已经完成
现在通过以下三个方面学习一下 XPath
image.png- XPath 简介
- XPath 术语
- XPath 语法
在 Scrapy Shell 中测试 XPath
- 随意找一篇文章详情页网址进行测试,命令行窗口运行 Scrapy Shell
scrapy shell http://blog.jobbole.com/114041/
image.png
- 测试提取文章标题
通过 XPath 提取项目中所有需要提取的文章内容
- 文章标题
- 文章发布日期
- 文章点赞数、收藏数、评论数
- 文章内容
- 文章标签
- XPath 提取数据完整代码
def parse(self, response):
title = response.xpath('//div[@class="entry-header"]/h1/text()').extract_first()
create_date = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/text()').extract_first().replace("·", "").strip()
praise_nums = response.xpath('//span[contains(@chass, vote-post-up)]/h10/text()').extract_first()
fav_nums = response.xpath('//div[@class="post-adds"]//span[2]/text()').extract_first()
re_find = re.findall('.*?(\d+).*', fav_nums)
if re_find:
fav_nums = re_find[0]
comment_nums = response.xpath('//div[@class="post-adds"]//a[@href="#article-comment"]/span/text()').extract_first()
re_find = re.findall('.*?(\d+).*', comment_nums)
if re_find:
comment_nums = re_find[0]
content = response.xpath('//div[@class="entry"]').extract_first()
tag_list = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/a/text()').extract()
tag_list = [tag for tag in tag_list if not tag.strip().endswith('评论')]
tags = ','.join(tag_list)
css 选择器
image.png image.png image.png- scrapy shell 下测试 css 选择器
- CSS 选择器提取数据完整代码
def parse(self, response):
# 通过 CSS 选择器提取字段信息
title = response.css('div.entry-header > h1::text').extract_first()
create_date = response.css('p.entry-meta-hide-on-mobile::text').extract_first().replace("·", "").strip()
praise_nums = response.css('div.post-adds > span.vote-post-up > h10::text').extract_first()
fav_nums = response.css('div.post-adds > span.bookmark-btn::text').extract_first()
re_find = re.findall('.*?(\d+).*', fav_nums)
if re_find:
fav_nums = re_find[0]
comment_nums = response.css('div.post-adds > a[href="#article-comment"] span::text').extract_first()
re_find = re.findall('.*?(\d+).*', comment_nums)
if re_find:
comment_nums = re_find[0]
content = response.css('div.entry').extract_first()
tag_list = response.css('p.entry-meta-hide-on-mobile a::text').extract()
tag_list = [tag for tag in tag_list if not tag.strip().endswith('评论')]
tags = ','.join(tag_list)
编写爬虫文件 spiders/jobbole.py
分析如何进行所有文章爬取
image.png image.png这里以爬取 Python 分类下全部文章为例,进行爬取
URL:http://python.jobbole.com/all-posts/
- 文章列表页,共有 82 页,每一页共有 20 篇文章,如果有下一页则会有下一页按钮,所以爬虫要做的就是在每一个列表页获取 20 篇文章详情页链接,和下一页的链接
- 爬虫代码如下
整体思路:从文章列表页第一页 http://python.jobbole.com/all-posts/ 开始爬,通过 parse 方法进行解析,parse 方法做两件事:①提取出 20 篇文章的详情页 URL 链接,依次交给 parse_detail 方法进行解析,提取字段数据;②提取下一页 URL 链接,再次交给 parse 方法进行解析
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['jobbole.com']
start_urls = ['http://python.jobbole.com/all-posts/'] # http://blog.jobbole.com/114041/
def parse(self, response):
"""
1. 提取文章列表页中所有文章详情页链接,并交给 parse_detail 方法进行解析
2. 提取下一页链接,并交给 Scrapy 进行下载
Args:
response: 响应信息
Yields:
1. 文章详情页链接,交给 parse_detail 解析
2. 下一页链接,交给 Scrapy 下载
"""
post_nodes = response.xpath('//div[@id="archive"]')
for post_node in post_nodes:
post_url = post_node.xpath('.//div[@class="post-meta"]//a[@class="archive-title"]/@href').extract_first('')
front_img_url = post_node.xpath('.//div[@class="post-thumb"]//img/@src').extract_first('')
yield scrapy.Request(url=urljoin(response.url, post_url), callback=self.parse_detail,
meta={'front_img_url': front_img_url})
next_url = response.xpath('//a[@class="next page-numbers"]/@href').extract_first()
if next_url:
yield scrapy.Request(url=next_url, callback=self.parse)
def parse_detail(self, response):
"""
1. 解析文章详情页页面信息
2. 将解析出来的数据交给 pipelines 文件进行处理
Args:
response: 响应信息
Yields:
item
"""
# 通过 XPath 提取字段信息
title = response.xpath('//div[@class="entry-header"]/h1/text()').extract_first()
create_date = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/text()').extract_first().replace("·", "").strip()
praise_nums = response.xpath('//span[contains(@chass, vote-post-up)]/h10/text()').extract_first()
fav_nums = response.xpath('//div[@class="post-adds"]//span[2]/text()').extract_first()
re_find = re.findall('.*?(\d+).*', fav_nums)
if re_find:
fav_nums = int(re_find[0])
else:
fav_nums = 0
comment_nums = response.xpath('//div[@class="post-adds"]//a[@href="#article-comment"]/span/text()').extract_first()
re_find = re.findall('.*?(\d+).*', comment_nums)
if re_find:
comment_nums = int(re_find[0])
else:
comment_nums = 0
content = response.xpath('//div[@class="entry"]').extract_first()
tag_list = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/a/text()').extract()
tag_list = [tag for tag in tag_list if not tag.strip().endswith('评论')]
tags = ','.join(tag_list)
将爬取数据保存到 JSON 文件
- 先列出完整的
jobbole.py
代码
# ArticleSpider/spiders/jobbole.py
# -*- coding: utf-8 -*-
import re
from urllib.parse import urljoin
import scrapy
from ArticleSpider.items import JobBoleArticleItem
from ArticleSpider.utils.common import get_md5
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['jobbole.com']
start_urls = ['http://python.jobbole.com/all-posts/'] # http://blog.jobbole.com/114041/
def parse(self, response):
"""
1. 提取文章列表页中所有文章详情页链接,并交给 parse_detail 方法进行解析
2. 提取下一页链接,并交给 Scrapy 进行下载
Args:
response: 响应信息
Yields:
1. 文章详情页链接,交给 parse_detail 解析
2. 下一页链接,交给 Scrapy 下载
"""
post_nodes = response.xpath('//div[@id="archive"]')
for post_node in post_nodes:
post_url = post_node.xpath('.//div[@class="post-meta"]//a[@class="archive-title"]/@href').extract_first('')
front_img_url = post_node.xpath('.//div[@class="post-thumb"]//img/@src').extract_first('')
yield scrapy.Request(url=urljoin(response.url, post_url), callback=self.parse_detail,
meta={'front_img_url': front_img_url})
next_url = response.xpath('//a[@class="next page-numbers"]/@href').extract_first()
if next_url:
yield scrapy.Request(url=next_url, callback=self.parse)
def parse_detail(self, response):
"""
1. 解析文章详情页页面信息
2. 将解析出来的数据交给 pipelines 文件进行处理
Args:
response: 响应信息
Yields:
item
"""
item = JobBoleArticleItem()
# # 通过 XPath 提取字段信息
# title = response.xpath('//div[@class="entry-header"]/h1/text()').extract_first()
# create_date = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/text()').extract_first().replace("·", "").strip()
# praise_nums = response.xpath('//span[contains(@chass, vote-post-up)]/h10/text()').extract_first()
# fav_nums = response.xpath('//div[@class="post-adds"]//span[2]/text()').extract_first()
# re_find = re.findall('.*?(\d+).*', fav_nums)
# if re_find:
# fav_nums = int(re_find[0])
# else:
# fav_nums = 0
# comment_nums = response.xpath('//div[@class="post-adds"]//a[@href="#article-comment"]/span/text()').extract_first()
# re_find = re.findall('.*?(\d+).*', comment_nums)
# if re_find:
# comment_nums = int(re_find[0])
# else:
# comment_nums = 0
# content = response.xpath('//div[@class="entry"]').extract_first()
# tag_list = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/a/text()').extract()
# tag_list = [tag for tag in tag_list if not tag.strip().endswith('评论')]
# tags = ','.join(tag_list)
# 通过 CSS 选择器提取字段信息
title = response.css('div.entry-header > h1::text').extract_first()
create_date = response.css('p.entry-meta-hide-on-mobile::text').extract_first().replace("·", "").strip()
praise_nums = response.css('div.post-adds > span.vote-post-up > h10::text').extract_first()
fav_nums = response.css('div.post-adds > span.bookmark-btn::text').extract_first()
re_find = re.findall('.*?(\d+).*', fav_nums)
if re_find:
fav_nums = int(re_find[0])
else:
fav_nums = 0
comment_nums = response.css('div.post-adds > a[href="#article-comment"] span::text').extract_first()
re_find = re.findall('.*?(\d+).*', comment_nums)
if re_find:
comment_nums = int(re_find[0])
else:
comment_nums = 0
content = response.css('div.entry').extract_first()
tag_list = response.css('p.entry-meta-hide-on-mobile a::text').extract()
tag_list = [tag for tag in tag_list if not tag.strip().endswith('评论')]
tags = ','.join(tag_list)
item['title'] = title
item['create_date'] = create_date
item['url'] = response.url
item['url_object_id'] = get_md5(response.url)
item['front_img_url'] = [response.meta.get('front_img_url')]
item['praise_nums'] = praise_nums
item['comment_nums'] = comment_nums
item['fav_nums'] = fav_nums
item['tags'] = tags
item['content'] = content
yield item
定义 ITEM
# ArticleSpider/items.py
import scrapy
class JobBoleArticleItem(scrapy.Item):
title = scrapy.Field() # 文章标题
create_date = scrapy.Field() # 文章发布日期
url = scrapy.Field() # 文章 URL
url_object_id = scrapy.Field() # 文章 URL 的 MD5 值
front_img_url = scrapy.Field() # 文章封面图(文章列表页显示的封面图,通常是文章详情第一张图片)
front_img_path = scrapy.Field() # 文章封面图存放路径
praise_nums = scrapy.Field() # 点赞数
comment_nums = scrapy.Field() # 评论数
fav_nums = scrapy.Field() # 收藏数
tags = scrapy.Field() # 文章标签
content = scrapy.Field() # 文章内容
处理 url_object_id 字段
url_object_id 字段用来存储文章 URL 的 MD5 值,所以要单独写一个函数用来获取 URL 的 MD5 值
- 在 ArticleSpider/ 目录下新建 utils/ 目录(Python Package),在 utils/ 目录下新建 common.py 文件,在 common.py 中写获取 URL 的 MD5 值的函数
import hashlib
def get_md5(url):
if isinstance(url, str):
url = url.encode('utf-8')
md5 = hashlib.md5()
md5.update(url)
return md5.hexdigest()
if __name__ == '__main__':
md5 = get_md5('http://python.jobbole.com/89187/')
print(md5)
image.png
- 在 spiders/jobbole.py 中只需要引入 get_md5 函数,并通过 get_md5 获取 URL 的 MD5 值即可
from ArticleSpider.utils.common import get_md5
item['url_object_id'] = get_md5(response.url)
处理 front_img_url 和 front_img_path 字段
这两个字段分别用来存储文章列表页的文章封面图 URL,以及图片存储到本地后的地址
- 处理 front_img_url
image.png上面爬虫中文件中 parse 方法中 meta={'front_img_url': front_img_url} 信息就是用来传递 封面图 URL 的,因为 front_img_url 图片在文章列表页,而不是在文章详情页,而 parse 方法就是解析文章列表页的数据,所以先在 parse 方法中得到 front_img_url,然后通过Scrapy 解析方法间传递数据的办法,即通过在scrapy.Request 中加入 meta 将数据传递给 callback 方法 parse_detail,在 parse_detail 方法中接收 front_img_url 即可
在 parse_detail 方法中接收 front_img_url
item['front_img_url'] = response.meta.get('front_img_url')
处理 front_img_path 字段
处理 front_img_path 字段分两步,①下载图片,②将图片路径信息存储到这个字段
利用 Scrapy 自带的图片处理管道文件即可下载图片
下载图片到本地
①在 ArticleSpider/ 目录下新建 images/ 目录用于存储图片
②配置 settings.py
文件
# ArticleSpider/settings.py
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
'scrapy.pipelines.images.ImagesPipeline': 100, # 添加图片处理 pipeline
}
# 获取项目目录
project_dir = os.path.dirname(os.path.abspath(__file__))
# 指定需要下载的图片字段,这个字段必须是可迭代对象,这里 front_img_url 就是图片字段名
IMAGES_URLS_FIELD = 'front_img_url'
# 指定图片存储路径,这里将项目目录与 images 目录进行拼接,作为图片存储路径
IMAGES_STORE = os.path.join(project_dir, 'images')
③将 front_img_url
字段的值改成列表,注意,要想使用 Scrapy 自动下载图片,在配置好 settings.py
文件后,需要下载的字段的值的类型一定要是可迭代的才可以,这里设为列表,即使只有一个图片 URL 链接,也要设置为列表,不然 Scrapy 下载图片的时候会报错
item['front_img_url'] = [response.meta.get('front_img_url')]
④安装 Pillow 库(Scrapy 下载图片依赖这个库)
pip install -i https://pypi.doubanio.com/simple/ Pillow
⑤运行项目已经可以下载图片了,图片被存储到 ArticleSpider/images/full 目录下,full/ 目录是 Scrapy 自动帮我们生成的,并且图片都是以 MD5 值来命名的
image.png提取下载后图片本地路径
-
查看
image.pngscrapy.pipelines.images.ImagesPipeline
源码,发现图片路径可以在item_completed
函数的results
变量中获得
-
自定义管道文件处理类
ArticleImagePipeline
继承自scrapy.pipelines.images.ImagesPipeline
,用来处理图片下载,并重写父类的item_completed
方法,以获取图片路径,并保存到字段 front_img_path 中
①定义ArticleImagePipeline
类
# ArticleSpider/pipelines.py
from scrapy.pipelines.images import ImagesPipeline
class ArticleImagePipeline(ImagesPipeline):
def item_completed(self, results, item, info):
for ok, v in results:
image_file_path = v['path']
item['front_img_path'] = image_file_path
return item
image.png
②配置 settings.py
文件,注释掉 scrapy.pipelines.images.ImagesPipeline
,添加自定义管道文件 ArticleSpider.pipelines.ArticleImagePipeline
# ArticleSpider/settings.py
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
# 'scrapy.pipelines.images.ImagesPipeline': 100, # 添加图片处理 pipeline
'ArticleSpider.pipelines.ArticleImagePipeline': 100,
}
③通过断点调试发现图片路径已经存储进来了,因为 ArticleSpider.pipelines.ArticleImagePipeline
执行顺序值设定的是 100,ArticleSpider.pipelines.ArticlespiderPipeline
执行顺序值设定的是 300,所以程序运行到这里的时候 item['front_img_path']
已经被成功赋值
所有字段信息全部可以正确提取,现在就可以将数据存储到 JSON 文件了
- 还是通过管道文件还处理,自定义
JsonWithEncodingPipeline
类,用来将数据存储到 JSON 文件
①pipelines.py 完整代码
# ArticleSpider/pipelines.py
import json
from scrapy.pipelines.images import ImagesPipeline
class ArticlespiderPipeline(object):
def process_item(self, item, spider):
return item
class ArticleImagePipeline(ImagesPipeline):
def item_completed(self, results, item, info):
for ok, v in results:
image_file_path = v['path']
item['front_img_path'] = image_file_path
return item
class JsonWithEncodingPipeline(object):
def __init__(self):
self.file = open('article.json', 'a', encoding='utf-8')
def process_item(self, item, spider):
self.file.write(json.dumps(dict(item), ensure_ascii=False) + '\n')
return item
def close_spider(self):
self.file.close()
② settings.py
配置
# ArticleSpider/settings.py
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
# 'scrapy.pipelines.images.ImagesPipeline': 100, # 添加图片处理 pipeline
'ArticleSpider.pipelines.ArticleImagePipeline': 100,
'ArticleSpider.pipelines.JsonWithEncodingPipeline': 200,
}
- 现在运行爬虫,图片下载成功,数据存入
article.json
文件
将数据存储到 MySQL
数据库设计
image.png image.pngpraise_nums / comment_nums / fav_nums 3 个字段设置默认值为 0,url_object_id 字段设为主键,content 字段为 longtext 类型
存储数据到 MySQL
- 需要修改
jobbole.py
中的create_date
字段,通过 CSS 选择器提取出来的时字符串,数据库表结构里面这个字段设计的时 datetime 类型,所以要将create_date
字段转换成 datetime 类型
# ArticleSpider/spiders/jobbole.py
create_date = response.css('p.entry-meta-hide-on-mobile::text').extract_first().replace("·", "").strip()
try:
create_date = datetime.strptime(create_date, '%Y/%m/%d')
except Exception as e:
create_date = datetime.now().date()
- 定义管道文件
MySQLPipeline
用来将数据存储到 MySQL
# ArticleSpider/pipelines.py
import pymysql
class MySQLPipeline(object):
def __init__(self):
self.conn = pymysql.connect('127.0.0.1', 'pythonic', 'pythonic', 'Articles', charset='utf8')
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
insert_sql = '''
insert into jobbole_article(title,create_date,url,url_object_id,front_image_url,
front_image_path,praise_nums,comment_nums,fav_nums,tags,content)
values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
'''
self.cursor.execute(insert_sql, (
item['title'], item['create_date'], item['url'], item['url_object_id'],
item['front_img_url'], item['front_img_url'], item['praise_nums'],
item['comment_nums'], item['fav_nums'], item['tags'], item['content']
))
self.conn.commit()
def close_spider(self):
self.cursor.close()
self.conn.close()
- 配置
settings.py
文件
这里注释掉存储 JSON 文件的管道 JsonWithEncodingPipeline,如果不注释掉是会报错的,因为现在 create_date 字段是 datetime 类型,而不再是 字符串,是不能转换成 JSON 的
# ArticleSpider/settings.py
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
# 'scrapy.pipelines.images.ImagesPipeline': 100, # 添加图片处理 pipeline
'ArticleSpider.pipelines.ArticleImagePipeline': 100,
# 'ArticleSpider.pipelines.JsonWithEncodingPipeline': 200,
'ArticleSpider.pipelines.MySQLPipeline': 200,
}
- 运行爬虫,数据可以成功存入 MySQL
利用 Twisted 将数据异步插入到 MySQL
为什么要使用 Twisted?因为爬虫爬取页面信息的速度远大于数据库存储的速度,而仅仅使用 connect / cursor 进行数据库的写入操作,是同步的操作做,数据没有 commit 写入数据库之前,后面代码就不会执行,阻塞状态,直到一次数据全部成功写入数据库,才会继续执行代码,所以出现爬虫等待数据写入的过程,拖累效率
Twisted 是一个异步框架,使用 Twisted 就可以将数据异步插入数据库
- 定义管道文件
MySQLTwistedPipeline
用来将数据 异步 存储到 MySQL
# ArticleSpider/pipelines.py
import pymysql
from twisted.enterprise import adbapi
class MySQLTwistedPipeline(object):
def __init__(self, dbpool):
self.dbpool = dbpool
# Scrapy 提供了一个类方法可以直接获取 settings.py 文件中的配置信息
@classmethod
def from_settings(cls, settings):
db_params = dict(
host=settings['MYSQL_HOST'],
user=settings['MYSQL_USER'],
password=settings['MYSQL_PASSWORD'],
database=settings['MYSQL_DATABASE'],
port=settings['MYSQL_PORT'],
charset='utf8',
cursorclass=pymysql.cursors.DictCursor,
)
# Twister 只是提供了一个异步的容器,并没有提供数据库连接,所以连接数据库还是要用 pymysql 进行连接
# adbapi 可以将 MySQL 的操作变为异步
# ConnectionPool 第一个参数是我们连接数据库所使用的 库名,这里是连接 MySQL 用的 pymysql
# 第二个参数就是 pymysql 连接操作数据库所需的参数,这里将参数组装成字典 db_params,当作关键字参数传递进去
dbpool = adbapi.ConnectionPool('pymysql', **db_params)
return cls(dbpool)
def process_item(self, item, spider):
# 使用 Twisted 将 MYSQL 插入变成异步
# 执行 runInteraction 方法的时候会返回一个 query 对象,专门用来处理异常
query = self.dbpool.runInteraction(self.do_insert, item)
# 添加错误处理方法到 query 对象
# addErrback 第一个参数是处理异常的方法,后面的参数是这个方法所需的参数
# 因为定义的 handle_error 方法需要接收 item、spider 参数,所以这里需要传递
query.addErrback(self.handle_error, item, spider)
def do_insert(self, cursor, item):
# 执行具体的插入操作
# 这里已经不需要手动 commit 了,Twisted 会自动 commit
insert_sql = '''
insert into jobbole_article(title,create_date,url,url_object_id,front_image_url,
front_image_path,praise_nums,comment_nums,fav_nums,tags,content)
values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
'''
cursor.execute(insert_sql, (
item['title'], item['create_date'], item['url'], item['url_object_id'],
item['front_img_url'], item['front_img_url'], item['praise_nums'],
item['comment_nums'], item['fav_nums'], item['tags'], item['content']
))
def handle_error(self, failure, item, spider):
# 异常处理方法,处理异步插入数据库时产生的异常
# failure 参数不需要我们自己传递,出现异常会自动将异常当作这个参数传递进来
print(f'出现异常:{failure}')
- 配置
settings.py
文件
# ArticleSpider/settings.py
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
# 'scrapy.pipelines.images.ImagesPipeline': 100, # 添加图片处理 pipeline
'ArticleSpider.pipelines.ArticleImagePipeline': 100,
# 'ArticleSpider.pipelines.JsonWithEncodingPipeline': 200,
# 'ArticleSpider.pipelines.MySQLPipeline': 200,
'ArticleSpider.pipelines.MySQLTwistedPipeline': 200,
}
# MySQL 配置
MYSQL_HOST = '127.0.0.1'
MYSQL_PORT = 3306
MYSQL_USER = 'pythonic'
MYSQL_PASSWORD = 'pythonic'
MYSQL_DATABASE = 'Articles'
- 运行爬虫,测试数据是否可以成功存入 MySQL
image.png image.png image.png发现即使出现异常,程序会继续往下执行,数据可以成功插入
ItemLoader
修改 jobbole.py 的 parse_detail 方法
# ArticleSpider/spiders/jobbole.py
import scrapy
from scrapy.loader import ItemLoader # 首先导入 ItemLoader
from ArticleSpider.items import JobBoleArticleLoadItem
# from ArticleSpider.items import JobBoleArticleItem
from ArticleSpider.utils.common import get_md5
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['jobbole.com']
start_urls = ['http://python.jobbole.com/all-posts/'] # http://blog.jobbole.com/114041/
def parse(self, response):
...
def parse_detail(self, response):
"""
1. 解析文章详情页页面信息
2. 将解析出来的数据交给 pipelines 文件进行处理
Args:
response: 响应信息
Yields:
item
"""
# 通过 ItemLoader 加载 Item
front_img_url = [response.meta.get('front_img_url')]
# ItemLoader 实际上提供了一个容器,有两个主要参数,item 参数是要实例化的 item 类,response 参数是爬虫返回的 response
item_loader = ItemLoader(item=JobBoleArticleLoadItem(), response=response)
# ItemLoader 提供了三种方式来加载字段,XPath、CSS 选择器 或者直接将之添加进来
# 第一个参数是字段名,第二个参数是字段值
item_loader.add_xpath('title', '//div[@class="entry-header"]/h1/text()')
item_loader.add_xpath('create_date', '//p[@class="entry-meta-hide-on-mobile"]/text()')
item_loader.add_value('url', response.url)
item_loader.add_value('url_object_id', get_md5(response.url))
item_loader.add_value('front_img_url', front_img_url)
item_loader.add_css('praise_nums', 'div.post-adds > span.vote-post-up > h10::text')
item_loader.add_css('comment_nums', 'div.post-adds > a[href="#article-comment"] span::text')
item_loader.add_css('fav_nums', 'div.post-adds > span.bookmark-btn::text')
item_loader.add_css('tags', 'p.entry-meta-hide-on-mobile a::text')
item_loader.add_css('content', 'div.entry')
item = item_loader.load_item()
yield item
修改 items.py
Scrapy 的 item 字段并没有 Django 的丰富,事实上它只提供了一种字段类型 scrapy.Field(),但其实 scrapy 还是提供了一些处理方法在里面,每个字段都有一个 input_processor 方法,和一个 output_processor 方法,可以对字段数据进行处理
常用方法:
- MapCompose:接收无限个函数,会将字段中的值依次作用于各个函数
- TakeFirst:返回列表中第一个非空值,和 extract_first() 效果一样
- Join:将列表中各个值拼接成字符串,同 python 的 join 内建函数
# ArticleSpider/items.py
from scrapy.loader.processors import MapCompose, TakeFirst, Join
def add_jobbole(value):
return f'{value} --jobbole'
class JobBoleArticleLoadItem(scrapy.Item):
title = scrapy.Field(
# 两个函数会依次作用于 title 字段中的每一个值,可以看到函数是支持 lambda 表达式
input_processor=MapCompose(add_jobbole, lambda x: x+'[article]'),
output_processor=TakeFirst()
)
...
image.png断点调试可以看到两个函数都生效了,依次作用于 title 字段
这样,所有的字段处理逻辑其实都可以写在 items.py 文件中,而爬虫 jobbole.py 文件 中 parse_detail 函数只是提取数据,不写具体数据清洗的逻辑,这样代码更加简洁合理。
但是还有一个问题,其实之前不用ItemLoader
的代码也可以看出,每次提取字段都要写一次 extract_first(),比较麻烦,而且现在用了ItemLoader
其实还是每个字段写了一遍,output_processor=TakeFirst()。为了解决这个问题,可以自定义ItemLoader
类,并且继承自ItemLoader
,定义一个默认的 output_processor 方法,让其默认等于 TakeFirst(),就可以了,如果我们不想在其中某一个字段使用此方法,就可以在这个字段中覆写 output_processor 方法,这样就屏蔽掉了默认 output_processor=TakeFirst(),而是用自定义的方法
# ArticleSpider/items.py
from scrapy.loader import ItemLoader
class ArticleItemLoader(ItemLoader):
"""
自定义 ItemLoader,继承自 Scrapy 的 ItemLoader
来改变 ItemLoader 的默认 output_processor 方法
"""
default_output_processor = TakeFirst()
- 已经自定义了
ItemLoader
类,所以现在在 jobbole.py 中实例化ItemLoader
的时候就要使用自定义的ItemLoader
类ArticleItemLoader
# -*- coding: utf-8 -*-
import re
from urllib.parse import urljoin
from datetime import datetime
import scrapy
# from scrapy.loader import ItemLoader # 不在使用 scrapy 提供的 ItemLoader,而是使用自定义的 ArticleItemLoader
from ArticleSpider.items import JobBoleArticleLoadItem, ArticleItemLoader
from ArticleSpider.items import JobBoleArticleItem
from ArticleSpider.utils.common import get_md5
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['jobbole.com']
start_urls = ['http://python.jobbole.com/all-posts/'] # http://blog.jobbole.com/114041/
def parse(self, response):
...
def parse_detail(self, response):
# 通过 ItemLoader 加载 Item
front_img_url = [response.meta.get('front_img_url')]
# 这里改用自定义的 ArticleItemLoader
item_loader = ArticleItemLoader(item=JobBoleArticleLoadItem(), response=response)
item_loader.add_xpath('title', '//div[@class="entry-header"]/h1/text()')
item_loader.add_xpath('create_date', '//p[@class="entry-meta-hide-on-mobile"]/text()')
item_loader.add_value('url', response.url)
item_loader.add_value('url_object_id', get_md5(response.url))
item_loader.add_value('front_img_url', front_img_url)
item_loader.add_css('praise_nums', 'div.post-adds > span.vote-post-up > h10::text')
item_loader.add_css('comment_nums', 'div.post-adds > a[href="#article-comment"] span::text')
item_loader.add_css('fav_nums', 'div.post-adds > span.bookmark-btn::text')
item_loader.add_css('tags', 'p.entry-meta-hide-on-mobile a::text')
item_loader.add_css('content', 'div.entry')
item = item_loader.load_item()
yield item
- items.py 完整代码
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import re
from datetime import datetime
import scrapy
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose, TakeFirst, Join
def convert_date(value):
"""
将字符串转换成日期
"""
try:
return datetime.strptime(value, '%Y/%m/%d')
except Exception as e:
return datetime.now().date()
def take_nums(value):
"""
提取数字
"""
re_find = re.findall('.*?(\d+).*', value)
if re_find:
return int(re_find[0])
else:
return 0
def remove_tags_comment(value):
"""
移除 tags 中的评论
"""
return '' if '评论' in value else value
class ArticleItemLoader(ItemLoader):
"""
自定义 ItemLoader,继承自 Scrapy 的 ItemLoader
来改变 ItemLoader 的默认 output_processor 方法
"""
default_output_processor = TakeFirst()
class JobBoleArticleLoadItem(scrapy.Item):
title = scrapy.Field()
create_date = scrapy.Field(
input_processor=MapCompose(convert_date)
)
url = scrapy.Field()
url_object_id = scrapy.Field()
front_img_url = scrapy.Field(
# 因为 使用 scrapy 自带的下载图片管道图片字段必须是可迭代的
# 所以这里不再使用默认的 output_processor=TakeFirst()
# 而是通过 lambda 表达式返回一个列表,因为本来传进来
# 就是一个列表,所以什么也不需要做,只需要直接将值重新返回
output_processor=MapCompose(lambda x: x)
)
front_img_path = scrapy.Field()
praise_nums = scrapy.Field(
input_processor=MapCompose(take_nums)
)
comment_nums = scrapy.Field(
input_processor=MapCompose(take_nums)
)
fav_nums = scrapy.Field(
input_processor=MapCompose(take_nums)
)
tags = scrapy.Field(
input_processor=MapCompose(remove_tags_comment),
output_processor=Join(',')
)
content = scrapy.Field()
网友评论