|
| 1 | +# 自定义下载器 |
| 2 | + |
| 3 | +下载器一共分为三种:**普通下载器**、**支持保持session的下载器**以及**浏览器渲染下载器**。默认已经在框架中内置,setting中的配置如下 |
| 4 | + |
| 5 | +``` |
| 6 | +DOWNLOADER = "feapder.network.downloader.RequestsDownloader" # 请求下载器 |
| 7 | +SESSION_DOWNLOADER = "feapder.network.downloader.RequestsSessionDownloader" |
| 8 | +RENDER_DOWNLOADER = "feapder.network.downloader.SeleniumDownloader" # 渲染下载器 |
| 9 | +``` |
| 10 | + |
| 11 | +- session下载器当配置中`USE_SESSION = True`时会启用 |
| 12 | +- 渲染下载器当使用浏览器下载功能时会启用 |
| 13 | + |
| 14 | +这些下载器均为插件的形式,我们可以自定义 |
| 15 | + |
| 16 | +## 自定义普通下载器 |
| 17 | + |
| 18 | +1. 编写下载器。如在 `xxx-spider/downloader/my_downloader.py `下自定义了如下下载器 |
| 19 | + |
| 20 | + ``` |
| 21 | + import requests |
| 22 | + |
| 23 | + from feapder.network.downloader.base import Downloader |
| 24 | + from feapder.network.response import Response |
| 25 | + |
| 26 | + class RequestsDownloader(Downloader): |
| 27 | + def download(self, request) -> Response: |
| 28 | + response = requests.request( |
| 29 | + request.method, request.url, **request.requests_kwargs |
| 30 | + ) |
| 31 | + # 将requests的response转化为feapder的Response 对象,方便后续解析时使用xpath、re等方法 |
| 32 | + response = Response(response) |
| 33 | + return response |
| 34 | + ``` |
| 35 | + |
| 36 | + 注:这里返回的response对象不强制要求为是feapder的Response。返回值会传到解析函数的response参数里,若返回的是文本,则接收到的也是文本。 |
| 37 | + |
| 38 | + 但为了代码可读性,建议将返回值转为feapder的Response后再返回。 |
| 39 | + |
| 40 | + 转feapder的Response的方式有如下几种 |
| 41 | + |
| 42 | + ``` |
| 43 | + # 方式1 |
| 44 | + # response参数为reqeusts的response |
| 45 | + Response(response) |
| 46 | + |
| 47 | + # 方式2 |
| 48 | + Response.from_text(text="html内容") |
| 49 | + ``` |
| 50 | +
|
| 51 | +2. 在settings中指定下载器 |
| 52 | +
|
| 53 | + ``` |
| 54 | + DOWNLOADER = "downloader.my_downloader.RequestsDownloader" |
| 55 | + ``` |
| 56 | +
|
| 57 | +## 自定义session下载器 |
| 58 | +
|
| 59 | +1. 和普通下载器一样,都是继承`Downloader`,如何保持session,可自定义。代码示例 `xxx-spider/downloader/my_downloader.py ` |
| 60 | +
|
| 61 | + ``` |
| 62 | + class RequestsSessionDownloader(Downloader): |
| 63 | + session = None |
| 64 | + |
| 65 | + @property |
| 66 | + def _session(self): |
| 67 | + if not self.__class__.session: |
| 68 | + self.__class__.session = requests.Session() |
| 69 | + # pool_connections – 缓存的 urllib3 连接池个数 pool_maxsize – 连接池中保存的最大连接数 |
| 70 | + http_adapter = HTTPAdapter(pool_connections=1000, pool_maxsize=1000) |
| 71 | + # 任何使用该session会话的 HTTP 请求,只要其 URL 是以给定的前缀开头,该传输适配器就会被使用到。 |
| 72 | + self.__class__.session.mount("http", http_adapter) |
| 73 | + |
| 74 | + return self.__class__.session |
| 75 | + |
| 76 | + def download(self, request) -> Response: |
| 77 | + response = self._session.request( |
| 78 | + request.method, request.url, **request.requests_kwargs |
| 79 | + ) |
| 80 | + response = Response(response) |
| 81 | + return response |
| 82 | + ``` |
| 83 | +
|
| 84 | +2. 在settings中指定下载器 |
| 85 | +
|
| 86 | + ``` |
| 87 | + SESSION_DOWNLOADER = "downloader.my_downloader.RequestsSessionDownloader" |
| 88 | + ``` |
| 89 | +
|
| 90 | +注意,这里要配置 `SESSION_DOWNLOADER` |
| 91 | +
|
| 92 | +## 自定义浏览器渲染下载器 |
| 93 | +
|
| 94 | +1. 编写下载器 `xxx-spider/downloader/my_downloader.py ` |
| 95 | +
|
| 96 | +**若浏览器框架本身不支持多线程,但想在多线程中使用,如playwright使用,参考如下:** |
| 97 | +
|
| 98 | +``` |
| 99 | +import feapder.setting as setting |
| 100 | +import feapder.utils.tools as tools |
| 101 | +from feapder.network.downloader.base import RenderDownloader |
| 102 | +from feapder.network.response import Response |
| 103 | +from feapder.utils.webdriver import WebDriverPool, PlaywrightDriver |
| 104 | + |
| 105 | + |
| 106 | +class MyDownloader(RenderDownloader): |
| 107 | + webdriver_pool: WebDriverPool = None |
| 108 | + |
| 109 | + @property |
| 110 | + def _webdriver_pool(self): |
| 111 | + if not self.__class__.webdriver_pool: |
| 112 | + self.__class__.webdriver_pool = WebDriverPool( |
| 113 | + **setting.PLAYWRIGHT, driver_cls=PlaywrightDriver, thread_safe=True |
| 114 | + ) |
| 115 | + |
| 116 | + return self.__class__.webdriver_pool |
| 117 | + |
| 118 | + def download(self, request) -> Response: |
| 119 | + # 代理优先级 自定义 > 配置文件 > 随机 |
| 120 | + if request.custom_proxies: |
| 121 | + proxy = request.get_proxy() |
| 122 | + elif setting.PLAYWRIGHT.get("proxy"): |
| 123 | + proxy = setting.PLAYWRIGHT.get("proxy") |
| 124 | + else: |
| 125 | + proxy = request.get_proxy() |
| 126 | + |
| 127 | + # user_agent优先级 自定义 > 配置文件 > 随机 |
| 128 | + if request.custom_ua: |
| 129 | + user_agent = request.get_user_agent() |
| 130 | + elif setting.PLAYWRIGHT.get("user_agent"): |
| 131 | + user_agent = setting.PLAYWRIGHT.get("user_agent") |
| 132 | + else: |
| 133 | + user_agent = request.get_user_agent() |
| 134 | + |
| 135 | + cookies = request.get_cookies() |
| 136 | + url = request.url |
| 137 | + render_time = request.render_time or setting.PLAYWRIGHT.get("render_time") |
| 138 | + wait_until = setting.PLAYWRIGHT.get("wait_until") or "domcontentloaded" |
| 139 | + if request.get_params(): |
| 140 | + url = tools.joint_url(url, request.get_params()) |
| 141 | + |
| 142 | + driver: PlaywrightDriver = self._webdriver_pool.get( |
| 143 | + user_agent=user_agent, proxy=proxy |
| 144 | + ) |
| 145 | + try: |
| 146 | + if cookies: |
| 147 | + driver.url = url |
| 148 | + driver.cookies = cookies |
| 149 | + driver.page.goto(url, wait_until=wait_until) |
| 150 | + |
| 151 | + if render_time: |
| 152 | + tools.delay_time(render_time) |
| 153 | + |
| 154 | + html = driver.page.content() |
| 155 | + response = Response.from_dict( |
| 156 | + { |
| 157 | + "url": driver.page.url, |
| 158 | + "cookies": driver.cookies, |
| 159 | + "_content": html.encode(), |
| 160 | + "status_code": 200, |
| 161 | + "elapsed": 666, |
| 162 | + "headers": { |
| 163 | + "User-Agent": driver.user_agent, |
| 164 | + "Cookie": tools.cookies2str(driver.cookies), |
| 165 | + }, |
| 166 | + } |
| 167 | + ) |
| 168 | + |
| 169 | + response.driver = driver |
| 170 | + response.browser = driver |
| 171 | + return response |
| 172 | + except Exception as e: |
| 173 | + self._webdriver_pool.remove(driver) |
| 174 | + raise e |
| 175 | + |
| 176 | + def close(self, driver): |
| 177 | + if driver: |
| 178 | + self._webdriver_pool.remove(driver) |
| 179 | + |
| 180 | + def put_back(self, driver): |
| 181 | + """ |
| 182 | + 释放浏览器对象 |
| 183 | + """ |
| 184 | + self._webdriver_pool.put(driver) |
| 185 | + |
| 186 | + def close_all(self): |
| 187 | + """ |
| 188 | + 关闭所有浏览器 |
| 189 | + """ |
| 190 | + # 不支持 |
| 191 | + # self._webdriver_pool.close() |
| 192 | + pass |
| 193 | +``` |
| 194 | +
|
| 195 | +这里使用了WebDriverPool,参数`thread_safe=True`,即要保证使用时的线程安全,确保同个浏览器对象只能被同一个线程调用 |
| 196 | +
|
| 197 | +**若浏览器框架本身支持多线程,如selenium,则参考如下** |
| 198 | +
|
| 199 | +``` |
| 200 | +import feapder.setting as setting |
| 201 | +import feapder.utils.tools as tools |
| 202 | +from feapder.network.downloader.base import RenderDownloader |
| 203 | +from feapder.network.response import Response |
| 204 | +from feapder.utils.webdriver import WebDriverPool, SeleniumDriver |
| 205 | + |
| 206 | + |
| 207 | +class MyDownloader(RenderDownloader): |
| 208 | + webdriver_pool: WebDriverPool = None |
| 209 | + |
| 210 | + @property |
| 211 | + def _webdriver_pool(self): |
| 212 | + if not self.__class__.webdriver_pool: |
| 213 | + self.__class__.webdriver_pool = WebDriverPool( |
| 214 | + **setting.WEBDRIVER, driver=SeleniumDriver |
| 215 | + ) |
| 216 | + |
| 217 | + return self.__class__.webdriver_pool |
| 218 | + |
| 219 | + def download(self, request) -> Response: |
| 220 | + # 代理优先级 自定义 > 配置文件 > 随机 |
| 221 | + if request.custom_proxies: |
| 222 | + proxy = request.get_proxy() |
| 223 | + elif setting.WEBDRIVER.get("proxy"): |
| 224 | + proxy = setting.WEBDRIVER.get("proxy") |
| 225 | + else: |
| 226 | + proxy = request.get_proxy() |
| 227 | + |
| 228 | + # user_agent优先级 自定义 > 配置文件 > 随机 |
| 229 | + if request.custom_ua: |
| 230 | + user_agent = request.get_user_agent() |
| 231 | + elif setting.WEBDRIVER.get("user_agent"): |
| 232 | + user_agent = setting.WEBDRIVER.get("user_agent") |
| 233 | + else: |
| 234 | + user_agent = request.get_user_agent() |
| 235 | + |
| 236 | + cookies = request.get_cookies() |
| 237 | + url = request.url |
| 238 | + render_time = request.render_time or setting.WEBDRIVER.get("render_time") |
| 239 | + if request.get_params(): |
| 240 | + url = tools.joint_url(url, request.get_params()) |
| 241 | + |
| 242 | + browser: SeleniumDriver = self._webdriver_pool.get( |
| 243 | + user_agent=user_agent, proxy=proxy |
| 244 | + ) |
| 245 | + try: |
| 246 | + browser.get(url) |
| 247 | + if cookies: |
| 248 | + browser.cookies = cookies |
| 249 | + # 刷新使cookie生效 |
| 250 | + browser.get(url) |
| 251 | + |
| 252 | + if render_time: |
| 253 | + tools.delay_time(render_time) |
| 254 | + |
| 255 | + html = browser.page_source |
| 256 | + response = Response.from_dict( |
| 257 | + { |
| 258 | + "url": browser.current_url, |
| 259 | + "cookies": browser.cookies, |
| 260 | + "_content": html.encode(), |
| 261 | + "status_code": 200, |
| 262 | + "elapsed": 666, |
| 263 | + "headers": { |
| 264 | + "User-Agent": browser.user_agent, |
| 265 | + "Cookie": tools.cookies2str(browser.cookies), |
| 266 | + }, |
| 267 | + } |
| 268 | + ) |
| 269 | + |
| 270 | + response.driver = browser |
| 271 | + response.browser = browser |
| 272 | + return response |
| 273 | + except Exception as e: |
| 274 | + self._webdriver_pool.remove(browser) |
| 275 | + raise e |
| 276 | + |
| 277 | + def close(self, driver): |
| 278 | + if driver: |
| 279 | + self._webdriver_pool.remove(driver) |
| 280 | + |
| 281 | + def put_back(self, driver): |
| 282 | + """ |
| 283 | + 释放浏览器对象 |
| 284 | + """ |
| 285 | + self._webdriver_pool.put(driver) |
| 286 | + |
| 287 | + def close_all(self): |
| 288 | + """ |
| 289 | + 关闭所有浏览器 |
| 290 | + """ |
| 291 | + self._webdriver_pool.close() |
| 292 | +``` |
| 293 | +
|
| 294 | +2. 在settings中指定下载器 |
| 295 | +
|
| 296 | +``` |
| 297 | +RENDER_DOWNLOADER = "downloader.my_downloader.MyDownloader" |
| 298 | +``` |
| 299 | +
|
| 300 | +注,这里要写`RENDER_DOWNLOADER` |
0 commit comments