Scrapy

安装

1
$ pip install scrapy -i https://mirrors.ustc.edu.cn/pypi/web/simple

pip install时,如果要临时切换为国内镜像,可以使用-i指定国内镜像地址

常用的国内镜像地址:

中科大:https://mirrors.ustc.edu.cn/pypi/web/simple

清华:https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/

验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scrapy -v
Scrapy 2.11.0 - no active project

Usage:
scrapy <command> [options] [args]

Available commands:
bench Run quick benchmark test
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy

[ more ] More commands available when run from project directory

Use "scrapy <command> -h" to see more info about a command

可以看到,如果在scrapy项目内运行的话,还有更多的命令可用。

创建项目

1
$ scrapy startproject fz_spider

项目结构

1
2
3
4
5
6
7
8
9
fz_spider/
├── __init__.py
├── items.py
├── middlewares.py
├── pipelines.py
├── settings.py
└── spiders
└── __init__.py
└── scrapy.cfg

在fz_spider目录下重新运行scrapy -h,会看到更多可用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ scrapy -h
Scrapy 2.11.0 - active project: fz_spider

Usage:
scrapy <command> [options] [args]

Available commands:
bench Run quick benchmark test
check Check spider contracts
crawl Run a spider
edit Edit spider
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
list List available spiders
parse Parse URL (using its spider) and print the results
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy

Use "scrapy <command> -h" to see more info about a command

这里主要关注crawl命令,通过该命令可以在命令行启动已经编写好的爬虫。

创建爬虫

1
$ scrapy genspider fzapp "abc.com"

这其实创建了一个名为fzapp的爬虫,目标域名为abc.com。会在spiders目录下生成一个名为fzapp.py的文件:

1
2
3
4
5
6
7
8
9
10
import scrapy


class FzappSpider(scrapy.Spider):
name = "fzapp"
allowed_domains = ["abc.com"]
start_urls = ["https://abc.com"]

def parse(self, response):
pass

Python的文件名和类名并没有要求一致,因此可以修改文件名,也可以修改类名。比如可以将文件名修改为fz_app.py。也可以将类名修改为FzAppSpider。

这个文件没什么特殊,也可以不使用genspider命令,直接创建爬虫文件,然后继承scrapy.Spider,并实现parse方法。

这里有三个类变量,分别是name,allowed_domains和start_urls。对我们来说,因为不是爬取固定页面,所以有用的只有name。

name是爬虫的名称,虽然允许修改,但修改后运行时的名称也需要相应修改。例如,如果将name = "fzapp"修改为name = "fz_app",则运行爬虫时,需要修改为scrapy crawl fz_zpp

settings.py

可以在该文件中设置日志:

1
2
LOG_LEVEL = "DEBUG"
LOG_FILE = "./spider.log"

可以控制爬虫是否遵循目标网站的robots.txt。该文件会定义哪些资源是爬虫可爬的,哪些是不允许爬取的。默认是遵顼该约定的,当然也可以设置为False。

1
ROBOTSTXT_OBEY = False

控制并发,默认的并发是16

1
CONCURRENT_REQUESTS = 32

是否启用Telnet Console,不知道这个有什么用,所以就禁掉了。

1
TELNETCONSOLE_ENABLED = False

此外,请求头DEFAULT_REQUEST_HEADERS也在这里设置。

控制重试。默认会进行3次重试。如果不想重试,则使用RETRY_ENABLED = False关闭重试。可以针对特定的HTTP响应进行重试。

1
2
3
RETRY_ENABLED = False
# RETRY_TIMES = 3
# RETRY_HTTP_CODES = [500, 502, 503, 504, 408]

在PyCharm中调试

在Run/Debug Configurations中新建一个Python的配置,将Script path设置为scrapy提供的cmdline.py,可以用everything直接搜索并复制路径。一般是C:\Users\Administrator\AppData\Local\Programs\Python\Python38\Lib\site-packages\scrapy\cmdline.py。将Parameters设置为crawl fz_app。

start_requests方法

我们要爬取的url并不是一个固定的url,而是包含在一个文本文件中的url列表,因此还需要实现start_requests方法。

该方法必须返回一个可迭代对象。该对象包含了spider用于爬取的Request。当spider启动爬取且未指定URL时,该方法被调用。

1
2
3
4
5
6
7
8
9
10
def start_requests(self):
meta = {
# 'dont_redirect': True,
## handle_httpstatus_all:True会处理所有的http status,默认只会处理200-300之间的正确响应码
'handle_httpstatus_all': True
}
with open('other_202309271402', encoding='utf-8') as input_data:
urls = input_data.readlines()
for iurl in urls:
yield Request(url='http://{url}'.format(url=iurl), callback=self.parse, meta=meta)

Request对象有几个需要关注的keyword参数:

一个是url,这个很明显,就是需要爬取的URL。

一个是callback,它指定了回调函数为parse方法。

