Сделайте скриншот полной страницы с помощью Selenium Python с хромедрайвером

После опробования различных подходов... Я наткнулся на эту страницу, чтобы сделать полный снимок экрана с chromedriver, selenium и python.

Оригинальный код здесь. (и я копирую код в этой публикации ниже)

Он использует PIL и прекрасно работает! Однако есть одна проблема... она фиксирует фиксированные заголовки и повторяется для всей страницы, а также пропускает некоторые части страницы во время смены страницы. пример URL, чтобы сделать снимок экрана:

http://www.w3schools.com/js/default.asp

Как избежать повторяющихся заголовков с помощью этого кода... Или есть ли лучший вариант, который использует только Python... (яне знаю Java и не хочу использовать Java).

Смотрите скриншот текущего результата и пример кода ниже.

full page screenshot with repeated headers

test.py

"""
This script uses a simplified version of the one here:
https://snipt.net/restrada/python-selenium-workaround-for-full-page-screenshot-using-chromedriver-2x/

It contains the *crucial* correction added in the comments by Jason Coutu.
"""

import sys

from selenium import webdriver
import unittest

import util

class Test(unittest.TestCase):
    """ Demonstration: Get Chrome to generate fullscreen screenshot """

    def setUp(self):
        self.driver = webdriver.Chrome()

    def tearDown(self):
        self.driver.quit()

    def test_fullpage_screenshot(self):
        ''' Generate document-height screenshot '''
        #url = "http://effbot.org/imagingbook/introduction.htm"
        url = "http://www.w3schools.com/js/default.asp"
        self.driver.get(url)
        util.fullpage_screenshot(self.driver, "test.png")


if __name__ == "__main__":
    unittest.main(argv=[sys.argv[0]])

util.py

import os
import time

from PIL import Image

def fullpage_screenshot(driver, file):

        print("Starting chrome full page screenshot workaround ...")

        total_width = driver.execute_script("return document.body.offsetWidth")
        total_height = driver.execute_script("return document.body.parentNode.scrollHeight")
        viewport_width = driver.execute_script("return document.body.clientWidth")
        viewport_height = driver.execute_script("return window.innerHeight")
        print("Total: ({0}, {1}), Viewport: ({2},{3})".format(total_width, total_height,viewport_width,viewport_height))
        rectangles = []

        i = 0
        while i < total_height:
            ii = 0
            top_height = i + viewport_height

            if top_height > total_height:
                top_height = total_height

            while ii < total_width:
                top_width = ii + viewport_width

                if top_width > total_width:
                    top_width = total_width

                print("Appending rectangle ({0},{1},{2},{3})".format(ii, i, top_width, top_height))
                rectangles.append((ii, i, top_width,top_height))

                ii = ii + viewport_width

            i = i + viewport_height

        stitched_image = Image.new('RGB', (total_width, total_height))
        previous = None
        part = 0

        for rectangle in rectangles:
            if not previous is None:
                driver.execute_script("window.scrollTo({0}, {1})".format(rectangle[0], rectangle[1]))
                print("Scrolled To ({0},{1})".format(rectangle[0], rectangle[1]))
                time.sleep(0.2)

            file_name = "part_{0}.png".format(part)
            print("Capturing {0} ...".format(file_name))

            driver.get_screenshot_as_file(file_name)
            screenshot = Image.open(file_name)

            if rectangle[1] + viewport_height > total_height:
                offset = (rectangle[0], total_height - viewport_height)
            else:
                offset = (rectangle[0], rectangle[1])

            print("Adding to stitched image with offset ({0}, {1})".format(offset[0],offset[1]))
            stitched_image.paste(screenshot, offset)

            del screenshot
            os.remove(file_name)
            part = part + 1
            previous = rectangle

        stitched_image.save(file)
        print("Finishing chrome full page screenshot workaround...")
        return True

Ответ 1

Вы можете добиться этого, изменив CSS заголовка перед скриншотом:

topnav = driver.find_element_by_id("topnav")
driver.execute_script("arguments[0].setAttribute('style', 'position: absolute; top: 0px;')", topnav) 

