Python11 ---- Scrapy框架基础
Scrapy框架基础
路径管理
路径
-
绝对路径
总是从根目录开始H:\PyCharmProjects\tutorials_2\jd_crawler\main.py
-
相对路径
jd_crawler\main.py
-
.
和..
.
代表当前目录,..
代表父目录
-
-
工作目录
当前执行命令所在的目录
# 将工作目录添加进当前的路径列表 sys.path.append(os.getcwd())
路径列表
-
查看当前路径列表
只有在路径列表当中的包和模块才可以导入和调用import sys print(sys.path)
-
路径搜索顺序
- 当前脚本路径, 也就是执行文件的目录
PYTHONPATH
路径- 虚拟环境路径
site-packages
- 安装的第三方库所在路径
-
可以向路径列表添加路径
sys.path.append(r"H:\PyCharmProjects\tutorials_2")
常见报错
-
ModuleNotFoundError: No module named 'xxxx'
-
为什么在pycharm中不报错, 在命令行当中报错
Pycharm会自动将当前项目的根目录添加到路径列表当中
-
-
ModuleNotFoundError: No module named 'parser.search'; 'parser' is not a pac kage
-
自定义包和内置包名有冲突
修改包名即可
-
导入的不是一个包
-
-
ModuleNotFoundError: No module named '__main__.jd_parser'; '__main__' is no t a package
-
入口程序不可以使用相对路径
-
__main__
主程序模块名会被修改为
__main__
-
-
ValueError: attempted relative import beyond top-level package
当前访问路径已经超过了python已知的最大路径
from tutorial_2.jd_crawler.jd_parser.search import parse_jd_item top-level package 指的是上述from导入命令中的首路径tutorial_2, 而不是根据目录结构
- 把工作目录加入到路径列表当中
- 进入到项目根目录下执行命令
- 上述两个操作相当于将项目根目录加入到路径列表当中
注意事项
- 确定入口程序, 没有一个锚定的路径就没有办法做相对路径的管理
- 将项目根目录加入到入口程序当中
- 进入到项目根目录下执行命令
- 项目目录结构不要嵌套的太深
- 脚本文件或者临时运行单个模块中的方法, 可以将根目录临时添加到路径列表当中
课后作业
- 用命令行启动
jd_crawler
- 在
/test
目录中增加parser_test.py
模块做解析测试.
简单来说就是获取当前的工作目录,在执行程序
import sys
# print(sys.path)
import os
# 添加工作路径至环境变量中
sys.path.append(os.getcwd())
# print(sys.path)
from jd_parser.search import parse_jd_item
with open(r"test/search.html", "r", encoding="utf-8") as f:
html = f.read()
result = parse_jd_item(html)
print(result)
D:\python> python .\jd_crawler\main.py
Scrapy爬虫框架介绍
-
文档
-
什么是scrapy
基于twisted
搭建的异步爬虫框架.
scrapy爬虫框架根据组件化设计理念和丰富的中间件, 使其成为了一个兼具高性能和高扩展的框架 -
scrapy提供的主要功能
- 具有优先级功能的调度器
- 去重功能
- 失败后的重试机制
- 并发限制
- ip使用次数限制
- ....
-
scrapy的使用场景
- 不适合scrapy项目的场景
- 业务非常简单, 对性能要求也没有那么高, 那么我们写多进程, 多线程, 异步脚本即可.
- 业务非常复杂, 请求之间有顺序和失效时间的限制.
- 如果你不遵守框架的主要设计理念, 那就不要使用框架
- 适合使用scrapy项目
- 数据量大, 对性能有一定要求, 又需要用到去重功能和优先级功能的调度器
- 不适合scrapy项目的场景
-
scrapy组件
ENGINE
从SPIDERS
中获取初始请求任务Requests
ENGINE
得到Requests
之后发送给SCHEDULER
,SCHEDULER
对请求进行调度后产出任务.Scheduler
返回下一个请求任务给ENGINE
ENGINE
将请求任务交给DOWNLOADER
去完成下载任务, 途径下载器中间件.- 一旦下载器完成请求任务, 将产生一个
Response
对象给ENGINE
, 途径下载器中间件 ENGINE
收到Response
对象后, 将该对象发送给SPIDERS
去解析和处理, 途径爬虫中间件SPIDER
解析返回结果- 将解析结果
ITEMS
发送给ENGINE
- 生成一个新的
REQUESTS
任务发送给ENGINE
- 将解析结果
- 如果
ENGINE
拿到的是ITEMS
, 那么就会发送给ITEM PIPELINES
做数据处理, 如果是REQUESTS
则发送给SCHEDULER
- 周而复始, 直到没有任务产出
Scrapy教程
-
安装
pip install scrapy
-
创建项目 建议创建一个独立的项目
scrapy startproject jd_crawler_scrapy
-
目录结构
-
spiders(目录) 建议:初学者前期可分开,后期有需求在合并
存放SPIDERS
项目文件, 一个scrapy项目下可以有多个爬虫实例 -
items
解析后的结构化结果.一种约束,必要值 -
middlewares
下载器中间件和爬虫中间件的地方 -
piplines
处理items的组件, 一般都在pipelines中完成items插入数据表的操作 -
settings
统一化的全局爬虫配置文件 -
scrapy.cfg
项目配置文件
-
-
scrapy爬虫demo
import scrapy class JdSearch(scrapy.Spider): name = "jd_search" def start_requests(self): for keyword in ["鼠标", "键盘", "显卡", "耳机"]: for page_num in range(1, 11): url = f"https://search.jd.com/Search?keyword={keyword}&page={page_num}" # 选用FormRequest是因为它既可以发送GET请求, 又可以发送POST请求 yield scrapy.FormRequest( url=url, method='GET', # formdata=data, # 如果是post请求, 携带数据使用formdata参数 callback=self.parse_search # 指定回调函数处理response对象 ) def parse_search(self, response): print(response)
-
启动爬虫
需要到Scrapy根目录下去执行
D:\python\jd_crawler_scrapy> scrapy crawl jd_search
## 课后作业
- 背诵`scrapy`组件流程(必考)
- 完成scrapy项目的demo
## Scrapy的启动和debug
- 命令行
scrapy crawl jd_search
- 启动脚本
新建run.py
from scrapy import cmdline
command = "scrapy crawl jd_search".split()
cmdline.execute(command)
## Scrapy Item
只是对解析的结构化结果进行一个约束, 在到达pipeline前就可以检查出数据错误.
## Scrapy的设置
- ***ROBOTTEXT_OBEY**
ROBOTTEXT_OBEY=False
获取对方网站是否允许爬虫获取数据的信息.
- **设置中间件**
数字越小, 离`ENGINE`越近
DOWNLOADER_MIDDLEWARES = {
# 'jd_crawler_scrapy.middlewares.JdCrawlerScrapyDownloaderMiddleware': 543,
'jd_crawler_scrapy.middlewares.UAMiddleware': 100,
}
- **设置PIPELINE**
ITEM_PIPELINES = {
'jd_crawler_scrapy.pipelines.JdCrawlerScrapyPipeline': 300,
}
- **请求限制**
- ***CONCURRENT_REQUESTS**
请求并发数, 通过控制请求并发数达到避免或者延缓IP被封禁
假设值为32,1秒浏览32个页面,这一般是不可能的
```
CONCURRENT_REQUESTS = 1
```
- CONCURRENT_REQUESTS_PER_DOMAIN
控制每个`域名`请求的并发数
若队列是混合队列可使用此值,可以控制每一个域名的并发数,一般不会这样做,场景较少
- CONCURRENT_REQUESTS_IP
控制每个`IP`请求的次数. 通过这样的方式可以过掉一些对IP封禁严格的网站
假设有一个IP地址池,可以对IP进行并发次数的控制,简单来说就是IP请求次数上限控制
- CONCURRENT_ITEMS
默认为100, 控制处理`item`的并发数. 如果我存入的数据库性能比较差, 通过这样的方式解决防止数据库崩溃的情况(控制存入数据库并发数)
- ***DOWNLOAD_DELAY**
默认为0, 控制请求的频率. **在调度完一个请求后, 休息若干秒**. timesleep延迟
> Scrapy会自动帮我们进行随机休息 (DOWNLOAD_DELAY - 0.5, DOWNLOAD_DELAY + 0.5)
```
DOWNLOAD_DELAY = 2
```
- ***DOWNLOAD_TIMEOUT**
**控制每个请求的超时时间**. 通过这样的方式解决IP代理池质量差的问题.
```
# 根据自己的IP代理池质量自定决定
DOWNLOAD_TIMEOUT = 6
```
- ***REDIRECT_ENABLE**
默认为`True`, **建议修改为`False`**, 因为大部分情况下, 重定向都是识别出你当前身份有问题, 重定向到`log in`页面
- 重试机制
- ***RETRY_ENABLE**
```
RETRY_ENABLE = False
```
默认为`True`, 建议改成`False`, 然后自己重写重试中间件
- RETRY_TIMES
控制重新次数, RETRY_TIMES其实是当前项目的兜底配置
> **如果当前请求失败后永远会重试**, 正好你请求的接口是收费的, 万一有一天报错, 那么产生的费用是巨大的.
```
RETRY_TIMES = 3
```
- RETRY_HTTP_CODES
408 请求超时
429 太多请求
500 无处处理该请求
502 后端服务器问题
503 服务器过载,拒绝客户端连接或在队列中
504 后端服务器问题
```
RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429]
```
- 过滤器
- **设置中指定过滤器**
```
DUPEFILTER_CLASS = "jd_crawler_scrapy.middlewares.MyRFPDupeFilter"
```
- **Spider中打开过滤器**
```
yield scrapy.FormRequest(
dont_filter=False,
url=url,
method='GET',
# formdata=data,
callback=self.parse_search
)
```
- 过滤器
```
from scrapy.dupefilters import RFPDupeFilter
from w3lib.url import canonicalize_url
from scrapy.utils.python import to_bytes
import hashlib
import weakref
class MyRFPDupeFilter(RFPDupeFilter):
"""
过滤器是在到达下载器之前就生成了过滤指纹, 如果我们的下载器中间件报错了, 那么过滤指纹仍然生效, 但是没有实际请求.
所以我们可以通过一些特殊参数来进行自定义过滤规则
"""
def request_fingerprint(self, request, include_headers=None, keep_fragments=False):
cache = _fingerprint_cache.setdefault(request, {})
cache_key = (include_headers, keep_fragments)
if cache_key not in cache:
fp = hashlib.sha1()
fp.update(to_bytes(request.method))
fp.update(to_bytes(canonicalize_url(request.url, keep_fragments=keep_fragments)))
fp.update(request.body or b'')
fp.update(request.meta.get("batch_no", "").encode("utf-8"))
cache[cache_key] = fp.hexdigest()
return cache[cache_key]
```
- LOG
- LOG_ENABLE
默认为`True`, 是否使用log
- LOG_FILE
设置保存的log文件目录
- LOG_LEVEL(按严重程序排序)
- CRITICAL
- ERROR
- WARNING
- INFO
- DEBUG
## Scrapy的中间件
- 请求头中间件
class UAMiddleware:
def process_request(self, request, spider):
request.headers["user-agent"] = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"
- 重试中间件
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message
class MyRetryMiddleware(RetryMiddleware):
"""
解决对方服务器返回正常状态码200, 但是根据IP需要进行验证码验证的情况.
我们可以通过换IP可以解决验证码, 那么就应该重试.
"""
def process_response(self, request, response, spider):
if request.meta.get('dont_retry', False):
return response
if "验证码" in response.text:
reason = response_status_message(response.status)
return self._retry(request, reason, spider) or response
return response
## 课后作业
- 将jd_crawler_scrapy完善.
- 完成代理中间件的编写(查阅文档).
- 理解并重写重试中间件
- 理解并重写过滤器