最后是meta,它指定了爬虫的一些细节,如,是否进行302重定向,是否处理所有的http status code。以处理所有http status code为例,scrapy默认只会处理状态码为200-300之间的正确响应,对于4xx和5xx的响应码会丢弃,也就不会进入parse方法。如果我们想记录那些4xx和5xx的URL,并在事后进行分析,就需要在meta中添加对应的参数配置(handle_httpstatus_all设置为True)。

如何处理异常的请求

即使在start_requests方法中指定了处理所有响应状态,也不能保证不遗漏。当网站无法响应时,会抛出异常,这时候就不会进入spider,而需要额外的中间件进行处理。

可以定义一个ExceptionMiddleware,并在process_exception方法中对异常的站点进行记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class FzRecordExceptionMiddleware:
def process_request(self, request, spider):
return None

def process_response(self, request, response, spider):
return response

def process_exception(self, request, exception, spider):
cfg = ConfigParser()
cfg.read('config.ini')
_storage_root = cfg.get('storage', 'root')
_storage_ex = cfg.get('storage', 'exception')
if not os.path.exists(_storage_ex):
os.makedirs(_storage_ex)

ex_summary_file = os.path.join(_storage_ex, 'exception_summary')
with open(ex_summary_file, encoding='utf-8', mode='a') as ex_response:
ex_response.write('|'.join([request.url, exception.MESSAGE, '\n']))

return None

要使自定义的中间件生效,还需要再settings.py中定义中间件的顺序:

1
2
3
4
DOWNLOADER_MIDDLEWARES = {
# "fz_spider.middlewares.FzSpiderDownloaderMiddleware": 543,
"fz_spider.middlewares.FzRecordExceptionMiddleware": 543,
}

如何定义代理中间件

1
2
3
4
5
6
7
8
9
10
11
class DeepindarkHttpProxyMiddleware:
def __init__(self, proxy):
self._proxy = proxy

@classmethod
def from_crawler(cls, crawler):
return cls(proxy=crawler.settings.get('PROXIES'))

def process_request(self, request, spider):
request.meta['proxy'] = self._proxy
return None

在settings.py中配置:

1
2
3
4
5
6
7
DOWNLOADER_MIDDLEWARES = {
# "deepindark.middlewares.DeepindarkDownloaderMiddleware": 543,
# 增加一个代理中间件,在该中间件中为请求设置试用HTTP代理
"deepindark.middlewares.DeepindarkHttpProxyMiddleware": 543,
# "deepindark.middlewares.DeepindarkUserAgentMiddleware": 544
"deepindark.middlewares.BypassCloudflare": 400
}

Scrapy和requests不一样,requests原生就能支持使用socks5协议进行代理,而Scrapy暂时还不支持。

如何从settings.py中获取值

可以从Crawler对象和Spider对象中获取settings的值。在中间件中,process_request,process_response方法都会提供Spider对象作为参数,此时就可以从Spider对象中获取settings.py中的配置项:

1
2
spider.settings.get('PROXIES')
spider.settings['PROXIES']

而如果使用到from_crawler一类的classmethod,则会提供Crawler对象作为参数:

1
2
3
@classmethod
def from_crawler(cls, crawler):
return cls(proxy=crawler.settings.get('PROXIES'))

此时可以从Crawler对象获取settings.py中的配置项:

1
2
crawler.settings.get('PROXIES')
crawler.settings['PROXIES']

如何绕过Cloudflare的防封策略

需要使用cloudscraper

1
$ pip install cloudscraper -i https://mirrors.ustc.edu.cn/pypi/web/simple

然后一样定义一个中间件:

1
2
3
4
5
6
7
8
9
class BypassCloudflare:
def process_response(self, request, response, spider):
if response.status == 403:
if spider.name == 'onion666':
url = request.url
rsp = spider.browser.get(url, proxies={'http': spider.settings['PROXIES'],
'https': spider.settings['PROXIES']}, headers={'referer': url})
return HtmlResponse(url=url, body=rsp.text, encoding="utf-8", request=request)
return response

因为Cloudflare会对爬虫直接返回403,所以使用状态码进行判断。随着爬取的网站变多,spider.name的判断也需要扩充。

spider.browser是定义在对应爬虫中的一个变量,该变量是一个CloudScraper对象实例。

1
2
3
4
5
6
7
class Onion666Spider(scrapy.Spider):
"""
爬取onion666列出的所有站点
"""
name = "onion666"
start_urls = ['http://666666666tjjjeweu5iikuj7hkpke5phvdylcless7g4dn6vma2xxcad.onion/']
browser = cloudscraper.create_scraper()

request.meta和request.headers

meta中保存的是和Scrapy相关的内容,如:download_timeout是默认携带的,控制下载超时时间,proxy设置代理,dont_redirect控制是否允许重定向,handle_httpstatus_all控制是否处理所有的http状态码。

而headers保存的是和HTTP请求相关的内容,如:User-Agent设置浏览器信息从而绕过一些封堵检测,Content-Type设置允许接受什么类型的响应。