EDIT. Поместите эту строку после прокрутки окна:

driver.execute_script("document.getElementById('topnav').setAttribute('style', 'position: absolute; top: 0px;');")

Итак, в util.py это будет:

driver.execute_script("window.scrollTo({0}, {1})".format(rectangle[0], rectangle[1]))
driver.execute_script("document.getElementById('topnav').setAttribute('style', 'position: absolute; top: 0px;');")

Если сайт использует тег header, вы можете сделать это с помощью find_element_by_tag_name("header")

Ответ 2

element = driver.find_element_by_tag_name('body')
element_png = element.screenshot_as_png
with open("test2.png", "wb") as file:
    file.write(element_png)

Это работает для меня. Сохраняет всю страницу как скриншот. Для получения дополнительной информации вы можете прочитать документацию API:http://selenium-python.readthedocs.io/api.html

Ответ 3

Снимки экрана ограничены окном просмотра, но вы можете обойти это, захватив элемент body, так как веб-драйвер захватит весь элемент, даже если он больше, чем окно просмотра. Это избавит вас от необходимости прокрутки и сшивания изображений, однако вы можете увидеть проблемы с положением нижнего колонтитула (как на скриншоте ниже).

Протестировано на Windows 8 и Mac High Sierra с драйвером Chrome.

from selenium import webdriver

url = 'https://stackoverflow.com/'
path = '/path/to/save/in/scrape.png'

driver = webdriver.Chrome()
driver.get(url)
el = driver.find_element_by_tag_name('body')
el.screenshot(path)
driver.quit()

