scrapy
[TOC]
开始
scrapy安装
首先手动安装windows版本的Twisted
https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
pip install Twisted-18.4.0-cp36-cp36m-win_amd64.whl
安装scrapy
pip install -i https://pypi.douban.com/simple/ scrapy
windows系统额外需要安装pypiwin32
pip install -i https://pypi.douban.com/simple pypiwin32
新建项目
开始一个项目
E:\svnProject> scrapy startproject TestSpider
生成一个新的爬虫(generate)
E:\svnProject> cd TestSpider
E:\svnProject\TestSpider> scrapy genspider dongfeng www.dongfe.com
启动一个爬虫
E:\svnProject\TestSpider> scrapy crawl dongfeng
SHELL模式
> scrapy shell http://www.dongfe.com/ # 命令行调试该网页
pycharm调试启动文件
E:\svnProject\TestSpider> vim main.py
import sys
import os
from scrapy.cmdline import execute
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR)
# scrapy crawl dongfeng
execute(["scrapy", "crawl", "dongfeng"])
项目基本配置
E:\svnProject\TestSpider\TestSpider> vim settings.py
ROBOTSTXT_OBEY = False # 不要遵循网站robots文件
XPATH
表达式
说明
/body
选出当前选择器的根元素body
/body/div
选取当前选择器文档的根元素body的所有div子元素
/body/div[1]
选取body根元素下面第一个div子元素
/body/div[last()]
选取body根元素下面最后一个div子元素
/body/div[last()-1]
选取body根元素下面倒数第二个div子元素
//div
选取所有div子元素(不论出现在文档任何地方)
body//div
选取所有属于body元素的后代的div元素(不论出现在body下的任何地方)
/body/@id
选取当前选择器文档的根元素body的id属性
//@class
选取所有元素的class属性
//div[@class]
选取所有拥有class属性的div元素
//div[@class='bold']
选取所有class属性等于bold的div元素
//div[contains(@class,'bold')]
选取所有class属性包含bold的div元素
/div/*
选取当前文档根元素div的所有子元素
//*
选取文档所有节点
//div[@*]
获取所有带属性的div元素
//div/a | //div/p
选取所有div元素下面的子元素a和子元素p(并集)
//p[@id='content']/text()
选取id为content的p标签的内容(子元素的标签和内容都不会获取到)
> 注意: XPATH在选择时,参考的是HTML源码,而不是JS加载后的HTML代码
操作例子
title_selector = response.xpath("//div[@class='entry-header']/h1/text()")
title_str = title_selector.extract()[0]
CSS选择器
表达式
说明
*
选择所有节点
#container
选择Id为container的节点
.container
选取所有包含container类的节点
li a
选取所有li下的所有后代a元素(子和孙等所有的都会选中)
ul + p
选取ul后面的第一个相邻兄弟p元素
div#container > ul
选取id为container的div的所有ul子元素
ul ~ p
选取与ul元素后面的所有兄弟p元素
a[title]
选取所有有title属性的a元素
选取所有href属性等于http://taobao.com的a元素
a[href*='taobao']
选取所有href属性包含taobao的a元素
a[href^='http']
选取所有href属性开头为http的a元素
a[href$='.com']
选取所有href属性结尾为.com的a元素
input[type=radio]:checked
选取选中的radio的input元素
div:not(#container)
选取所有id非container的div元素
li:nth-child(3)
选取第三个li元素
tr:nth-child(2n)
选取偶数位的tr元素
a::attr(href)
获取所有a元素的href属性值
操作例子
h1_selector = response.css(".container h1::text") # 选取h1标题的内容
h1_str = h1_selector.extract_first() # 取出数组第一个,如果没有为空
爬虫
爬取某网站文章列表例子
>>> vim ArticleSpider/spiders/jobbole.py
import scrapy
from scrapy.http import Request
from urllib import parse
import re
from ArticleSpider.items import ArticleItem
from ArticleSpider.utils.common import get_md5 # url转md5
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/']
def parse(self, response):
"""
文章列表页的文章链接解析
:param response:
:return:
"""
css = "#archive > .post > .post-thumb > a"
article_urls_selector = response.css(css) # 获取当前列表页所有文章的链接
for article_url_selector in article_urls_selector:
head_img_url = article_url_selector.css("img::attr(src)").extract_first() # 封面URL
head_img_full_url = parse.urljoin(response.url, head_img_url) # 封面图片完整URL
article_url = article_url_selector.css("a::attr(href)").extract_first("") # 文章URL
article_full_url = parse.urljoin(response.url, article_url) # 智能的拼接URL,相对地址直接对接;绝对地址只取出域名对接;完全地址不对接,直接获取。
yield Request(url=article_full_url, callback=self.article_parse, meta={"head_img_full_url": head_img_full_url}) # 请求文章详情页并设置回调函数解析内容和meta传参
next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
if next_url:
yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse) # 下一页文章列表使用递归
def article_parse(self, response):
"""
文章详情页的内容解析
:param response:
:return:
"""
title = response.css(".grid-8 .entry-header > h1::text").extract_first("") # 标题内容
add_time = response.css(".grid-8 .entry-meta p::text").extract_first("")
add_time_match = re.match("[\s\S]*?(\d{2,4}[/-]\d{1,2}[/-]\d{1,2})[\s\S]*", add_time)
if add_time_match:
add_time = add_time_match.group(1)
else:
add_time = add_time.strip()
content = response.css(".grid-8 .entry").extract_first("") # 文章内容
star = response.css("h10::text").extract_first("") # 点赞数
head_img_url = response.meta.get("head_img_full_url") # 封面URL,通过上一个解释器在回调时传参得到的数据
# 把数据整理到item
article_item = ArticleItem() # 实例化一个item
article_item["title"] = title
article_item["content"] = content
# 把时间字符串转为可保存mysql的日期对象
try:
add_time = datetime.datetime.strptime(add_time, "%Y/%m/%d").date()
except Exception as e:
add_time = datetime.datetime.now().date()
article_item["add_time"] = add_time
article_item["star"] = star
article_item["head_img_url"] = [head_img_url] # 传递URL图片保存列表供ImagesPipeline使用
article_item["url"] = response.url
article_item["url_object_id"] = get_md5(response.url) # 获取url的md5值
yield article_item # 传递到pipeline。请看settings.py中ITEM_PIPELINES字典
# Item设计,类似于django的表单类
>>> vim ArticleSpider/items.py
import scrapy
class ArticleItem(scrapy.Item):
title = scrapy.Field() # 标题
content = scrapy.Field() # 内容
add_time = scrapy.Field() # 文章添加时间
url = scrapy.Field() # 文章URL
url_object_id = scrapy.Field() # URL的MD5值
head_img_url = scrapy.Field() # 封面图URL
head_img_path = scrapy.Field() # 封面图本地路径
star = scrapy.Field() # 点赞数
>>> vim ArticleSpider/spiders/settings.py
# 修改配置文件,去掉这个地方的注释,当爬虫解析函数返回Item对象时,需要经过这个管道
# Item管道
ITEM_PIPELINES = { # item的pipeline处理类;类似于item中间件
'ArticleSpider.pipelines.ArticlespiderPipeline': 300, # 处理顺序是按数字顺序,1代表第一个处理
}
# URL转md5函数
>>> create ArticleSpider/utils/__init__.py # 公共工具包
>>> vim common.py
import hashlib
def get_md5(url): # 获取URL的MD5值
if isinstance(url, str): # 如果是Unicode字符串
url = url.encode("utf-8")
m = hashlib.md5()
m.update(url) # 只接受UTF-8字节码
return m.hexdigest()
图片自动下载
>>> vim ArticleSpider/settings.py
PROJECT_DIR = os.path.join(BASE_DIR, "ArticleSpider")
# 修改配置文件,去掉这个地方的注释,当爬虫解析函数返回Item对象时,需要经过这个管道
# Item管道
ITEM_PIPELINES = { # item的pipeline处理类;类似于item中间件
'ArticleSpider.pipelines.ArticlespiderPipeline': 300, # 处理顺序是按数字顺序,1代表第一个处理
'scrapy.pipelines.images.ImagesPipeline': 1,
}
IMAGES_URLS_FIELD = "head_img_url" # 图片URL的字段名
IMAGES_STORE = os.path.join(PROJECT_DIR, "images") # 图片本地保存地址
# IMAGES_MIN_HEIGHT = 100 # 接收图片的最小高度
# IMAGES_MIN_WIDTH = 100 # 接收图片的最小宽度
图片自动下载自定义类
>>> vim ArticleSpider/settings.py
ITEM_PIPELINES = {
...
#'scrapy.pipelines.images.ImagesPipeline': 1,
'ArticleSpider.pipelines.ArticleImagePipeline': 1,
}
>>> vim ArticleSpider/pipelines.py
class ArticleImagePipeline(ImagesPipeline):
def item_completed(self, results, item, info):
if "head_img_url" in item: # 只处理有数据的URL
for ok, value in results: # 默认是多个图片URL,其实只传递了一个,所以results内只有一个
image_file_path = value["path"] # 获取图片保存的本地路径
item["head_img_path"] = image_file_path
return item # 返回item,下一个pipeline接收处理
数据保存
把item数据导出到json文件中
vim ArticleSpider/pipelines.py
import codecs # 文件操作模块
import json
# 把item保存到json文件
class JsonWithEncodingPipeline(object):
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" # 关闭ascii保存,因为有中文
self.file.write(lines)
return item
def spider_closed(self, spider):
# 当爬虫关闭时
self.file.close()
注册到item管道配置中
vim ArticleSpider/settings.py
ITEM_PIPELINES = {
'...',
'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
}
使用自带的模块导出json文件
>>> vim ArticleSpider/pipelines.py
from scrapy.exporters import JsonItemExporter
class JsonExporterPipeline(object):
# 调用scrapy提供的json export导出json文件
def __init__(self):
self.file = open("articleexport.json", "wb")
self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)
self.exporter.start_exporting()
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()
注册到item管道配置中
vim ArticleSpider/settings.py
ITEM_PIPELINES = {
'...',
'ArticleSpider.pipelines.JsonExporterPipeline': 2,
}
使用mysql保存
# 安装mysql驱动
>>> pip install mysqlclient
# centos需要另外安装驱动
>>> sudo yum install python-devel mysql-devel
使用同步的机制写入mysql
import MySQLdb
class MysqlPipeline(object):
def __init__(self):
self.conn = MySQLdb.connect('dongfe.com', 'root', 'Xiong123!@#', 'article_spider', charset="utf8", use_unicode=True)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
insert_sql = """
insert into article(title, url, add_time, star)
values (%s, %s, %s, %s)
"""
self.cursor.execute(insert_sql, (item['title'], item['url'], item['add_time'], item['star']))
self.conn.commit()
使用异步的机制写入mysql
import MySQLdb
import MySQLdb.cursors
from twisted.enterprise import adbapi
class MysqlTwistedPipline(object):
def __init__(self, dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls, settings):
pass # 这个方法会把settings文件传进来
dbparms = dict(
host="dongfe.com",
db="article_spider",
user="root",
passwd="Xiong123!@#",
charset="utf8",
cursorclass=MySQLdb.cursors.DictCursor,
use_unicode=True,
)
dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
return cls(dbpool)
def process_item(self, item, spider):
# 使用twisted将mysql插入变成异步执行
query = self.dbpool.runInteraction(self.do_insert, item)
query.addErrback(self.handle_error) # 处理异常
def do_insert(self, cursor, item):
# 执行具体的插入
insert_sql = """
insert into article(title, url, add_time, star)
values (%s, %s, %s, %s)
"""
cursor.execute(insert_sql, (item['title'], item['url'], item['add_time'], item['star']))
def handle_error(self, failure, item, spider):
# 处理异步插入的异常
print(failure)
item loader
直接将Item和CSS选择器绑定到一起,直接把选择出来的数据放入Item中。
item loader的一般使用
>>> vim ArticleSpider/spiders/jobbole.py
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/']
...
# 文章详情页的内容解析
def article_parse(self, response):
# 通过item loader加载item
item_loader = ItemLoader(item=ArticleItem(), response=response)
item_loader.add_css("title", ".grid-8 .entry-header > h1::text")
item_loader.add_css("content", ".grid-8 .entry")
item_loader.add_css("add_time", ".grid-8 .entry-meta p::text")
item_loader.add_value("url", response.url)
item_loader.add_value("url_object_id", get_md5(response.url))
item_loader.add_value("head_img_url", [head_img_url])
item_loader.add_css("star", "h10::text")
article_item = item_loader.load_item()
yield article_item # 传递到pipeline。请看settings.py中ITEM_PIPELINES字典
> 使用Item Loader的两个问题:
>
> 1. 原始数据需要处理
> 1. 解决办法:在Item内使用字段的处理器
> 2. 不管数据有几个,获取的是一个数组
> 1. 解决办法:在Item内字段处理器中使用TakeFirst()方法
配合item 的processor处理器的使用
>>> vim ArticleSpider/items.py
# MapCompose:可以调用多个函数依次运行
# TakeFirst: 与extract_first()函数一样,只选择数组第一个数据
# Join: 把数组用符号连接成字符串,比如Join(",")
from scrapy.loader.processors import MapCompose, TakeFirst, Join
# add_time键处理函数
def date_convert1(value):
add_time_match = re.match("[\s\S]*?(\d{2,4}[/-]\d{1,2}[/-]\d{1,2})[\s\S]*", value)
if add_time_match:
add_time = add_time_match.group(1)
else:
add_time = value.strip()
return add_time
def date_convert2(value):
try:
add_time = datetime.datetime.strptime(value, "%Y/%m/%d").date()
except Exception as e:
add_time = datetime.datetime.now().date()
return add_time
class ArticleItem(scrapy.Item):
...
add_time = scrapy.Field(
input_processor=MapCompose(date_convert1, date_convert2), # 处理原始数据
output_processor=TakeFirst() # 只取数组中第一个数据
) # 文章添加时间
...
自定义item loader
> 可以设置默认的default_output_processor = TakeFirst()
>>> vim ArticleSpider/items.py
from scrapy.loader import ItemLoader
class ArticleItemLoader(ItemLoader):
# 自定义item loader
default_output_processor = TakeFirst()
# 默认item处理器,什么都不做
def default_processor(value):
return value
class ArticleItem(scrapy.Item):
...
head_img_url = scrapy.Field(
# 图像URL需要一个数组类型,不取第一个数据,定义一个默认处理器覆盖掉
# 另外注意在使用sql保存时,需要取出数组第一个
output_processor=default_processor
) # 封面图URL
...
##########################################################################################
>>> vim ArticleSpider/spiders/jobbole.py
from ArticleSpider.items import ArticleItemLoader
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/']
...
# 文章详情页的内容解析
def article_parse(self, response):
...
# 通过item loader加载item
item_loader = ArticleItemLoader(item=ArticleItem(), response=response)
...