2025, Dec 29 00:02
Как исправить ошибку Too many open files в Selenium на Linux
Как исправить Too many open files в Selenium на Linux: утечки pyvirtualdisplay, корректное закрытие, повышение ulimit и пул переиспользуемых WebDriver.
Долгие сессии Selenium в Linux нередко падают не из‑за ошибок в вашей логике, а потому, что операционная система решает: хватит. Если парсер работает часами, а затем внезапно останавливается с невнятным сообщением, скорее всего, где‑то течёт ресурс. Признак очевиден.
OSError: [Errno 24] Too many open files
В этом руководстве разберём точный характер сбоя, что именно его провоцирует в связке Selenium и pyvirtualdisplay, и как убрать утечки: закрывать всё детерминированно, переиспользовать виртуальный дисплей, поднять ulimit и, где уместно, перейти на пул переиспользуемых WebDriver’ов.
Постановка проблемы
Сценарий: безголовый парсер перебирает поисковые запросы, запускает Chrome‑драйвер с прокси‑аутентификацией и использует pyvirtualdisplay в Linux. Через 10 часов он падал с «Too many open files». Ниже — показательная версия кода с той же структурой и поведением, но другими именами.
import logging
import time
import random
import platform
import traceback
import zipfile
from datetime import datetime
from selenium.webdriver.common.by import By
from fake_useragent import UserAgent
import undetected_chromedriver as uc
from selenium_stealth import stealth
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("scan_worker.log"),
logging.StreamHandler()
]
)
log = logging.getLogger('ScanLogger')
def pick_ua_chrome():
ua = UserAgent(browsers='chrome', os='windows', platforms='pc')
return ua.random
def make_driver(proxy_conf: dict, SHOW_UI=False):
chr_opts = uc.ChromeOptions()
if not SHOW_UI:
chr_opts.add_argument('--headless')
chr_opts.add_argument('--no-sandbox')
chr_opts.add_argument('--disable-dev-shm-usage')
chr_opts.add_argument('--start-maximized')
chr_opts.add_argument('--disable-blink-features=AutomationControlled')
chr_opts.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
manifest_json = """
{
"version": "1.0.0",
"manifest_version": 2,
"name": "Chrome Proxy",
"permissions": [
"proxy",
"tabs",
"unlimitedStorage",
"storage",
"<all_urls>",
"webRequest",
"webRequestBlocking"
],
"background": {
"scripts": ["background.js"]
},
"minimum_chrome_version":"22.0.0"
}
"""
background_js = """
var config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "http",
host: "%s",
port: parseInt(%s)
},
bypassList: ["localhost"]
}
};
chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});
function callbackFn(details) {
return {
authCredentials: {
username: "%s",
password: "%s"
}
};
}
chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{urls: ["<all_urls>"]},
['blocking']
);
""" % (proxy_conf["proxy"], proxy_conf["proxy_port"], proxy_conf["proxy_login"], proxy_conf["proxy_password"])
if proxy_conf:
ext_zip = 'proxy_auth_plugin.zip'
with zipfile.ZipFile(ext_zip, 'w') as zp:
zp.writestr("manifest.json", manifest_json)
zp.writestr("background.js", background_js)
chr_opts.add_extension(ext_zip)
browser = uc.Chrome(chrome_options=chr_opts)
stealth(browser,
languages=["ru-RU", "ru", "en-US", "en"],
vendor="Google Inc.",
platform="Linux x86_64",
webgl_vendor="Intel Inc.",
renderer="Intel Open Source Technology Center Mesa DRI Intel(R) UHD Graphics 620 (KBL GT2)",
fix_hairline=True,
)
return browser
class YaSearchScraper:
def __init__(self, proxy_conf, SHOW_UI=False, SAVE_SHOT=False):
print(f"SHOW_UI={SHOW_UI} SAVE_SHOT={SAVE_SHOT}")
self.save_snap = SAVE_SHOT
self.proxy_conf = proxy_conf
self.show_ui = SHOW_UI
self.browser = None
self.vdisplay = None
self._boot_driver()
def _boot_driver(self):
if platform.system() == 'Darwin':
self._chr_opts = uc.ChromeOptions()
self._chr_opts.add_argument('--headless=new')
self._chr_opts.add_argument('--disable-gpu')
self._chr_opts.add_argument('--no-sandbox')
self._chr_opts.add_argument('--disable-dev-shm-usage')
self.browser = make_driver(proxy_conf=self.proxy_conf, SHOW_UI=self.show_ui)
else:
from pyvirtualdisplay import Display
self.vdisplay = Display(visible=0, size=(800, 600))
self.vdisplay.start()
self.browser = make_driver(proxy_conf=self.proxy_conf, SHOW_UI=self.show_ui)
self.browser.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
'''
})
self.browser.get("https://ya.ru/")
time.sleep(random.uniform(1.5, 3.5))
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.shutdown()
def shutdown(self):
try:
if self.browser:
self.browser.quit()
self.browser = None
except Exception as e:
log.error(f"Error closing driver: {e}")
try:
if self.vdisplay:
self.vdisplay.stop()
self.vdisplay = None
except Exception as e:
log.error(f"Error stopping display: {e}")
def tick_captcha(self):
now_str = str(datetime.now()).replace(' ', '_')
if "showcaptcha" in self.browser.current_url:
log.info("Captcha found")
if self.save_snap:
self.browser.save_screenshot(f'screens/img_captcha_{now_str}.png')
btns = self.browser.find_elements(By.XPATH, "//input[@class='CheckboxCaptcha-Button']")
if btns:
btns[0].click()
log.info("Button clicked")
time.sleep(15)
if self.save_snap:
self.browser.save_screenshot(f'screens/img_captcha_afterclick_{now_str}.png')
elif self.save_snap:
self.browser.save_screenshot(f'screens/img_{now_str}.png')
def harvest(self, title: str):
log.info(f"Start parse {title}")
found = []
try:
self.browser.get(f"https://ya.ru/search/?text={title}&lr=213&search_source=yaru_desktop_common&search_domain=yaru")
self.tick_captcha()
for i in range(1, 5):
found.extend(self.harvest_page(page_id=i))
self.flip_page()
self.tick_captcha()
time.sleep(random.uniform(2, 5))
except Exception:
log.error(f"Exception in {traceback.format_exc()}")
finally:
log.info(f"Found {len(found)} for film {title}: {found}")
def harvest_page(self, page_id):
acc = []
nodes = self.browser.find_elements(By.XPATH, value='//a[@class="Link Link_theme_normal OrganicTitle-Link organic__url link"]')
for node in nodes:
href = node.get_attribute("href")
if href and "yabs.yandex.ru" not in href:
acc.append(href)
log.info(f"Found {len(acc)} urls on page {page_id}")
return acc
def flip_page(self):
nxt = self.browser.find_elements(By.XPATH, '//div[@class="Pager-ListItem Pager-ListItem_type_next"]')
if nxt:
nxt[0].click()
time.sleep(random.uniform(3, 6))
if __name__ == "__main__":
proxy_book = [
# Данные прокси
]
titles = ["Terminator smotret", "Sasha Tanya smotret", "John Wick smotret onlayn"]
turn = 0
while True:
try:
with YaSearchScraper(SHOW_UI=False, proxy_conf=proxy_book[turn % 2]) as runner:
item = titles[turn]
turn = (turn + 1) % len(titles)
runner.harvest(item)
time.sleep(random.uniform(8, 15))
except Exception:
log.error(f"Exception {traceback.format_exc()}")
time.sleep(1)
Почему число дескрипторов файлов растёт
Это классический случай накопления ресурсов при длинных прогонах. Три точки давления. Во‑первых, каждый раз создаются новые виртуальные дисплеи через pyvirtualdisplay.Display, и они не всегда корректно глушатся — процесс накапливает открытые дескрипторы. Во‑вторых, Selenium и Chrome сами по себе держат множество дескрипторов, поэтому даже небольшой «подтёк» быстро набегает. Наконец, Linux навешивает предельное число открытых файлов на процесс, обычно 1024, и при часах работы петли этот лимит пробивается.
Как остановить утечку и сохранить стабильность
Решение послойное. Сначала сделайте завершение работы безотказным, чтобы каждый цикл гарантированно чистил драйверы, Xvfb и временные артефакты вроде прокси‑расширения. Затем снизьте churn — переиспользуйте дисплей вместо создания на каждом заходе. После этого поднимите системные лимиты до разумных для вашей нагрузки. Добавьте явный сборщик мусора и небольшие гигиенические флаги в Chrome, а также периодические «передышки», чтобы система успевала подчищать хвосты. И, наконец, там, где подходит, используйте пул драйверов, чтобы вообще не плодить их бесконечно.
Укрепляем путь завершения
Убедитесь, что все ресурсы закрываются, и удаляйте файл расширения, если он создавался. Ниже — доработанный метод с упорядоченным выключением и удалением zip.
def shutdown(self):
if self.browser:
try:
self.browser.quit()
except Exception as e:
log.error(f"Error closing driver: {e}")
finally:
self.browser = None
if self.vdisplay:
try:
self.vdisplay.stop()
except Exception as e:
log.error(f"Error stopping display: {e}")
finally:
self.vdisplay = None
import os
try:
ext_zip = 'proxy_auth_plugin.zip'
if os.path.exists(ext_zip):
os.remove(ext_zip)
except Exception as e:
log.error(f"Error removing plugin file: {e}")
Переиспользуйте виртуальный дисплей
Создавать новый Xvfb в каждом цикле расточительно. Одиночка на уровне класса предотвращает появление дисплея на каждый прогон. Дисплей запускается один раз и шарится между экземплярами.
class YaSearchScraper:
_shared_display = None
def _boot_driver(self):
if platform.system() == 'Darwin':
self._chr_opts = uc.ChromeOptions()
self._chr_opts.add_argument('--headless=new')
self._chr_opts.add_argument('--disable-gpu')
self._chr_opts.add_argument('--no-sandbox')
self._chr_opts.add_argument('--disable-dev-shm-usage')
self.browser = make_driver(proxy_conf=self.proxy_conf, SHOW_UI=self.show_ui)
else:
from pyvirtualdisplay import Display
if YaSearchScraper._shared_display is None:
YaSearchScraper._shared_display = Display(visible=0, size=(800, 600))
YaSearchScraper._shared_display.start()
self.vdisplay = YaSearchScraper._shared_display
self.browser = make_driver(proxy_conf=self.proxy_conf, SHOW_UI=self.show_ui)
Поднимите лимиты дескрипторов в ОС
Если нагрузка действительно тяжёлая, разумно увеличить лимиты. Отредактируйте конфигурацию limits и поднимите nofile. Понадобятся повышенные привилегии.
nano /etc/security/limits.conf
Добавьте строки ниже и сохраните через Ctrl + x, затем y и Enter.
* soft nofile 4096
* hard nofile 8192
Добавьте явный GC и «облегчающие» флаги Chrome
Принудительная сборка мусора после каждой итерации помогает быстрее освобождать объекты на стороне Python, что хорошо сочетается с детерминированным закрытием драйвера. Дополнительные флаги Chrome чуть сокращают потребление ресурсов.
import gc
# в блоке finally цикла или обслуживающем разделе
gc.collect()
# при сборке опций Chrome
chr_opts.add_argument('--disable-extensions')
chr_opts.add_argument('--disable-software-rasterizer')
chr_opts.add_argument('--disable-logging')
Давайте циклу передышку
Небольшая пауза каждые несколько итераций даёт системе время на фоновую уборку.
if turn % 10 == 0:
time.sleep(30)
Сделайте основной цикл устойчивым
Собрав всё вместе, цикл может регулировать темп, вызывать сборку мусора и корректно остановить общий дисплей при завершении программы.
if __name__ == "__main__":
proxy_book = [/* ваши прокси */]
titles = ["Terminator smotret", "Sasha Tanya smotret", "John Wick smotret onlayn"]
turn = 0
try:
while True:
try:
with YaSearchScraper(SHOW_UI=False, proxy_conf=proxy_book[turn % 2]) as runner:
item = titles[turn]
turn = (turn + 1) % len(titles)
runner.harvest(item)
if turn % 10 == 0:
time.sleep(30)
import gc
gc.collect()
else:
time.sleep(random.uniform(8, 15))
except Exception:
log.error(f"Exception {traceback.format_exc()}")
time.sleep(30)
finally:
if YaSearchScraper._shared_display:
YaSearchScraper._shared_display.stop()
Используйте пул WebDriver вместо постоянного пересоздания
Ещё один эффективный шаг — не поднимать драйвер для каждого запроса. Небольшой пул, переиспользуемый между задачами, резко снижает churn дескрипторов и накладные на запуск. Ниже — реализация пула Chrome‑драйверов с поддержкой прокси и stealth‑настроек: экземпляры возвращаются в пул после использования и автоматически заменяются при поломке.
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options
import threading
import queue
import time
import random
import logging
from fake_useragent import UserAgent
import zipfile
import os
from selenium_stealth import stealth
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("driver_pool.log"),
logging.StreamHandler()
]
)
pool_log = logging.getLogger('DriverPoolLogger')
class DriverStash:
def __init__(self, pool_size=3, proxy_conf=None, headless=True):
self.pool_size = pool_size
self.proxy_conf = proxy_conf
self.headless = headless
self._q = queue.Queue(maxsize=pool_size)
self._lock = threading.Lock()
self._seed()
def _spawn(self):
opts = Options()
if self.headless:
opts.add_argument('--headless')
opts.add_argument('--no-sandbox')
opts.add_argument('--disable-dev-shm-usage')
opts.add_argument('--start-maximized')
opts.add_argument('--disable-blink-features=AutomationControlled')
ua = UserAgent(browsers='chrome', os='windows', platforms='pc')
opts.add_argument(f"user-agent={ua.random}")
if self.proxy_conf:
addon = self._proxy_addon()
opts.add_extension(addon)
d = Chrome(options=opts)
stealth(d,
languages=["ru-RU", "ru", "en-US", "en"],
vendor="Google Inc.",
platform="Linux x86_64",
webgl_vendor="Intel Inc.",
renderer="Intel Open Source Technology Center Mesa DRI Intel(R) UHD Graphics 620 (KBL GT2)",
fix_hairline=True,
)
d.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
'''
})
return d
def _proxy_addon(self):
manifest_json = """
{
"version": "1.0.0",
"manifest_version": 2,
"name": "Chrome Proxy",
"permissions": [
"proxy",
"tabs",
"unlimitedStorage",
"storage",
"<all_urls>",
"webRequest",
"webRequestBlocking"
],
"background": {
"scripts": ["background.js"]
},
"minimum_chrome_version":"22.0.0"
}
"""
background_js = """
var config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "http",
host: "%s",
port: parseInt(%s)
},
bypassList: ["localhost"]
}
};
chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});
function callbackFn(details) {
return {
authCredentials: {
username: "%s",
password: "%s"
}
};
}
chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{urls: ["<all_urls>"]},
['blocking']
);
""" % (self.proxy_conf["proxy"], self.proxy_conf["proxy_port"],
self.proxy_conf["proxy_login"], self.proxy_conf["proxy_password"])
addon_zip = 'proxy_auth_plugin.zip'
with zipfile.ZipFile(addon_zip, 'w') as zp:
zp.writestr("manifest.json", manifest_json)
zp.writestr("background.js", background_js)
return addon_zip
def _seed(self):
for _ in range(self.pool_size):
d = self._spawn()
self._q.put(d)
def acquire(self, timeout=30):
try:
d = self._q.get(timeout=timeout)
d.delete_all_cookies()
return d
except queue.Empty:
raise TimeoutError("No available drivers in the pool")
def release(self, d):
try:
d.delete_all_cookies()
d.get("about:blank")
self._q.put(d)
except Exception as e:
pool_log.error(f"Error returning driver to pool: {e}")
try:
d.quit()
except Exception:
pass
fresh = self._spawn()
self._q.put(fresh)
def cleanup(self):
while not self._q.empty():
try:
d = self._q.get_nowait()
d.quit()
except Exception as e:
pool_log.error(f"Error cleaning up driver: {e}")
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()
Ниже — обновлённый каркас программы с двумя пулами, работающими через разные прокси. Он чередует пулы и периодически делает «обслуживающие» паузы, чтобы удерживать ресурсы в стабильных рамках.
if __name__ == "__main__":
proxies = [
{"proxy": "proxy1.example.com", "proxy_port": "8080", "proxy_login": "user1", "proxy_password": "pass1"},
{"proxy": "proxy2.example.com", "proxy_port": "8080", "proxy_login": "user2", "proxy_password": "pass2"}
]
titles = ["Terminator smotret", "Sasha Tanya smotret", "John Wick smotret onlayn"]
farms = [
DriverStash(pool_size=2, proxy_conf=proxies[0], headless=True),
DriverStash(pool_size=2, proxy_conf=proxies[1], headless=True)
]
k = 0
rounds = 0
try:
while True:
try:
farm_idx = k % len(farms)
title = titles[k % len(titles)]
# Пример использования, если YaSearchScraper интегрирован с пулом драйверов
# with YaSearchScraper(driver_pool=farms[farm_idx]) as runner:
# runner.harvest(title)
k += 1
rounds += 1
if rounds % 20 == 0:
pool_log.info("Performing periodic maintenance")
time.sleep(30)
time.sleep(random.uniform(8, 15))
except Exception:
pool_log.error(f"Exception {traceback.format_exc()}")
time.sleep(30)
finally:
for farm in farms:
farm.cleanup()
Почему это важно
Долгоживущие системы парсинга или автотесты редко рушатся от одного катастрофического события. Чаще они медленно деградируют из‑за мелких утечек, которые ОС в какой‑то момент перестаёт терпеть. Понимая, как накапливаются дескрипторы, где их потребляют Selenium и Xvfb, и как ulimit ограничивает процесс, вы получаете рычаги для построения устойчивых сервисов. Особенно это критично для headless‑нагрузок в Linux, где драйверы, прокси и stealth‑настройки крутятся сутками.
Практические заключительные заметки
Пара рабочих привычек сильно помогает. Мониторьте число открытых файлов и мягкие/жёсткие лимиты штатными утилитами, смотрите на вывод lsof в долгих прогонах и проверяйте, не растёт ли количество дескрипторов монотонно. Если даже с одним дисплеем наблюдаете рост, пересмотрите порядок остановки и убедитесь, что временные файлы действительно удаляются. Переиспользуйте всё, что можно — от дисплеев до драйверов — и периодически замедляйте цикл, чтобы сборщики успевали отрабатывать.
Решения тут просты: агрессивно закрывайте ресурсы, переиспользуйте Display, разумно поднимайте потолок nofile, собирайте мусор, «облегчайте» Chrome и, где уместно, перестаньте создавать драйверы в горячем цикле, заменив это пулом. С этими мерами «Too many open files» перестаёт быть роковой стеной и превращается в рутинную операционную задачу.