Возвращает: (полный размер: https://i.stack.imgur.com/ppDiI.png)

SO_scrape

Ответ 4

Этот ответ улучшен по сравнению с предыдущими ответами am05mhz и Джаведа Карима.

Предполагается, что безголовый режим и что опция размера окна изначально не была установлена. Перед вызовом этой функции убедитесь, что страница загружена полностью или достаточно.

Он пытается установить ширину и высоту как то, что необходимо. Скриншот всей страницы может иногда включать ненужную вертикальную полосу прокрутки. Один из способов вообще избежать полосы прокрутки - сделать скриншот элемента body. После сохранения снимка экрана он возвращает размер к первоначальному размеру, в противном случае размер следующего снимка экрана может быть задан неправильно.

В конечном счете, этот метод все еще может работать не совсем хорошо для некоторых примеров.

def save_screenshot(driver: webdriver.Chrome, path: str = '/tmp/screenshot.png') -> None:
    # Ref: https://stackoverflow.com/a/52572919/
    original_size = driver.get_window_size()
    required_width = driver.execute_script('return document.body.parentNode.scrollWidth')
    required_height = driver.execute_script('return document.body.parentNode.scrollHeight')
    driver.set_window_size(required_width, required_height)
    # driver.save_screenshot(path)  # has scrollbar
    driver.find_element_by_tag_name('body').screenshot(path)  # avoids scrollbar
    driver.set_window_size(original_size['width'], original_size['height'])

Если вы используете Python старше 3.6, удалите аннотации типов из определения функции.

Ответ 5

Узнав подход @Moshisho.

Моя полная автономная работа script - это... (добавлен сон 0,2 после каждой прокрутки и позиции)

import sys
from selenium import webdriver
import util
import os
import time
from PIL import Image

def fullpage_screenshot(driver, file):

        print("Starting chrome full page screenshot workaround ...")

        total_width = driver.execute_script("return document.body.offsetWidth")
        total_height = driver.execute_script("return document.body.parentNode.scrollHeight")
        viewport_width = driver.execute_script("return document.body.clientWidth")
        viewport_height = driver.execute_script("return window.innerHeight")
        print("Total: ({0}, {1}), Viewport: ({2},{3})".format(total_width, total_height,viewport_width,viewport_height))
        rectangles = []

        i = 0
        while i < total_height:
            ii = 0
            top_height = i + viewport_height

            if top_height > total_height:
                top_height = total_height

            while ii < total_width:
                top_width = ii + viewport_width

                if top_width > total_width:
                    top_width = total_width

                print("Appending rectangle ({0},{1},{2},{3})".format(ii, i, top_width, top_height))
                rectangles.append((ii, i, top_width,top_height))

                ii = ii + viewport_width

            i = i + viewport_height

        stitched_image = Image.new('RGB', (total_width, total_height))
        previous = None
        part = 0

        for rectangle in rectangles:
            if not previous is None:
                driver.execute_script("window.scrollTo({0}, {1})".format(rectangle[0], rectangle[1]))
                time.sleep(0.2)
                driver.execute_script("document.getElementById('topnav').setAttribute('style', 'position: absolute; top: 0px;');")
                time.sleep(0.2)
                print("Scrolled To ({0},{1})".format(rectangle[0], rectangle[1]))
                time.sleep(0.2)

            file_name = "part_{0}.png".format(part)
            print("Capturing {0} ...".format(file_name))

            driver.get_screenshot_as_file(file_name)
            screenshot = Image.open(file_name)

            if rectangle[1] + viewport_height > total_height:
                offset = (rectangle[0], total_height - viewport_height)
            else:
                offset = (rectangle[0], rectangle[1])

            print("Adding to stitched image with offset ({0}, {1})".format(offset[0],offset[1]))
            stitched_image.paste(screenshot, offset)

            del screenshot
            os.remove(file_name)
            part = part + 1
            previous = rectangle

        stitched_image.save(file)
        print("Finishing chrome full page screenshot workaround...")
        return True


driver = webdriver.Chrome()

''' Generate document-height screenshot '''
url = "http://effbot.org/imagingbook/introduction.htm"
url = "http://www.w3schools.com/js/default.asp"
driver.get(url)
fullpage_screenshot(driver, "test1236.png")

Ответ 6

Не уверен, что люди все еще имеют эту проблему. Я сделал небольшой хак, который работает довольно хорошо и хорошо сочетается с динамическими зонами. Надеюсь, поможет

# 1. get dimensions
browser = webdriver.Chrome(chrome_options=options)
browser.set_window_size(default_width, default_height)
browser.get(url)
time.sleep(sometime)
total_height = browser.execute_script("return document.body.parentNode.scrollHeight")
browser.quit()

# 2. get screenshot
browser = webdriver.Chrome(chrome_options=options)
browser.set_window_size(default_width, total_height)
browser.get(url)  
browser.save_screenshot(screenshot_path)

Ответ 7

Я изменил код для Python 3.6, может быть, это будет полезно для кого-то:

from selenium import webdriver
from sys import stdout
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import unittest
#from Login_Page import Login_Page
from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
from io import BytesIO
from PIL import Image

def testdenovoUIavailable(self):
        binary = FirefoxBinary("C:\\Mozilla Firefox\\firefox.exe") 
        self.driver  = webdriver.Firefox(firefox_binary=binary)
        verbose = 0

        #open page
        self.driver.get("http://yandex.ru")

        #hide fixed header        
        #js_hide_header=' var x = document.getElementsByClassName("topnavbar-wrapper ng-scope")[0];x[\'style\'] = \'display:none\';'
        #self.driver.execute_script(js_hide_header)

        #get total height of page
        js = 'return Math.max( document.body.scrollHeight, document.body.offsetHeight,  document.documentElement.clientHeight,  document.documentElement.scrollHeight,  document.documentElement.offsetHeight);'

        scrollheight = self.driver.execute_script(js)
        if verbose > 0:
            print(scrollheight)

        slices = []
        offset = 0
        offset_arr=[]

        #separate full screen in parts and make printscreens
        while offset < scrollheight:
            if verbose > 0: 
                print(offset)

            #scroll to size of page 
            if (scrollheight-offset)<offset:
                #if part of screen is the last one, we need to scroll just on rest of page
                self.driver.execute_script("window.scrollTo(0, %s);" % (scrollheight-offset))
                offset_arr.append(scrollheight-offset)
            else:
                self.driver.execute_script("window.scrollTo(0, %s);" % offset)
                offset_arr.append(offset)

            #create image (in Python 3.6 use BytesIO)
            img = Image.open(BytesIO(self.driver.get_screenshot_as_png()))


            offset += img.size[1]
            #append new printscreen to array
            slices.append(img)


            if verbose > 0:
                self.driver.get_screenshot_as_file('screen_%s.jpg' % (offset))
                print(scrollheight)

        #create image with 
        screenshot = Image.new('RGB', (slices[0].size[0], scrollheight))
        offset = 0
        offset2= 0
        #now glue all images together
        for img in slices:
            screenshot.paste(img, (0, offset_arr[offset2])) 
            offset += img.size[1]
            offset2+= 1      

        screenshot.save('test.png')

Ответ 8

Почему бы просто не получить ширину и высоту страницы, а затем изменить размер драйвера? Так будет как то так

total_width = driver.execute_script("return document.body.offsetWidth")
total_height = driver.execute_script("return document.body.scrollHeight")
driver.set_window_size(total_width, total_height)
driver.save_screenshot("SomeName.png")

Это сделает скриншот всей вашей страницы без необходимости объединять разные части.

Ответ 9

Ключ должен включить режим headless! Нет необходимости прошивать и не нужно загружать страницу дважды.

Полный рабочий код:

URL = 'http://www.w3schools.com/js/default.asp'

options = webdriver.ChromeOptions()
options.headless = True

driver = webdriver.Chrome(options=options)
driver.get(URL)

S = lambda X: driver.execute_script('return document.body.parentNode.scroll'+X)
driver.set_window_size(S('Width'),S('Height')) # May need manual adjustment
driver.find_element_by_tag_name('body').screenshot('web_screenshot.png')

driver.quit()

Это практически тот же код, который опубликовал @Acumenus с небольшими улучшениями.

Краткое изложение моих выводов

Я решил опубликовать это в любом случае, потому что я не нашел объяснения тому, что происходит, когда режим headless выключен (отображается браузер) для создания снимков экрана. Как я тестировал (с Chrome WebDriver), если включен режим headless, снимок экрана сохраняется по желанию. Однако, если режим headless отключен, сохраненный снимок экрана имеет приблизительно правильную ширину и высоту, но результат варьируется в зависимости от конкретного случая. Обычно верхняя часть страницы, которая видна на экране, сохраняется, но остальная часть изображения просто белая. Был также случай с попыткой сохранить эту ветку Qaru, используя вышеуказанную ссылку; даже верхняя часть не была сохранена, что интересно теперь было прозрачным, в то время как остальное все еще белое. Последний случай, который я заметил, был только один раз с указанной ссылкой W3Schools; там, где нет белых частей, но верхняя часть страницы повторяется до конца, включая заголовок.

Я надеюсь, что это поможет многим из тех, кто по какой-то причине не получает ожидаемого результата, так как я не видел, чтобы кто-то явно объяснял требование режима headless при таком простом подходе. Только когда я сам нашел решение этой проблемы, я обнаружил сообщение by @vc2279, в котором говорится, что окно безголового браузера можно установить на любой размер (что, по-видимому, также верно для противоположного случая), Хотя решение в моем посте улучшает то, что оно не требует повторного открытия браузера/драйвера или перезагрузки страницы.

Дополнительные предложения

Если для некоторых страниц это не работает, я предлагаю попробовать добавить time.sleep(seconds), прежде чем получить размер страницы. Другой случай, если страница требует прокрутки до конца, чтобы загрузить дополнительный контент, что можно решить с помощью метода scheight из этого сообщения:

scheight = .1
while scheight < 9.9:
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight/%s);" % scheight)
    scheight += .01

