- 基本操作
- 使用日志
- spider编写
- Request与Response
- 登录处理
- 数据解析 Selector选择器
- scrapy shell
- 下载中间件的使用
- 爬虫中间件的使用
- POST请求
- 数据存储 与 pipelines
- scrapy使用selenium
- settings.py常用设置
基本操作
创建scrapy项目
scrapy startproject mySpider
scrapy目录结构
scrapy.cfg :项目的配置文件
mySpider/ :项目的Python模块,将会从这里引用代码
mySpider/items.py :项目的字段文件(在爬虫文件中保存数据使用)
mySpider/pipelines.py :项目的管道文件
mySpider/settings.py :项目的设置文件
mySpider/spiders/ :存储爬虫文件
创建spider
# itcast是爬虫名, "itcast.cn"是url范围
scrapy genspider itcast "itcast.cn"
自定义items字段类(类似字典)
- items字段类中所定义的字段, 将在spiders目录下的爬虫代码中使用
- 爬虫代码中使用items中字段类的对象保存提取的数据, yield返回给engine
- 如果直接使用return item, 数据不会经过pipelines
import scrapy
class TencentJobItem(scrapy.Item):
# define the fields for your item here like:
# 定义提取数据时需要放入字典的key
title = scrapy.Field()
company = scrapy.Field()
address = scrapy.Field()
salary = scrapy.Field()
date = scrapy.Field()
...
...
# 在提取数据时, item的key对应 字段类中定义的字段
def parse(self, response):
div_list = response.xpath("//div[@class='dw_table']/div")[3:-5]
for div in div_list:
item = TencentJobItem() # 使用字段类实例化一个用于保存数据的对象(类似字典)
item['title'] = div.xpath("./p/span/a/@title").extract_first()
item['company'] = div.xpath("./span[1]/a/text()").extract_first()
item['address'] = div.xpath("./span[2]/text()").extract_first()
item['salary'] = div.xpath("./span[3]/text()").extract_first()
item['date'] = div.xpath("./span[4]/text()").extract_first()
print(self.num, item)
yield item # 传递数据给engine, 由engine传递给piplines
...
...
# 在pipeline中使用mongodb时
# 在写入item之前, 需要转换为dict类型
# 在pipelines中:
from pymongo import MongoClient
client = MongoClient()
collection = client['tieba']['job']
class TencentJobPipeline(object):
def process_item(self, item, spider):
# 由于只能讲dict类型写入mongo中, 所以需要类型转换
# 将TencentJobItem字段类对象转换为dict
collection.insert(dict(item))
print('写入成功')
return item
使用log
- 在setting.py中加入log配置
# 日志信息等级
# 'WARNING'代表比此等级低的log都不会输出
# LOG_LEVEL = 'WARNING'
# LOG_LEVEL = 'ERROR'
# LOG_LEVEL = 'DEBUG'
LOG_LEVEL = 'INFO'
# log输出的文件
LOG_FILE = 'log.txt'
# cookies的信息
COOKIES_DEBUG = True
- 在需要打印log的py文件中使用logging模块
import logging
# 创建一个日志对象, 传入__name__, 输出时将打印出本模块(文件)的名字
logger = logging.getLogger(__name__)
logger.warning('warning')
logger.info('info')
logger.error('error')
logger.debug('debug')
- 如果在普通项目中想打印log, 可以自定义logger
import logging
logging.basicConfig('xxx')
logger = logging.getLogger()
logger.warning('xxxx')
yield 传递数据给 scrapy engine
- yield支持的数据类型:
- None
- dict
- Request
- BaseItem
User-Agent的设置
- 在setting.py文件中配置
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36'
构造Request
- Request用于将新的请求封装, 发送给scrapy engine
- url 请求的地址
- callback 响应的处理方法(提取数据)
- meta 传递item字典到其他的处理函数中使用
- dont_filter 默认False, 请求的地址不重复
- method 请求方式
- headers 请求头
- body 请求体
- cookies 携带cookie
class Request(object_ref):
def __init__(self, url, callback=None, method='GET', headers=None, body=None,
cookies=None, meta=None, encoding='utf-8', priority=0,
dont_filter=False, errback=None, flags=None):
实现翻页请求
def parse(self, response):
div_list = response.xpath("//div[@class='dw_table']/div")[3:-5]
for div in div_list:
item = dict()
item['title'] = div.xpath("./p/span/a/@title").extract_first()
item['company'] = div.xpath("./span[1]/a/text()").extract_first()
item['address'] = div.xpath("./span[2]/text()").extract_first()
item['salary'] = div.xpath("./span[3]/text()").extract_first()
item['date'] = div.xpath("./span[4]/text()").extract_first()
print(self.num, item)
yield item
# 获取下一页的地址
next_url = response.xpath("//a[text()='下一页']/@href").extract_first()
print('next_url: ', next_url)
# 当下一页地址不是None时发送请求
if next_url != None :
# 构造新的请求发送给引擎
yield scrapy.Request(
next_url, # 下一页的地址
callback=self.parse # 处理下一页地址的方法
)
DeBug信息
- Filtered offsite request to ‘xxx.com’
- 请求的域名被allowed_domains = [‘xxx’]过滤掉了, 不在规定的域名范围内
setting.py文件说明
# -*- coding: utf-8 -*-
# Scrapy settings for yang_guang_spider project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# https://doc.scrapy.org/en/latest/topics/settings.html
# https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
# https://doc.scrapy.org/en/latest/topics/spider-middleware.html
BOT_NAME = 'yang_guang_spider'
SPIDER_MODULES = ['yang_guang_spider.spiders']
NEWSPIDER_MODULE = 'yang_guang_spider.spiders'
LOG_LEVEL = 'WARNING'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
# Configure maximum concurrent requests performed by Scrapy (default: 16)
# 并发请求数量
#CONCURRENT_REQUESTS = 32
# Configure a delay for requests for the same website (default: 0)
# See https://doc.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
# 下载延迟
#DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16
# 是否带有cookie信息, 默认带有
# Disable cookies (enabled by default)
#COOKIES_ENABLED = False
# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False
# Override the default request headers:
# 默认请求头
#DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
#}
# Enable or disable spider middlewares
# See https://doc.scrapy.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
# 'yang_guang_spider.middlewares.YangGuangSpiderSpiderMiddleware': 543,
#}
# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
# 'yang_guang_spider.middlewares.YangGuangSpiderDownloaderMiddleware': 543,
#}
# Enable or disable extensions
# See https://doc.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
#}
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'yang_guang_spider.pipelines.YangGuangSpiderPipeline': 300,
}
# 自动限速
# Enable and configure the AutoThrottle extension (disabled by default)
# See https://doc.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False
# Enable and configure HTTP caching (disabled by default)
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
访问settings.py文件中的内容
- 在spiders下的爬虫代码中
self.settings.get("MONGO_HOST")
- 在pipelines中
spider.settings.get("MONGO_HOST")
数据清洗
- 字符串数组去掉不需要的字符
content = [re.sub(r"\xa0|\s", "", i) for i in content] # 替换字符串中的 '\xa0' 和 空白字符(\n\t等)为 ''
content = [i for i in content if len(i) > 0] # 删除空字符串
使用日志
配置log
LOG_FILE = "TencentSpider.log" # 日志文件
LOG_LEVEL = "INFO" # 日志等级
# LOG_ENABLED 默认: True,启用logging
# LOG_ENCODING 默认: 'utf-8',logging使用的编码
# LOG_FILE 默认: None,在当前目录里创建logging输出文件的文件名
# LOG_LEVEL 默认: 'DEBUG',log的最低级别
# LOG_STDOUT 默认: False 如果为 True,进程所有的标准输出(及错误)将会被重定向到log中。
# 例如,执行 print "hello" ,其将会在Scrapy log中显示。
日志等级
Scrapy提供5层logging级别:
CRITICAL - 严重错误(critical)
ERROR - 一般错误(regular errors)
WARNING - 警告信息(warning messages)
INFO - 一般信息(informational messages)
DEBUG - 调试信息(debugging messages) 最低等级
spider编写
1. scrapy.Spider
- 创建爬虫:
scrapy genspider tencent “tencent.com”
from mySpider.items import TencentItem
import scrapy
import re
class TencentSpider(scrapy.Spider):
name = "tencent"
allowed_domains = ["hr.tencent.com"]
start_urls = [
"http://hr.tencent.com/position.php?&start=0#a"
]
# 解析start_urls
def parse(self, response):
items = response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]')
for item in items:
temp = dict(
name=item.xpath("./td[1]/a/text()").extract()[0],
detailLink="http://hr.tencent.com/"+item.xpath("./td[1]/a/@href").extract()[0],
positionInfo=item.xpath('./td[2]/text()').extract()[0] if len(item.xpath('./td[2]/text()').extract())>0 else None,
peopleNumber=item.xpath('./td[3]/text()').extract()[0],
workLocation=item.xpath('./td[4]/text()').extract()[0],
publishTime=item.xpath('./td[5]/text()').extract()[0]
)
yield temp
now_page = int(re.search(r"\d+", response.url).group(0))
print("*" * 100)
if now_page < 216:
url = re.sub(r"\d+", str(now_page + 10), response.url)
print("this is next page url:", url)
print("*" * 100)
yield scrapy.Request(url, callback=self.parse)
2. crawlSpider
- 说明:
CrawlSpider使用rules来决定爬虫的爬取规则,并将匹配后的url请求提交给引擎。
所以在正常情况下,CrawlSpider不需要单独手动返回请求了。
在rules中包含一个或多个Rule对象,每个Rule对爬取网站的动作定义了某种特定操作,比如提取当前相应内容里的特定链接,是否对提取的链接跟进爬取,对提交的请求设置回调函数等。
如果多个rule匹配了相同的链接,则根据规则在本集合中被定义的顺序,第一个会被使用。
-
创建spider时添加参数: -t crawl
scrapy genspider –t crawl csdn csdn.cn -
Rule类
class scrapy.spiders.Rule(
link_extractor, # 该对象定义了url提取规则
callback = None, # 该函数用于解析符合规则的url, 它接收response对象
cb_kwargs = None,
follow = None, # 是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进.
# 如果callback为None,follow 默认设置为True ,否则默认为False。
process_links = None, # 指定该spider中哪个的函数将会被调用, 从link_extractor中获取到链接列表时将会调用该函数。
# 该方法主要用来过滤url。
process_request = None # 指定该spider中哪个的函数将会被调用, 该规则提取到每个request时都会调用该函数。
# 用来过滤request
)
- LinkExtractor类
class scrapy.linkextractors.LinkExtractor(
allow = (), # 满足括号中“正则表达式”的URL会被提取,如果为空,则全部匹配。
deny = (), # 满足括号中“正则表达式”的URL一定不提取(优先级高于allow)。
allow_domains = (), # 会被提取的链接的domains。
deny_domains = (), # 一定不会被提取链接的domains。
deny_extensions = None,
restrict_xpaths = (), # 使用xpath表达式,和allow共同作用过滤链接。
tags = ('a','area'),
attrs = ('href'),
canonicalize = True,
unique = True,
process_value = None
)
- scrapy shell 测试提取url
scrapy shell "http://hr.tencent.com/position.php?&start=0#a"
from scrapy.linkextractors import LinkExtractor
page_lx = LinkExtractor(allow=('position\.php\?&start=\d+')) # 提取规则, 注意.和?需要转义
page_lx.extract_links(response) # 从response对象中提取所有url
-
注意:
由于CrawlSpider使用parse方法来实现其逻辑,如果覆盖了 parse方法,crawl spider将会运行失败。 -
案例 1:
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class TecentSpider(CrawlSpider):
name = 'tecent'
allowed_domains = ['hr.tencent.com']
start_urls = ['http://hr.tencent.com/position.php?&start=0']
page_lx = LinkExtractor(allow=r'start=\d+') # 提取规则
#position.php?&start=10#a
rules = (
Rule(page_lx, callback='parse_item', follow=True),
)
def parse_item(self, response):
items = response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]')
for item in items:
temp = dict(
position=item.xpath("./td[1]/a/text()").extract()[0],
detailLink="http://hr.tencent.com/" + item.xpath("./td[1]/a/@href").extract()[0],
type=item.xpath('./td[2]/text()').extract()[0] if len(
item.xpath('./td[2]/text()').extract()) > 0 else None,
need_num=item.xpath('./td[3]/text()').extract()[0],
location=item.xpath('./td[4]/text()').extract()[0],
publish_time=item.xpath('./td[5]/text()').extract()[0]
)
print(temp)
yield temp
# parse() 方法不需要重写
# def parse(self, response):
# pass
- 案例 2:
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
import re
class CfSpider(CrawlSpider):
name = 'cf'
allowed_domains = ['segmentfault.com']
start_urls = ['https://segmentfault.com/']
# 提取url地址的规则
rules = (
# url提取器, 根据规则提取url发送请求
# 发送后的请求交给callback的方法进行处理
# follow代表在当前获取的响应中是否继续使用该规则提取url,提取翻页地址需要设置为true
# Rule(LinkExtractor(allow=r'/chinese/home/docView/xzcf_[a-zA-Z0-9]+\.html'), callback='parse_item'),
Rule(LinkExtractor(allow=r'/a/\d+'), callback='parse_item'),
)
def parse_item(self, response):
item = {}
item['title'] = response.xpath("//h1[@id='articleTitle']/a/text()").extract_first()
item['author'] = response.xpath("//div[@class='article__authorleft']/a/img/@alt").extract_first()
print(item)
Request与Response
Request
- 常用参数
url: 就是需要请求,并进行下一步处理的url
callback: 指定该请求返回的Response,由那个函数来处理。
method: 请求一般不需要指定,默认GET方法,可设置为"GET", "POST", "PUT"等,且保证字符串大写
headers: 请求时,包含的头文件。一般不需要。内容一般如下:
# 自己写过爬虫的肯定知道
Host: media.readthedocs.org
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0
Accept: text/css,*/*;q=0.1
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://scrapy-chs.readthedocs.org/zh_CN/0.24/
Cookie: _ga=GA1.2.1612165614.1415584110;
Connection: keep-alive
If-Modified-Since: Mon, 25 Aug 2014 21:59:35 GMT
Cache-Control: max-age=0
meta: 比较常用,在不同的请求之间传递数据使用的.dict类型
response会携带meta指定的字典.
例如:
request_with_cookies = Request(
url="http://www.example.com",
cookies={'currency': 'USD', 'country': 'UY'},
meta={'item': (title, content_url)} # name, content_url可以传递给parse_detail方法使用
callback='parse_detail'
)
def parse_detail(self, response):
title, content_url = response.meta['item']
pass
encoding: 使用默认的 'utf-8' 就行。
dont_filter: 表明该请求不由调度器过滤。这是当你想使用多次执行相同的请求,忽略重复的过滤器。默认为False。
errback: 指定错误处理函数
Response对象
- 常用参数
status: 响应码
_set_body(body): 响应体
_set_url(url): 响应url
self.request = request
登录处理
携带cookies访问登录之后的页面
-
某些信息在登录之后才能访问数据
-
首先使用账户名和密码登录网站, 复制登录网站的cookies
-
默认情况下, 首地址的请求时engine自动发送的, 现在需要携带cookies
-
重写start_requests方法, 让首地址的请求携带cookies信息
# -*- coding: utf-8 -*-
import scrapy
import re
class GithubSpider(scrapy.Spider):
name = 'github'
allowed_domains = ['github.com']
start_urls = ['https://github.com/ken723'] # 首地址
# 从登录后页面的响应中复制的cookies信息
cookies = "has_recent_activity=1; _device_id=965692cc867f21a27eb1f797bdaa9ea6; _octo=GH1.1.864247960.1558680030; _ga=GA1.2.2073575103.1558680038; tz=Asia%2FShanghai; user_session=4pAlPpAijwGX_pLUMFVeCLVR5oI0R_ewSv6kWoTfmKIIS_MZ; __Host-user_session_same_site=4pAlPpAijwGX_pLUMFVeCLVR5oI0R_ewSv6kWoTfmKIIS_MZ; logged_in=yes; dotcom_user=ken723; _gat=1; _gh_sess=LzY5Ni9KMy9qWngrSXFwNjNMQWI4bU9qbWdzWEFmSGRRNWhZQkw0MkhaVk4wL0JNV1ovQlR2alZHMmN4VjZmLzBHd09yT1Bjc2d2TGtYRE5hcDdsY2EyWnh0YVhvaHppWi9sYXJjZmd3ZDZHUW9IYlNzZE9GWk4xWjJyaGdmRmRPSFdZRUtBV01rVUpva045dUlFcEhCOE5pVnFVY1pjVU5LUk9UOVMvcVVuaXdLZDNUTjAwNkd0QkhnZEs3RmVtenlnL2NhanRSM1Fqa2NSZlVTQkdaTGVWTW83ZjlnZzRCVzN5d2xNU2JKL3lCNjFKOU4veElBN1gxd3lyOXJXb0NrRkxYWWVaWHAxM2lLajFQVm5OOEQrenFQSUlranUzY0doQXh1L2RVR01mbXFqWWJuTFNPcCtIV1RHWXZtMlJTSW5seXg5aC8xOWRyeThxVFJXVXd3PT0tLXRncitlbVpiMm8rSWJrV2hKUVl1OWc9PQ%3D%3D--d54930605abd55998836f9649bf1608217fa7d7a"
# 将cookies字符串转换为dict
cookies = {i.split("=")[0]:i.split("=")[1] for i in cookies.split("; ")}
# print(cookies)
# 首地址的请求默认调用该方法
# 重写该方法, 携带自定义cookies
def start_requests(self):
yield scrapy.Request(
url=self.start_urls[0], # 首地址
callback=self.parse, # 处理响应的方法
cookies=self.cookies # 自定义的cookies
)
# 处理响应
def parse(self, response):
# print('parse_func')
# 如果访问成功, 可以匹配到登录后的邮箱名
print(re.findall("wqhken723@sina.com",response.body.decode()
))
数据解析 Selector选择器
xpath() 常用
- 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表
css()
- 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表,语法同 BeautifulSoup4
re()
- 根据传入的正则表达式对数据进行提取,返回字符串list列表
extract() 常用
- 序列化该节点为字符串并返回list, 用于提取节点中的数据组成的list
- 同getall()
extract_first() 常用
- 序列化节点为字符串并返回, 用于提取节点中的数据
- 同get()
xpath表达式案例:
/html/head/title # 选择<HTML>文档中 <head> 标签内的 <title> 元素
/html/head/title/text() # 选择上面提到的 <title> 元素的文字
//td # 选择所有的 <td> 元素
//div[@class="mine"] # 选择所有具有 class="mine" 属性的 div 元素
scrapy shell
- 解析数据时, 可以通过scrapy shell进行测试
- 终端虚拟python环境中, 启动scrapy shell
(venv)$: scrapy shell “http://hr.tencent.com/position.php?&start=0#a”
# 返回 xpath选择器对象列表
response.xpath('//title')
# [<Selector xpath='//title' data='<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title'>]
# 使用 extract()方法返回字符串列表
response.xpath('//title').extract()
# ['<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>']
# 打印列表第一个元素,没有则返回None
response.xpath('//title').extract_first()
# <title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>
# response常用属性
response.url: # 当前响应的url地址
response.request.url: # 当前响应对应的请求的url地址
response.headers: # 响应头
response.body: # 响应体,也就是html代码,默认是byte类型
response.requests.headers: # 当前响应的请求头
下载中间件的使用
1. 使用user-agent池
2. 使用proxy池
- settings.py文件, 设置user-agent列表, 设置proxy列表
- 开启下载中间件
DOWNLOADER_MIDDLEWARES = {
# 'login.middlewares.LoginDownloaderMiddleware': 543,
# 这里对应着middlewares.py文件中定义的类
'login.middlewares.RandomUserAgentMiddleware': 543,
}
USER_AGENTS_LIST = [
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
"Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"
]
PROXY_IP_LIST = [
'118.89.150.177:1080',
'119.36.161.174:80',
'52.83.253.74:3128',
'39.137.168.230:80',
'103.205.26.78:56341',
'106.2.238.2:3128',
'182.111.129.37:53281',
'193.112.113.26:1080'
]
- 编写middlewares.py文件, 自定义下载中间件类
from scrapy import signals
import random
# 自定义下载中间件
class RandomUserAgentMiddleware():
# 每一个request通过下载中间件时被调用
def process_request(self, request, spider):
print('request_func')
# 调用settings.py中的列表
proxy_ip_list = spider.settings.get('PROXY_IP_LIST')
proxy = random.choice(proxy_ip_list)
# 设置代理ip
request.meta['proxy'] = 'http://' + proxy
# 调用settings.py中的列表
user_agent_list = spider.settings.get("USER_AGENTS_LIST")
# 随机一个列表内的user-agent
user_agent = random.choice(user_agent_list)
# 设置请求头的user-agent
request.headers['User-Agent'] = user_agent
# 当下载器完成请求, 传递response给engine时被调用
def process_response(self, request, response, spider):
# 获取请求头中的user-agent
user_agent = request.headers['User-Agent']
proxy = request.meta['proxy']
print(user_agent)
return response
爬虫中间件的使用
process_spider_output()
- yield scrapy.Request()或者yield item之后调用
# 该函数在数据传递到pipeline之前调用, 可以对parse解析之后的request或者item进行处理.
# result参数记录了Request和item对象.
def process_spider_output(response, result, spider):
for item in result:
if isinstance(item, scrapy.Item):
print('传递给pipeline的item对象', item)
yield item # 传递给pipeline
for request in result:
request.meta['request_start_time'] = time.time()
yield request # 传递给调度器
process_spider_exception()
- 爬虫代码出现异常时调用
process_spider_input()
- 爬虫中解析函数parse调用之前调用
process_start_requests()
- start_requests()运行时调用
import datetime
class ExceptionCheckSpider(object):
def process_spider_exception(self, response, exception, spider):
print('爬虫异常类型: ', type(exception))
# 记录出错的请求, 传递给pipeline
# page = response.meta['page'] # 获取设置在Request()里面携带的meta
# now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# error_item = ErrorItem() # 使用items.py中定义的错误类ErrorItem
# error_item['page'] = page
# error_item['error_time'] = now_time
# yield error_item
# 如果直接返回了Request或者item对象, 将绕过爬虫, 传递给pipeline
# pipeline可以保存到数据库
# yield scrapy.Request()
# yield item
return None
POST请求
使用scrapy.FormRequest() 发送post请求登录
- 构造post请求进行登录
- 需要寻找登录请求的地址和构造post需要的数据
# -*- coding: utf-8 -*-
import scrapy
import re
class PostLoginSpider(scrapy.Spider):
name = 'post_login'
allowed_domains = ['github.com']
start_urls = ['https://github.com/login'] # 登录页面
def parse(self, response):
# 由于post请求需要的某些数据在https://github.com/login响应中.
# 首先发送login地址, 提取需要的数据.
# 提取post请求需要的数据
commit = response.xpath("//input[@name='commit']/@value").extract_first()
authenticity_token = response.xpath("//input[@name='authenticity_token']/@value").extract_first()
utf8 = response.xpath("//input[@name='utf8']/@value").extract_first()
password = 'root'
login = 'root'
# 发送post请求需要的数据
data = dict(
commit=commit,
utf8=utf8,
authenticity_token=authenticity_token,
login=login,
password=password
)
# post登录请求的地址
post_url = "https://github.com/session"
# 发送post请求
yield scrapy.FormRequest(
url=post_url,
formdata=data,
callback=self.parse_user_page # 处理登录之后页面的响应
)
def parse_user_page(self, response):
# 验证登录之后的响应中是否有用户名
user_info = re.findall("root", response.body.decode())
print(user_info)
使用scrapy.FormRequest().from_response()自动登录
- 在form中定义了action的地址可以使用自动登录
- 特别的, 当一个页面中有两个或以上的form时, 可以根据from_resonse()的参数定位某一个form. id, name, xpath都可以进行定位form
# from_response方法的源码
def from_response(
cls,
response,
formname=None, formid=None, formnumber=0, formdata=None,
clickdata=None,
dont_click=False,
formxpath=None,
formcss=None, **kwargs):
# -*- coding: utf-8 -*-
import scrapy
import re
class PostLogin2Spider(scrapy.Spider):
name = 'post_login2'
allowed_domains = ['github.com']
start_urls = ['https://github.com/login']
def parse(self, response):
# post需要的数据
data = dict(
# login和password是form中的input的name属性
login='root',
password='root'
)
# from_response可以自动搜索form表单的提交地址进行登录请求
# 只需要提供用户名和密码
yield scrapy.FormRequest.from_response(
response,
formdata=data,
callback=self.parse_user_page
)
def parse_user_page(self, response):
html = response.body.decode()
print(html)
user_name = re.findall("root", html)
print(user_name)
baidu贴吧 进入详情页获取图片地址
requests.utils.unquote(item[‘href’])地址解码
urllib.parse.urljoin(response.url, )自动拼接完整url
# -*- coding: utf-8 -*-
import scrapy
import urllib
import re
import requests
class TbSpider(scrapy.Spider):
name = 'tb'
allowed_domains = ['tieba.baidu.com']
start_urls = ['http://tieba.baidu.com/mo/q---E48FA39FEC36AE1C5C941D0C8BE63613%3AFG%3D1--1-3-0--2--wapp_1558705534165_899/m?kw=python&lp=5011&lm=&pn=0']
def parse(self, response):
# 分组
div_list = response.xpath("//div[@class='i']")
for div in div_list:
item = dict()
item['title'] = div.xpath("./a/text()").extract_first()
item['title'] = re.sub(r"\xa0", "", item['title'])
item['url'] = div.xpath("./a/@href").extract_first()
item['img_url'] = list()
# print(item)
if item['url'] is not None:
# 这里使用urllib.parse.urljoin()方法根据response.url自动拼接url
item['url'] = urllib.parse.urljoin(response.url, item['url'])
yield scrapy.Request(
url=item['url'],
callback=self.parse_detail,
meta={'item': item}
)
#翻页
next_url = response.xpath("//a[text()='下一页']/@href").extract_first()
if next_url is not None:
next_url = urllib.parse.urljoin(response.url, next_url)
yield scrapy.Request(
url=next_url,
callback=self.parse
)
def parse_detail(self, response):
item = response.meta['item']
item['img_url'] = response.xpath("//div[@class='d']/div[1]/a/@href").extract()
# 对地址进行解码, 然后切割获取需要的部分
item['img_url'] = [requests.utils.unquote(i).split("src=")[-1] for i in item['img_url']]
print(item)
数据存储 与 pipelines
不使用pipeline 直接导出json,jsonl,csv,xml
# json格式,默认为Unicode编码
scrapy crawl itcast -o teachers.json
# json lines格式,默认为Unicode编码
scrapy crawl itcast -o teachers.jsonl
# csv 逗号表达式,可用Excel打开
scrapy crawl itcast -o teachers.csv
# xml格式
scrapy crawl itcast -o teachers.xml
使用item pipeline
- pipiline组件是一个独立的Python类,其中process_item()方法必须实现
- 使用之前需要在settings里面配置:
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'mySpider.pipelines.SomethingPipeline': 300,
}
- pipelines文件:
import something
class SomethingPipeline(object):
def __init__(self):
# 可选实现,做参数初始化等
# doing something
def process_item(self, item, spider):
# item (Item 对象) – 被爬取的item
# spider (Spider 对象) – 爬取该item的spider
# 这个方法必须实现,每个item pipeline组件都需要调用该方法,
# 这个方法必须返回一个 Item 对象,不返回item将不会被之后的pipeline组件所处理。
return item
def open_spider(self, spider):
# spider (Spider 对象) – 被开启的spider
# 可选实现,当spider被开启时,这个方法被调用。
def close_spider(self, spider):
# spider (Spider 对象) – 被关闭的spider
# 可选实现,当spider被关闭时,这个方法被调用
普通写入json文件
import json
class ItcastJsonPipeline(object):
def __init__(self):
self.file = open('teacher.json', 'wb')
def process_item(self, item, spider):
content = json.dumps(dict(item), ensure_ascii=False) + "\n" # item需要转换为dict
self.file.write(content)
return item
def close_spider(self, spider):
self.file.close()
异步写入json文件
JsonItemExporter
- 保存json数据的时候,可以使用这两个类,让操作变得得更简单。
JsonItemExporter
:这个是每次把数据添加到内存中。最后统一写入到磁盘中。- 好处是,存储的数据是一个满足json规则的数据。
- 坏处是, 如果数据量比较大,那么比较耗内存。示例代码如下:
from scrapy.exporters import JsonItemExporter
class QsbkPipeline(object):
def __init__(self):
self.fp = open("duanzi.json",'wb')
self.exporter = JsonItemExporter(self.fp, ensure_ascii=False, encoding='utf-8')
self.exporter.start_exporting() # 设置导出器开始导出
def open_spider(self,spider):
print('爬虫开始了...')
def process_item(self, item, spider):
self.exporter.export_item(item) # 导出器导出数据
return item
def close_spider(self,spider):
self.exporter.finish_exporting() # 导出器关闭
self.fp.close() # 文件资源释放
print('爬虫结束了...')
JsonLinesItemExporter
JsonLinesItemExporter
:这个是每次调用export_item
的时候就把这个item存储到硬盘中。- 坏处是, 每一个字典是一行,整个文件不是一个满足json格式的文件。
- 好处是, 每次处理数据的时候就直接存储到了硬盘中,这样不会耗内存,数据也比较安全。示例代码如下:
from scrapy.exporters import JsonLinesItemExporter
class QsbkPipeline(object):
def __init__(self):
self.fp = open("duanzi.json",'wb')
self.exporter = JsonLinesItemExporter(self.fp, ensure_ascii=False, encoding='utf-8')
def open_spider(self,spider):
print('爬虫开始了...')
def process_item(self, item, spider):
self.exporter.export_item(item)
return item
def close_spider(self,spider):
self.fp.close()
print('爬虫结束了...')
from scrapy.exporters import JsonLinesItemExporter
from soufang.items import NewHouseItem
from soufang.items import EsfHouseItem
# 异步写入json文件
class SoufangPipeline(object):
def __init__(self):
self.newhouse_fp = open('newhouse.json', 'wb')
self.esfhouse_fp = open('esfhouse.json', 'wb')
# 默认情况下, 写入的是asc2编码, 需要关闭
self.newhouse_exporter = JsonLinesItemExporter(self.newhouse_fp, ensure_ascii=False)
self.esfhouse_exporter = JsonLinesItemExporter(self.esfhouse_fp, ensure_ascii=False)
def process_item(self, item, spider):
# 根据不同的item类型, 写入不同的文件
if isinstance(item, NewHouseItem):
self.newhouse_exporter.export_item(item) # 写入
if isinstance(item, EsfHouseItem):
self.esfhouse_exporter.export_item(item)
return item
def close_spider(self, spider):
self.newhouse_fp.close() # 释放资源
self.esfhouse_fp.close()
3. 异步写入mysql
- 在pipelines中处理存储操作
- twisted提供了创建异步连接池的方式操作数据库
# -*- coding: utf-8 -*-
import pymysql
from twisted.enterprise import adbapi # 使用twisted中的异步连接池
from pymysql import cursors
# 普通写入mysql
class JianshuAllPipeline(object):
def __init__(self):
self.conn = pymysql.connect(host='192.168.145.130', port=3306, database='jianshu', user='ken', password='ken723', charset='utf8')
self.cursor = self.conn.cursor()
self.sql = "insert into article values(0, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"
def process_item(self, item, spider):
print('开始写入'+'='*200)
self.cursor.execute(self.sql, (item['title'],
item['content'],
item['author'],
item['avatar'],
item['publish_time'],
item['article_id'],
item['origin_url'],
item['read_count'],
item['like_count'],
item['word_count'],
item['subjects'],
item['comment_count']))
self.conn.commit()
print('写入成功'+'='*200)
return item
# 异步写入mysql
class JianshuAllTwistedPipeline(object):
def __init__(self):
dbparams = {
'host': '192.168.145.130',
'port': 3306,
'database': 'jianshu',
'user': 'ken',
'password': 'ken723',
'charset': 'utf8',
'cursorclass': cursors.DictCursor
}
# 使用twidted创建异步连接池
self.dbpool = adbapi.ConnectionPool('pymysql', **dbparams)
# 定义插入语句
self.sql = "insert into article values(0, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"
def process_item(self, item, spider):
print('开始写入' + '*'*150)
# 使用异步连接池执行insert_item方法
defer = self.dbpool.runInteraction(self.insert_item, item)
# 如果执行insert_item出现异常, 执行handle_error
defer.addErrback(self.handle_error, item, spider)
print('写入成功' + '*' * 150)
return item
# 插入数据库
def insert_item(self, cursor, item):
# 执行插入语句
cursor.execute(self.sql, (item['title'],
item['content'],
item['author'],
item['avatar'],
item['publish_time'],
item['article_id'],
item['origin_url'],
item['read_count'],
item['like_count'],
item['word_count'],
item['subjects'],
item['comment_count']))
# 错误处理
def handle_error(self, error, item, spider):
print('*'*20 + 'error' + '*'*20)
print(error)
print('*'*20 + 'error' + '*'*20)
4. 写入mongodb
# 在pipelines中:
from pymongo import MongoClient
client = MongoClient()
collection = client['tieba']['job']
class TencentJobPipeline(object):
def process_item(self, item, spider):
# 由于只能讲dict类型写入mongo中, 所以需要类型转换
# 将TencentJobItem字段类对象转换为dict
collection.insert(dict(item))
print('写入成功')
return item
scrapy使用selenium
- 在spider拿到响应信息之前, 在下载中间件中截获url, 使用selenium发送请求
- 使用selenium获取到的完整网页源码, 封装成response对象返回给spider
- spider部分的解析不变, 只是每次拿到的是经过selenium获取的动态加载后的完整网页源码.
# 下载中间件
# -*- coding: utf-8 -*-
from scrapy import signals
from selenium import webdriver
import time
from scrapy.http.response.html import HtmlResponse
# 截获request对象的url, 使用selenium发送请求获得网页源码,
# 将地址和源码封装成Response对象, 直接返回给spider
class SeleniumDownloaderMiddleware(object):
def __init__(self):
self.driver = webdriver.Chrome(r'C:\Software\chromedriver.exe')
def process_request(self, request, spider):
self.driver.get(request.url) # 截获request对象的url,使用selenium发送请求
time.sleep(1)
# 进行额外的点击操作, 获得所有页面的动态内容
try:
while True:
show_more = self.driver.find_element_by_class_name("show-more") # 查看更多按钮
show_more.click()
time.sleep(0.5)
if not show_more:
break
except:
pass
source = self.driver.page_source # 获取所有动态内容加载后的的网页源码
# 封装Response对象
response = HtmlResponse(url=self.driver.current_url, body=source, request=request, encoding='utf-8')
return response # 返回对象给爬虫部分
settings.py常用设置
BOT_NAME
# 默认: 'scrapybot'
# 当您使用 startproject 命令创建项目时其也被自动赋值。
CONCURRENT_ITEMS
# 默认: 100
# Item Processor(即 Item Pipeline) 同时处理(每个response的)item的最大值。
CONCURRENT_REQUESTS
# 默认: 16
# Scrapy downloader 并发请求(concurrent requests)的最大值。
DEFAULT_REQUEST_HEADERS
# 默认: 如下
# {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
# }
# Scrapy HTTP Request使用的默认header。
DEPTH_LIMIT
# 默认: 0
# 爬取网站最大允许的深度(depth)值。如果为0,则没有限制。
DOWNLOAD_DELAY
# 默认: 0
# 下载器在下载同一个网站下一个页面前需要等待的时间。该选项可以用来限制爬取速度, 减轻服务器压力。同时也支持小数:
# DOWNLOAD_DELAY = 0.25 # 250 ms of delay
# 默认情况下,Scrapy在两个请求间不等待一个固定的值, 而是使用0.5到1.5之间的一个随机值 * DOWNLOAD_DELAY 的结果作为等待间隔。
DOWNLOAD_TIMEOUT
# 默认: 180
# 下载器超时时间(单位: 秒)。
ITEM_PIPELINES
# 默认: {}
# 项目中启用的pipeline及其顺序的字典。该字典默认为空,值(value)任意,不过值(value)习惯设置在0-1000范围内,值越小优先级越高。
# ITEM_PIPELINES = {
# 'mySpider.pipelines.SomethingPipeline': 300,
# 'mySpider.pipelines.ItcastJsonPipeline': 800,
# }
LOG_ENABLED
# 默认: True
# 是否启用logging。
LOG_ENCODING
# 默认: 'utf-8'
# logging使用的编码。
LOG_LEVEL
# 默认: 'DEBUG'
# log的最低级别。可选的级别有: CRITICAL、 ERROR、WARNING、INFO、DEBUG 。
USER_AGENT
# 默认: "Scrapy/VERSION (+http://scrapy.org)"
# 爬取的默认User-Agent,除非被覆盖。
PROXIES:
# 代理设置
# PROXIES = [
# {'ip_port': '111.11.228.75:80', 'password': ''},
# {'ip_port': '120.198.243.22:80', 'password': ''},
# {'ip_port': '111.8.60.9:8123', 'password': ''},
# {'ip_port': '101.71.27.120:80', 'password': ''},
# {'ip_port': '122.96.59.104:80', 'password': ''},
# {'ip_port': '122.224.249.122:8088', 'password':''},
# ]
COOKIES_ENABLED = False
# 禁用Cookies
# 默认是注释的, 也就是开启了cookies