使用Chrome来做一张长截图

Chrome

遇见一篇有意思的文章,或者是在购物时发现了好东西,需要分享。

不想发送链接,也许搞一个长截图是个很好的选择。

Selenium

Selenium是一种自动化测试工具,它支持各种浏览器,包括 Chrome,Safari,Firefox 等主流界面式浏览器,如果你在这些浏览器里面安装一个 Selenium 的插件,那么便可以方便地实现Web界面的测试。换句话说叫 Selenium 支持这些浏览器驱动。Selenium支持多种语言开发,比如 Java,C,Ruby, Python等等。

在这个案例中,我们使用基于Python3的运行环境进行演示。

使用pip3安装selenium


$ pip3 install selenium

获取chrome-driver

在淘宝npm镜像中可以找到Chrome版本对应的驱动: http://npm.taobao.org/mirrors/chromedriver/

初始化浏览器


    def init_browser():
        chrome_options = Options()
        # --headless参数表示,Chrome将不会有一个可视化的图形界面
        # chrome_options.add_argument("--headless")
        chrome_options.add_argument("--disable-gpu")
        chrome_options.add_argument("--disable-web-security")
        # 以iPhone 6的屏幕宽度作为基准
        mobile_emulation = { "deviceName": "iPhone 6" }
        chrome_options.add_experimental_option("mobileEmulation", mobile_emulation)
        return webdriver.Chrome("./chromedriver",
                                chrome_options=chrome_options)

    browser = inti_browser()

调用Webdriver提供的API, 获取网页基本信息


    url = "https://code.evink.me"

    # browser是我们刚刚初始化的浏览器实例
    # 设置页面渲染超时
    browser.set_page_load_timeout(30)
    # 设置脚本执行超时
    browser.set_script_timeout(100)
    # 获取网页资源
    browser.get(url)
    # 给浏览器设置一个默认的初始宽高(非必要)
    browser.set_window_size(375, 1000)

Chrome在iPhone 6的模拟环境下进行网页渲染,其宽是一定的,所以可以通过调用运行于webdriver内部的javascript代码,获得我们想要的内容。

webdrive提供了丰富的接口供我们全方位操控这个小小的浏览器。


    # browser是我们刚刚初始化的浏览器实例
    body_height = browser.body.get_attribute("clientHeight")

或者,这里还有一个更加可操作的方法,直接在webdriver里跑js代码。Selenium提供了execute_script(str)接口,可以让用户通过自己更为熟悉的方式,得到自己想要的内容。


    # browser是我们刚刚初始化的浏览器实例
    body_height = browser.execute_script(get_script("body_height")))

    # get_script(str) -> str
    def get_script(_type):
        if _type == "body_height":
            return """
                    // get body_height
                    return document.getElementsByTagName("body")[0].clientHeight;
            """

滚屏截图

webdriver提供了一个保存当前浏览器窗口截图的接口save_screenshot(path)

我们只需要让网页沿着一个预设定的高度滚动就好。

这里,这个高度是667(基于我们以iPhone 6的虚拟环境初始化的浏览器)。设定大于667的高度,每次也并不会截取到更多的页面内容,而小于667的高度,会让你最后进行图片处理的时候非常头疼。


    dir_path = "tmp_screenshots"
    filename = "evink" + int(datetime.now().timestamp())
    paging_list = []

    def take_screenshot():
        # loop_times由网页高度和单屏高度计算而来
        for i in range(loop_times):
            browser.execute_script(get_script("scroll_window") % (i + 1))
            # 截取屏幕
            path = "%s/%s_%s.png"%(dir_path, filename, i)
            browser.save_screenshot(path)
            paging_list.append(path)
        return paging_list


    def get_script(_type):

        if _type == "scroll_window":
            return """
                    // 滚动的次数
                    var m = %s;
                    // 屏幕range
                    var begin = 0;
                    var end = 667;
                    // 滚屏
                    window.scrollTo(begin, end * m);
                """
        if _type == "scroll_y":
            return """
                return window.scrollY;
            """

合成图片