Также обратите внимание, что для некоторых страниц контент может отсутствовать ни в одном из тегов HTML верхнего уровня, таких как <html> или <body>, например, YouTube использует тег <ytd-app>. В качестве последней заметки я нашел одну страницу, которая "возвращала" снимок экрана с горизонтальной полосой прокрутки, размер окна требовал ручной настройки, т.е. ширину изображения нужно было увеличить на 18 пикселей, например: S('Width')+18.

Ответ 10

element=driver.find_element_by_tag_name('body')
element_png = element.screenshot_as_png
with open("test2.png", "wb") as file:
    file.write(element_png)

Произошла ошибка в коде, предложенном ранее в строке 2. Вот исправленная. Будучи noob здесь, я еще не могу редактировать свой собственный пост.

Иногда baove не дает лучших результатов. Таким образом, можно использовать другой метод для получения высоты всех элементов и суммировать их для установки высоты захвата, как показано ниже:

element=driver.find_elements_by_xpath("/html/child::*/child::*")
    eheight=set()
    for e in element:
        eheight.add(round(e.size["height"]))
    print (eheight)
    total_height = sum(eheight)
    driver.execute_script("document.getElementsByTagName('html')[0].setAttribute('style', 'height:"+str(total_height)+"px')")
    element=driver.find_element_by_tag_name('body')
    element_png = element.screenshot_as_png
    with open(fname, "wb") as file:
        file.write(element_png)

