爬虫基本概念
- 关于误伤:
假如网站管理人员发现某个 IP 访问过于频繁,判定为爬虫,可以将其 IP 禁封,这是最有效的方法。但是这样做就会带来误伤,①比如学校或者网吧,他们对外的 IP 只有一个或者几个,内部全部属于局域网,如果学校或者网吧的某一个人写了一个爬虫,那么如果禁用掉这个对外的公网 IP ,内部所有人就都不能访问这个网站了,损失广大用户。②现在 IP 通常都是动态分配的 IP,比如某个小区、某个区域,当我们重启路由器后网络 IP 实际上是会变的(大多数情况下),假如某个人写了一个爬虫,禁用掉这个人所用 IP,过段时间这个 IP 分配给了另一个人,那么另一个人就无法访问这个网站,即使他并没有写过爬虫。
所以网站通常采用的是禁用某个 IP 一段时间。
反爬虫的目的
爬虫和反爬虫对抗过程
- 其他反爬虫策略:
①当判断是一个爬虫在访问的时候,可以返回假的数据,而不是直接禁用掉
②分析用户行的的时候发现某些 IP 请求的时候只请求 HTML 页面,而不请求 CSS、JS、图片等文件(爬虫为了并发),这就可以判断明显的是爬虫行为,这种判断方式非常有效
③但是如果通过 selenium + 浏览器的策略,是无法判断是否为爬虫的,一切请求和真实用户并无差别,所以理论上网站是不可能从技术上根本的解决爬虫问题,成本过高只好放弃
Scrapy 架构图
- engine 是最核心的部分,爬虫所有流向都经过引擎
- 爬虫的第一步是从 spiders 开始的
- 注意 spiders 过来的 requests 不是直接交给 downloader 去下载的,而是交给调度器 scheduler,然后 engine 再从 scheduler 里面区区,取出来才交给下载器 downloader 去下载
Scrapy 的 Request 和 Response
官方文档:https://doc.scrapy.org/en/latest/topics/request-response.html
Request 部分源码
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):
self._encoding = encoding # this one has to be set first
self.method = str(method).upper()
self._set_url(url)
self._set_body(body)
assert isinstance(priority, int), "Request priority not an integer: %r" % priority
self.priority = priority
if callback is not None and not callable(callback):
raise TypeError('callback must be a callable, got %s' % type(callback).__name__)
if errback is not None and not callable(errback):
raise TypeError('errback must be a callable, got %s' % type(errback).__name__)
assert callback or not errback, "Cannot use errback without a callback"
self.callback = callback
self.errback = errback
self.cookies = cookies or {}
self.headers = Headers(headers or {}, encoding=encoding)
self.dont_filter = dont_filter
self._meta = dict(meta) if meta else None
self.flags = [] if flags is None else list(flags)
@property
def meta(self):
if self._meta is None:
self._meta = {}
return self._meta
...
参数说明:
- url:需要请求的 URL
- callback:请求返回的 Response 由这个指定的方法来处理
- method:请求方法,默认 GET,注意大写
- headers:请求时包含的头信息
- body:请求体
- cookies:指定请求的 cookies,如果请求的网页带有 cookies,scrapy 在发送第二次请求的时候会默认携带 cookies
- meta:在不同的请求之间传递数据,会经常用到,类型为 dict
- encoding:指定编码格式,默认 'utf-8'
- priority:指定请求的优先级,会影响 scheduler 的优先调度顺序,可以通过设定此参数的值来改变请求顺序,加入设置某个 Request 的 priority
比较高,那么即使这个 Request 是后传递过来的,也会先调度- dont_filter:yield 过去重复的 Request 要不要过滤,默认 False 是会过滤重复请求的,设为 True 就可以不去重
- errback:是一个回调方法,错误处理,比如请求返回的响应是 500或者404等的时候,可以通过这个回调设置后续的处理
- flags
Response部分源码
class Response(object_ref):
def __init__(self, url, status=200, headers=None, body=b'', flags=None, request=None):
self.headers = Headers(headers or {})
self.status = int(status)
self._set_body(body)
self._set_url(url)
self.request = request
self.flags = [] if flags is None else list(flags)
@property
def meta(self):
try:
return self.request.meta
except AttributeError:
raise AttributeError(
"Response.meta not available, this response "
"is not tied to any request"
)
...
参数说明:
- url:网页 URL
- status:返回状态码
- headers:服务器返回的 headers
- body:服务器返回的 HTML 内容
- flags:
- request:就是之前 yield 过来的 Request,可以通过这个参数知道当前
Response 是对哪个 Request 进行的下载
Response 是有子类的:https://doc.scrapy.org/en/latest/topics/request-response.html#response-subclasses
用的最多的就是 HtmlResponse
总结:
spiders 产出 Request
downloader 返回 Resonse
整体流程:
spider yield Request 给 engine,engine 将这个 Request 发给 scheduler,scheduler 将 Request 再发送给 engine,engine 再将 Request 发送给 downloader,downloader 下载完成后返回 Response 给 engine,engine 将 Response 交个 spider,spider 解析 Response 数据,提取出 item 交给 item pipelines,提取出 Request 再次交个 scheduler,循环 1~7 的步骤
随机更换 User-Agent
-
以知乎爬虫为例:
方法 1
在 settings.py 中自定义一个存储所有 User-Agent 的列表
然后在 zhihu spider 中随机取出一个 User-Agent
- settings.py 中添加如下代码
# ArticleSpider/settings.py
# User-Agent 列表
user_agent_list = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
]
# ArticleSpider/spiders/zhihu_question.py
from ArticleSpider.settings import user_agent_list
class ZhihuQuestionSpider(scrapy.Spider):
name = 'zhihu_question'
allowed_domains = ['www.zhihu.com']
# start_urls = ['https://www.zhihu.com/explore'] # 重写了爬虫入口 start_requests,所以 start_urls 也就没必要了
# question 的第一页 answer 请求 URL
start_answer_url = 'https://www.zhihu.com/api/v4/questions/{question}/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit={limit}&offset={offset}'
# 随机取出一个 User-Agent
import random
random_agent = random.choice(user_agent_list)
headers = {
'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
'Host': 'www.zhihu.com',
'Referer': 'https://www.zhihu.com/',
'User-Agent': random_agent,
}
但是,事实上这样做是无效的,是不能真正实现每次请求时随机取一个 User-Agent 的,因为这是写在类变量里面的,只在类初始化的时候随机取一次,以后的每次请求都是用的这个,而不会再次随机取一个 User-Agent
方法 2
settings.py 内容不变,zhihu_question.py 代码要修改下,在每个 scrapy.Request 请求发送出去之前,都随机获取一个 User-Agent,这样就能够真正达到每次发送请求爬取页面都会随机切换 User-Agent
# ArticleSpider/spiders/zhihu_question.py
from ArticleSpider.settings import user_agent_list
class ZhihuQuestionSpider(scrapy.Spider):
name = 'zhihu_question'
allowed_domains = ['www.zhihu.com']
# start_urls = ['https://www.zhihu.com/explore'] # 重写了爬虫入口 start_requests,所以 start_urls 也就没必要了
# question 的第一页 answer 请求 URL
start_answer_url = 'https://www.zhihu.com/api/v4/questions/{question}/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit={limit}&offset={offset}'
headers = {
'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
'Host': 'www.zhihu.com',
'Referer': 'https://www.zhihu.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
}
def start_requests(self):
"""
重写爬虫入口 start_requests
"""
url = 'https://www.zhihu.com/node/ExploreAnswerListV2?params='
# offset = 0
for offset in range(4):
time.sleep(random.random())
paramse = {"offset": offset * 5, "type": "day"}
full_url = f'{url}{json.dumps(paramse)}'
# 以后每次发送请求都会随机获取一个 User-Agent
import random
random_agent = random.choice(user_agent_list)
self.headers['User-Agent'] = random_agent
yield scrapy.Request(url=full_url, headers=self.headers, callback=self.parse_question)
但是,这个方法也有一个弊端,就是每个要发送请求的方法中都要写一遍同样的代码,代码重复
方法 3
通过自定义 downloader middleware 来实现随机切换 User-Agent
settings.py 中事实上已经配置了 DOWNLOADER_MIDDLEWARES,只不过默认是被注释掉的
DOWNLOADER_MIDDLEWARES 和 ITEM_PIPELINES 类似,同样需要配置类名以及后面的数字,数字代表顺序
scrapy 本身提供了一个 UserAgentMiddleware,默认 user_agent='Scrapy'
Downloader Middleware 官方文档:https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
Downloader Middleware 是一个介于 scrapy 的 Request 和 Response 之间的一个钩子框架,可以用来全局修改 scrapy 的 Request 和 Response,这里处理的是 User-Agent 所以是对 Request 的修改(重载 process_request),Response 修改实际上是和 Request 修改(重载 process_response)是一样的
- 具体实现
首先 settings.py 中的 user_agent_list 配置不变
# ArticleSpider/settings.py
# User-Agent 列表
user_agent_list = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
]
middlewares.py 中编写自定义 middleware
# ArticleSpider/middlewares.py
class RandomUserAgentMiddleware(object):
"""
随机更换 User-Agent
"""
def __init__(self, crawler):
super().__init__()
self.user_agent_list = crawler.settings.get('user_agent_list', [])
@classmethod
def from_crawler(cls, crawler):
return cls(crawler)
def process_request(self, request, spider):
import random
random_agent = random.choice(self.user_agent_list)
request.headers.setdefault('User-Agent', random_agent)
将 RandomUserAgentMiddleware 配置到 settings.py 中
# ArticleSpider/settings.py
DOWNLOADER_MIDDLEWARES = {
# 'ArticleSpider.middlewares.ArticlespiderDownloaderMiddleware': 543,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'ArticleSpider.middlewares.RandomUserAgentMiddleware': 1,
}
现在这样写,已经基本满足需求了,单位一不足的是要自己维护这个 user_agent_list 列表,如果每次新增 User-Agent 的话,就要重启一次爬虫,会显得稍有麻烦
方法 4
使用 GitHub 上开源的库 fake-useragent,来实现随机切换 useragent
- GitHub 地址:https://github.com/hellysmile/fake-useragent
文档给出了使用方法
事实上 fake-useragent 维护了大量的 UserAgent,查看源代码可以发现其维护 UserAgent 的网址
fake-useragent 帮我们在线维护了很多 UserAgent,所以我们就没必要自己去维护了,可以直接拿过来用,将 fake-useragent 集成到 自己定义的 RandomUserAgentMiddleware 中间件中
# ArticleSpider/middlewares.py
from fake_useragent import UserAgent
class RandomUserAgentMiddleware(object):
"""
随机更换 User-Agent
"""
def __init__(self, crawler):
super().__init__()
self.ua = UserAgent()
@classmethod
def from_crawler(cls, crawler):
return cls(crawler)
def process_request(self, request, spider):
request.headers.setdefault('User-Agent', self.ua.random)
还可以对上面的配置做下优化,在 settings.py 中增加一个变量 RANDOM_UA_TYPE 用来改变随机获取的 UserAgent 类型,可以通过这个变量值来设定到底需要获取哪种 UserAgent
# ArticleSpider/settings.py
# 随机 User-Agent 类型配置
RANDOM_USER_AGENT = 'random'
# ArticleSpider/middlewares.py
from fake_useragent import UserAgent
class RandomUserAgentMiddleware(object):
"""
随机更换 User-Agent
"""
def __init__(self, crawler):
super().__init__()
self.ua = UserAgent()
self.ua_type = crawler.settings.get('RANDOM_USER_AGENT', 'random')
@classmethod
def from_crawler(cls, crawler):
return cls(crawler)
def process_request(self, request, spider):
def get_ua():
return getattr(self.ua, self.ua_type) # 获取 self.ua 对象的 self.ua_type 属性的值
request.headers.setdefault('User-Agent', get_ua())
Scrapy 实现 IP 代理池
如果只是切换一条代理,只需要在自定义 middleware 中设置 request.meta['proxy'] 即可,如下,格式为 'http/https://IP地址:port'
request.meta['proxy'] = 'http://120.39.167.238:38806'
实际代码中例子,仍然使用随机更换 UserAgent 的中间件 RandomUserAgentMiddleware 测试
# ArticleSpider/middlewares.py
from fake_useragent import UserAgent
class RandomUserAgentMiddleware(object):
"""
随机更换 User-Agent
"""
def __init__(self, crawler):
super().__init__()
self.ua = UserAgent()
self.ua_type = crawler.settings.get('RANDOM_USER_AGENT', 'random')
@classmethod
def from_crawler(cls, crawler):
return cls(crawler)
def process_request(self, request, spider):
def get_ua():
return getattr(self.ua, self.ua_type) # 获取 self.ua 对象的 self.ua_type 属性的值
request.headers.setdefault('User-Agent', get_ua())
# 设置代理 IP,只适用于设置一条代理 IP 的情况
request.meta['proxy'] = 'http://120.39.167.238:38806'
测试 IP 是否切换成功,编写一个简单的 HTTP Server 文件 simple_http_server.py,这个文件功能很简单,就是在 8088 端口启动一个服务,然后可以打印访问者的 IP 地址
# simple_http_server.py
#!/usr/bin/env python
"""
Very simple HTTP server in python.
Usage::
./dummy-web-server.py []
Send a GET request::
curl http://localhost
Send a HEAD request::
curl -I http://localhost
Send a POST request::
curl -d "foo=bar&bin=baz" http://localhost
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
class S(BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
def do_GET(self):
self._set_headers()
print(self.address_string()) # 打印访问用户的 IP 地址
self.wfile.write("Hello!
".encode("utf-8"))
def do_HEAD(self):
self._set_headers()
def do_POST(self):
# Doesn't do anything with posted data
self._set_headers()
self.wfile.write("POST!
")
def run(server_class=HTTPServer, handler_class=S, port=8088):
server_address = ('', port)
httpd = server_class(server_address, handler_class)
print('Starting httpd...')
httpd.serve_forever()
if __name__ == "__main__":
from sys import argv
if len(argv) == 2:
run(port=int(argv[1]))
else:
run()
在服务器上启动这个 Server
在浏览器中访问效果
启动爬虫测试
可以发现,只需要简单的一个设置,既可以成功切换 Scrapy 爬虫的 IP
但是只是切换一条代理不能满足爬虫需求,只是演示还可以,实际项目中还是要使用 IP 池来随机切换 IP
大致思路:自己写一个爬虫,来爬取分享免费代理 IP 的网址上面可用的 IP,然后放到数据库或文件中,这样就有了 IP 代理的数据源,然后运行爬虫的时候从这些代理中随机取出一个来请求网页,类似之前的随机设置 UserAgent 一个道理,这样就实现了 IP 代理池随机切换 IP
以西刺网站为例进行爬取
首先在项目根目录下创建 tools python package,专门用来存放一些脚本文件,在这里新建一个 crawl_xici_ip.py 爬虫文件,专门用来爬取 西刺网站上免费的 IP 代理
新建表 proxy_ip,用来存放 IP
爬取西刺免费 IP 代理代码实现
# tools/crawl_xici_ip.py
"""
爬取西刺免费 IP 代理
"""
import requests
from scrapy.selector import Selector
import pymysql
conn = pymysql.Connect(host='127.0.0.1', user='pythonic', passwd='pythonic', db='articles', port=3306)
cursor = conn.cursor()
def crawl_ips():
"""
爬取西刺免费 IP 代理
Returns:
None
"""
url = 'http://www.xicidaili.com/nn/{0}'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
}
for i in range(1, 101): # 获取 100 页的数据
response = requests.get(url=url.format(i), headers=headers)
# 使用 Scrapy 的 Selector 来提取数据
selector = Selector(text=response.text)
trs = selector.xpath('//table[@id="ip_list"]//tr')
# trs = selector.css('#ip_list tr')
ip_list = []
for tr in trs[1:]: # 去掉第一行表头
ip = tr.xpath('./td[2]/text()').extract_first()
port = tr.xpath('./td[3]/text()').extract_first()
speed = tr.xpath('./td[7]/div/@title').extract_first().split('秒')[0]
proxy_type = tr.xpath('./td[6]/text()').extract_first()
ip_list.append((ip, port, proxy_type, speed))
print(ip_list)
for ip_info in ip_list:
cursor.execute(
"""
insert into proxy_ip(ip, port, proxy_type, speed)
values('{0}', '{1}', '{2}', '{3}')
""".format(ip_info[0], ip_info[1], ip_info[2], ip_info[3])
) # 注意因为数据库字段中这几个值都为 varchar 类型,所以 format 的时候 values 里面的值又要加引号
conn.commit()
class GetIP(object):
"""
获取 IP
"""
def get_random_ip(self):
"""
从数据库中随机获取一个 IP
Returns:
"""
random_sql = """select ip,port from articles.proxy_ip where proxy_type='HTTP' order by rand() limit 1"""
cursor.execute(random_sql)
ip, port = cursor.fetchone()
judge_result = self.judge_ip(ip, port)
if judge_result:
return 'http://{0}:{1}'.format(ip, port)
else:
return self.get_random_ip()
def judge_ip(self, ip, port):
"""
判断 IP 是否可用
"""
http_url = 'http://www.baidu.com' # 访问百度首页来测试 IP 是否可用
proxy_url = 'http://{0}:{1}'.format(ip, port)
proxy_dict = {
'http': http_url,
# 'https': ''
}
try:
response = requests.get(http_url, proxies=proxy_dict)
except Exception as e:
print('无效 IP')
self.delete_ip(ip)
return False
else:
code = response.status_code
if 200 <= code < 300:
print('可用 IP')
return True
else:
print('无效 IP')
self.delete_ip(ip)
return False
def delete_ip(self, ip):
"""
删除数据库中无效 IP
"""
delete_sql = """delete from proxy_ip where ip='{0}'""".format(ip)
cursor.execute(delete_sql)
conn.commit()
return True
if __name__ == '__main__':
# crawl_ips()
get_ip = GetIP()
print(get_ip.get_random_ip())
数据可以正常写入
定义一个 middleware 用来切换随机代理 IP
# ArticleSpider/middlewares.py
from tools.crawl_xici_ip import GetIP
class RandomProxyMiddleware(object):
"""
随机切换代理 IP
"""
def process_request(self, request, spider):
get_ip = GetIP()
request.meta['proxy'] = get_ip.get_random_ip()
print(request.meta['proxy'])
settings.py 中配置
DOWNLOADER_MIDDLEWARES = {
...
'ArticleSpider.middlewares.RandomProxyMiddleware': 10,
}
这样就可以实现代理池随机切换代理 IP 了
- 几个好用的 随机切换代理 IP 库
scrapy-proxies:https://github.com/aivarsk/scrapy-proxies [免费]
scrapy-crawlera:https://github.com/scrapy-plugins/scrapy-crawlera [收费]
tor:http://www.theonionrouter.com/ (洋葱浏览器)
云打码实现验证码识别
常用验证码识别方法
- 编码实现(ocr 识别):实现过程繁琐,识别准确率低
- 在线打码:折中方式,价格比人工打码便宜很多
- 人工打码:有一些人工打码平台,有很多专门做兼职打码的人,识别效率最高
以在线打码平台 '云打码' 为例:http://www.yundama.com/
云打码需要注册两个账号,一个普通用户账号,一个开发者账户。只有注册开发者账号才可以跟客服申请要测试积分,可以识别几十到上百个验证码
关于普通用户账号和开发者账号,开发者账号是和云打码平台合作的开发者,通过开发者账号开发了一个软件,这个软件使用的是云打码平台的 API,然后卖给很多客户的时候(这个客户就是指普通用户),这个打码是要收费的,每识别一个验证码会收一部分钱,这个钱要分成给开发者
开发文档
注册开发者账号,我的软件-添加新软件,软件名称随便起一个,自动生成软件代码和通讯密钥
注册普通用户,可以在线充值和查看打码记录等
查看之前下载的示例代码
关于验证码类型有很多种类,类型越明确识别成功率越高
对普通用户充值后,测试识别验证码
在普通用户的打码记录里可以查看到刚才运行程序打码的这条记录
可以说这个方案目前来说是很好的解决办法,通常一分钱即可识别一个验证码
cookie禁用、自动限速、自定义spider的settings
- cookie禁用
COOKIES_ENABLED = False # settings.py 中配置禁用 cookies
- 自动限速
Scrapy 默认在每个网页之间下载空隙是为 0 的,也就是不停的去下载网页,事实上 Scrapy 提供了一个扩展,可以动态帮我们设置下载速度
中文文档:https://scrapy-chs.readthedocs.io/zh_CN/latest/topics/autothrottle.html
英文文档:https://doc.scrapy.org/en/latest/topics/autothrottle.html
- 自定义spider的settings
典型应用例子,ArticleSpider 这个项目中,zhihu spider 是不能禁用 cookies 的,禁用的话就不能爬取了,而 jobbole spider 是允许禁用 cookies 的,但是在 settings.py 中只有一个
COOKIES_ENABLED = False
可配置,所以就需要能够自定义每一个 spider 的 setting 功能了,scrapy 已经提供了此功能
经过以上配置,settings.py 中
COOKIES_ENABLED
默认值设为False
自定义每个 spider 的custom_settings
,就可以实现不同 spider 都有各自的配置,不会相互干扰。
事实上custom_settings
中可以设置很多自定义配置,如headers
等