上一步结束之后,我们在/tmp_screenshots目录下会发现若干png格式的图片,利用Python中的PIL Image库,可以很方便的对图片做处理。


   def paste_imgs(self):
       # loop_times由网页高度和单屏高度计算而来
       for i in range(loop_times):
           path = "%s/%s_%s.png"%(dir_path, filename, i)
           if os.path.exists(path):
               continue
           else:
               # 记录存在的最大图片张数
               max_screen = i
       # 根据总图片张数计算合成的单张图片高度
       page_total_height = 667 * 2 * loop_times
       # 声明一张空白的图片实例
       image = Image.new(
           'RGB', (375 * 2, page_total_height), (255, 255, 255))

       for i in range(loop_times):
           path = "%s/%s_%s.png"%(dir_path, filename, i)
           from_img = Image.open(path)
           # 粘贴图片
           image.paste(from_img, (0, 1334 * ( i + 1 )))

       path = "%s/%s_part_%s.jpg"%(dir_path, filename, part+1)
       # 保存图片 以JPEG格式,60质量
       image.save(path,format='JPEG', quality=60)
       return "%s/%s.jpg"%(dir_path, filename)

处理细节

上一步结束之后,我们获得了一张完整的图片,但是,你一定会发现很多小细节没有处理。

图片未被加载

如果你要截取的网页采用了图片懒加载模式(可以提升访问速度),你会发现所截取的网页的图片都被灰色的色块所替代。

我们可以分析网页中,未被加载的图片是否有什么共同点。比如说,是否含有特定的class,是否有自定义的属性值。


    # 假设图片未加载时,此网页图片的class会有 "img_loading"

    # 获取所有的图片元素
    imgs = browser.find_elements_by_tag_name("img")
    img_load_start = datetime.now().timestamp()
    while True:
        # 已加载的图片数
        img_loadeds = 0
        for img in imgs:
            # 拿到class属性
            clz = img.get_attribute("class")
            if clz.find("img_loading") == -1:
                img_loadeds += 1
        print("已经加载完毕的图像数:%s"%img_loadeds)
        if img_loadeds == len(wait_load_imgs):
            print("all imgs loaded")
            break
        # 给他设置一个超时
        if datetime.now().timestamp() - img_load_start > 60:
            print("imgs loading timeout")
            break
        sleep(0.1)

图片底部 显示不完全 / 留有大量的空白 / 拼接不完美

造成这个原因,无非是高度和留余问题。

网页高度

某些网页上,在滚屏完毕(即所有图片都被加载后)的高度和网页初始化后的高度并不一致。

高度的错误会导致生成图片时抛出异常


    # 妈妈让我再滚一次
    def scroll_window(need_renew_height=False):

        for pre_scroll in range(loop_times + 1):
            self.browser.execute_script(self.get_script("scroll_window")%pre_scroll)
            sleep(0.5)

        print("-- 已经滚完 --")

        if need_renew_height:
            # 重新录入高度
            body = browser.find_element_by_id('activity-detail')
            body_height = int(body.get_attribute("clientHeight"))
            loop_times = math.ceil(body_height / height)

    def get_script(self, _type):

        if _type == "scroll_window":
            return """
                    // 滚动的次数
                    var m = %s;
                    // 取出所有图片
                    var imgs = document.querySelectorAll('img');
                    // 屏幕range
                    var begin = 0;
                    var end = 667;
                    // 滚屏
                    window.scrollTo(begin, end * m);
                    // 图片的相对距离
                    for(var i = 0;i < imgs.length;i++){
                        var y = imgs[i].getBoundingClientRect()["y"];
                        if(y >= begin && y <= end){
                            imgs[i].setAttribute("type", "wait_load");
                        }
                    }
            """

处理好收尾工作

在生成滚屏时,最后一张图片含有两种状态。

  • 完美占据一屏的空间 (375 * 667)
  • 只占据部分空间,底部含有留白

处理好最后一屏图片和倒数第二屏的关系,就可以避免出现图片拼接不完美的情况。

图片 黑屏 / 损坏

图片过长,尝试按照固定的屏幕数,将一张图切成几张小图。


原文地址: https://code.evink.me/2018/07/post/python-use-chrome-to-make-a-long-page-screenshot/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342