Кстати, он работает на FF.

Ответ 11

Немного измените код @ihightower и @A.Minachev и сделайте так, чтобы он работал в Mac Retina:

import time
from PIL import Image
from io import BytesIO

def fullpage_screenshot(driver, file, scroll_delay=0.3):
    device_pixel_ratio = driver.execute_script('return window.devicePixelRatio')

    total_height = driver.execute_script('return document.body.parentNode.scrollHeight')
    viewport_height = driver.execute_script('return window.innerHeight')
    total_width = driver.execute_script('return document.body.offsetWidth')
    viewport_width = driver.execute_script("return document.body.clientWidth")

    # this implementation assume (viewport_width == total_width)
    assert(viewport_width == total_width)

    # scroll the page, take screenshots and save screenshots to slices
    offset = 0  # height
    slices = {}
    while offset < total_height:
        if offset + viewport_height > total_height:
            offset = total_height - viewport_height

        driver.execute_script('window.scrollTo({0}, {1})'.format(0, offset))
        time.sleep(scroll_delay)

        img = Image.open(BytesIO(driver.get_screenshot_as_png()))
        slices[offset] = img

        offset = offset + viewport_height

    # combine image slices
    stitched_image = Image.new('RGB', (total_width * device_pixel_ratio, total_height * device_pixel_ratio))
    for offset, image in slices.items():
        stitched_image.paste(image, (0, offset * device_pixel_ratio))
    stitched_image.save(file)

fullpage_screenshot(driver, 'test.png')

Ответ 12

Я изменил ответ jeremie-s, чтобы он получал URL только один раз.

browser = webdriver.Chrome(chrome_options=options)
browser.set_window_size(default_width, default_height)
browser.get(url)
height = browser.execute_script("return document.body.parentNode.scrollHeight")

# 2. get screenshot
browser.set_window_size(default_width, height)
browser.save_screenshot(screenshot_path)

browser.quit()

Ответ 13

Вы можете использовать Splinter
Splinter - это слой абстракции поверх существующих инструментов автоматизации браузера, таких как Selenium
В новой версии 0.10.0 появилась новая функция browser.screenshot(..., full=True).
full=True опция сделает полный снимок экрана для вас.

Ответ 14

Понял!!! работает как шарм

Для NodeJS, но концепция та же:

await driver.executeScript('
      document.documentElement.style.display = "table";
      document.documentElement.style.width = "100%";
      document.body.style.display = "table-row";
');

await driver.findElement(By.css('body')).takeScreenshot();

Ответ 15

легко питоном, но медленно

import os

from selenium import webdriver
from PIL import Image


def full_screenshot(driver: webdriver):
    driver.execute_script(f"window.scrollTo({0}, {0})")
    total_width = driver.execute_script("return document.body.offsetWidth")
    total_height = driver.execute_script("return document.body.parentNode.scrollHeight")
    viewport_width = driver.execute_script("return document.body.clientWidth")
    viewport_height = driver.execute_script("return window.innerHeight")
    rectangles = []
    i = 0
    while i < total_height:
        ii = 0
        top_height = i + viewport_height
        if top_height > total_height:
            top_height = total_height
        while ii < total_width:
            top_width = ii + viewport_width
            if top_width > total_width:
                top_width = total_width
            rectangles.append((ii, i, top_width, top_height))
            ii = ii + viewport_width
        i = i + viewport_height
    stitched_image = Image.new('RGB', (total_width, total_height))
    previous = None
    part = 0

    for rectangle in rectangles:
        if not previous is None:
            driver.execute_script("window.scrollTo({0}, {1})".format(rectangle[0], rectangle[1]))
        file_name = "part_{0}.png".format(part)
        driver.get_screenshot_as_file(file_name)
        screenshot = Image.open(file_name)

        if rectangle[1] + viewport_height > total_height:
            offset = (rectangle[0], total_height - viewport_height)
        else:
            offset = (rectangle[0], rectangle[1])
        stitched_image.paste(screenshot, offset)
        del screenshot
        os.remove(file_name)
        part = part + 1
        previous = rectangle
    return stitched_image

Ответ 16

Я изменил ответ, данный @ihightower, вместо того, чтобы сохранять скриншот в этой функции, возвращать общую высоту и общую ширину веб-страницы, а затем установить размер окна на общую высоту и общую ширину.

from PIL import Image
from io import BytesIO

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def open_url(url):
    options = Options()

    options.headless = True

    driver = webdriver.Chrome(chrome_options=options)

    driver.maximize_window()
    driver.get(url)
    save_screenshot(driver, 'screen.png')

def save_screenshot(driver, file_name):
    height, width = scroll_down(driver)
    driver.set_window_size(width, height)
    img_binary = driver.get_screenshot_as_png()
    img = Image.open(BytesIO(img_binary))
    img.save(file_name)
    # print(file_name)
    print(" screenshot saved ")


def scroll_down(driver):
    total_width = driver.execute_script("return document.body.offsetWidth")
    total_height = driver.execute_script("return document.body.parentNode.scrollHeight")
    viewport_width = driver.execute_script("return document.body.clientWidth")
    viewport_height = driver.execute_script("return window.innerHeight")

    rectangles = []

    i = 0
    while i < total_height:
        ii = 0
        top_height = i + viewport_height

        if top_height > total_height:
            top_height = total_height

        while ii < total_width:
            top_width = ii + viewport_width

            if top_width > total_width:
                top_width = total_width

            rectangles.append((ii, i, top_width, top_height))

            ii = ii + viewport_width

        i = i + viewport_height

    previous = None
    part = 0

    for rectangle in rectangles:
        if not previous is None:
            driver.execute_script("window.scrollTo({0}, {1})".format(rectangle[0], rectangle[1]))
            time.sleep(0.5)
        # time.sleep(0.2)

        if rectangle[1] + viewport_height > total_height:
            offset = (rectangle[0], total_height - viewport_height)
        else:
            offset = (rectangle[0], rectangle[1])

        previous = rectangle

    return (total_height, total_width)

open_url("https://www.medium.com")

Ответ 17

Как это работает: установите максимальную высоту браузера...

#coding=utf-8
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def test_fullpage_screenshot(self):
    chrome_options = Options()
    chrome_options.add_argument('--headless')
    chrome_options.add_argument('--start-maximized')
    driver = webdriver.Chrome(chrome_options=chrome_options)
    driver.get("yoururlxxx")
    time.sleep(2)

    #the element with longest height on page
    ele=driver.find_element("xpath", '//div[@class="react-grid-layout layout"]')
    total_height = ele.size["height"]+1000

    driver.set_window_size(1920, total_height)      #the trick
    time.sleep(2)
    driver.save_screenshot("screenshot1.png")
    driver.quit()

if __name__ == "__main__":
    test_fullpage_screenshot()