Flask集成Selenium实现网页截图

news2025/5/31 14:27:19

先看效果

程序实现的功能为:截取目标网址对应的页面,并将截取后的页面图片返回到用户端,用户可自由保存该截图。

支持的url参数如下:

url目标网址(必填项),字符串类型,如:https://www.baidu.com

w:截图宽度(选填项),默认为浏览器的可视窗口宽度,数值类型

h:截图高度(选填项),默认为浏览器的可视窗口高度(包含滚动区域),数值类型

p:截取的屏幕数量,默认为全部(选填项),数值类型

offset:最后一屏的偏差高度,默认为 0(选填项),数值类型

headless:是否打开浏览器,默认为 静默运行(选填项),两者的区别如下:

显式运行:截图更精确,但占用的系统资源相对多一点。

静默运行:在截图精度上有一点丢失(肉眼几乎看不出),但占用更少的系统资源。

最简请求示例:

以静默状态截取百度首页

http://127.0.0.1:8181/?url=https://www.baidu.com

完整请求示例:

以显式状态按指定的宽高截取百度首页

http://127.0.0.1:8181/?url=https://www.baidu.com&w=800&h=500&p=1&offset=0&headless=0

实现代码

import io
import os
import time
import logging
from uuid import uuid4
from PIL import Image
from screeninfo import get_monitors
from flask import Flask, request, send_file
from selenium import webdriver
from selenium.webdriver.chrome.service import Service

# 网页截图存储目录
image_folder = "static"

app = Flask(__name__)

# 禁止控制台输出请求信息
logging.getLogger('werkzeug').disabled = True
# 设置总的日志输出级别
app.logger.setLevel(logging.ERROR)

# 404错误处理
@app.errorhandler(404)
def page_not_found(e):
    # 直接返回字符串
    return "您请求的资源不存在!", 404

# 500错误处理
@app.errorhandler(500)
def internal_error(e):
    return "服务器内部错误!", 500

# 捕获所有未处理异常
@app.errorhandler(Exception)
def handle_exception(e):
    return "发生错误,请稍后再试!", 500

@app.route('/')
def process_request():

    url = request.args.get('url')

    if not url:
        return '无效的请求参数!'

    args = request.args

    
    # 截取的屏数,默认为全部
    pages = int(args.get('p', 0))
    
    # 最后一屏的偏差高度
    offset_height = int(args.get('offset', 0))
    
    # 默认使用无头模式,headless=0 表示使用有头模式
    headless = args.get('headless')
    
    # 捕获网页截图
    img_file = init(url, pages, offset_height, headless)

    if img_file is None:
        return '网页截取失败!'

    # 调整图片尺寸
    img = Image.open(img_file)
    (width, height) = get_image_size(img, args.get('w'), args.get('h'))
    # 高质量抗锯齿
    img = img.resize((width, height), Image.Resampling.LANCZOS)

    # 转换为字节流返回
    img_io = io.BytesIO()
    img.save(img_io, 'PNG')
    img_io.seek(0)

    return send_file(img_io, mimetype='image/png')

def init(url, pages=0, offset_height=0, headless=None):
    chrome_options = webdriver.ChromeOptions()
    # 截图时建议关闭无头模式
    if headless is None:
        chrome_options.add_argument('--headless=new')
    chrome_options.add_argument('log-level=3')
    chrome_options.add_argument('--disable-gpu')
    chrome_options.add_argument('--disable-infobars')
    chrome_options.add_argument('--ignore-certificate-errors')
    chrome_options.add_experimental_option('excludeSwitches', ['enable-logging', 'enable-automation'])

    service = Service()
    service.path = 'ChromeV123/chromedriver.exe'
    chrome_options.binary_location = 'ChromeV123/chrome-win64/chrome.exe'

    driver = webdriver.Chrome(options=chrome_options, service=service)
    if headless is None:
        # 无头模式需要设置窗口尺寸
        set_window_size(driver)
    else:
        driver.maximize_window()
    try:
        driver.get(url)
        time.sleep(2)
        file_name = capture_full_page(driver, pages, offset_height)
    finally:
        driver.quit()

    return file_name

# 设置浏览器窗口宽高
def set_window_size(driver):
    primary_monitor = get_monitors()[0]
    width = primary_monitor.width + 16
    height = primary_monitor.height - 31
    driver.set_window_size(width, height)

'''
截取整个网页,包含滚动部分,保存路径为当前项目的{image_folder}/screenshot目录下
pages 截取的屏数,默认为全部
offset_height 偏差纠正,处理最后一屏的特殊参数,需传入合适的数值(注意:无头模式和界面模式该参数值可能是不同的)
'''
def capture_full_page(driver, pages=0, offset_height=0):
    file_name = None
    try:
        date = time.strftime("%Y%m%d", time.localtime(time.time()))
        root_path = f'{image_folder}/screenshot/{date}'
        if not os.path.exists(root_path):
            os.makedirs(root_path)
        file_name = '{}/{}.png'.format(root_path, uuid4())
        # 获取网页可视区域宽高
        inner_width = driver.execute_script("return window.innerWidth")
        inner_height = driver.execute_script("return window.innerHeight")
        
        # 获取网页高度
        height = get_page_height(driver, pages)
        
        # 滚动截图,创建一个指定宽高的白色背景,用于在上面拼接图片
        screenshot = Image.new('RGB', (inner_width, height if pages == 0 else min(inner_height*pages, height)), (255, 255, 255))
        scroll_total = 0
        
        num = 0
        # 每次滚动一屏,一屏的高度为 inner_height
        for scroll_height in range(0, height, inner_height):
            if pages > 0 and num >= pages:
                break
            driver.execute_script("window.scrollTo(0, %d)" % scroll_height)
            time.sleep(0.5)
            screenshot_part = driver.get_screenshot_as_png()
            img = Image.open(io.BytesIO(screenshot_part))
            screenshot.paste(img, (0, scroll_height, img.width, scroll_height + img.height))
            scroll_total = scroll_height
            num += 1
        # 最后一屏需要特殊处理
        if scroll_total > 0 and scroll_total < height and height-scroll_total < inner_height:
            driver.execute_script(f"window.scrollTo(0, {height})")
            time.sleep(0.5)
            # 截取剩余区域
            img_file = capture_region(driver, 0, inner_height - (height - scroll_total) - offset_height, inner_width, inner_height)
            if img_file is not None:
                img = Image.open(img_file)
                screenshot.paste(img, (0, scroll_total, img.width, scroll_total + img.height))
                os.remove(img_file)
        # 保存截图
        screenshot.save(file_name)
    except:
        pass
    return file_name

'''
截取指定区域,保存路径为当前项目的screenshot/region目录下
left_top_x 左上角X轴坐标
left_top_y 左上角Y轴坐标
right_bottom_x 右下角X轴坐标
right_bottom_y 右下角Y轴坐标
'''
def capture_region(driver, left_top_x, left_top_y, right_bottom_x, right_bottom_y):
    try:
        date = time.strftime("%Y%m%d", time.localtime(time.time()))
        root_path = f'{image_folder}/screenshot/{date}'
        if not os.path.exists(root_path):
            os.makedirs(root_path)
        file_name = '{}/{}.png'.format(root_path, uuid4())
        screenshot = driver.get_screenshot_as_png()
        img = Image.open(io.BytesIO(screenshot))
        region = img.crop((left_top_x, left_top_y, right_bottom_x, right_bottom_y))
        region.save(file_name)
        return file_name
    except Exception:
        pass
    return None

# 计算图片宽高
def get_image_size(img, target_width=None, target_height=None):
    original_width, original_height = img.size
    # 默认为图片原始宽高
    if not target_width and not target_height:
        return (original_width, original_height)

    # 计算目标尺寸
    if target_width and target_height:
        # 情况1:指定宽高参数
        crop_width = min(int(target_width), original_width)
        crop_height = min(int(target_height), original_height)
    elif target_width:
        # 情况2:仅指定宽度,高度按比例计算
        crop_width = min(int(target_width), original_width)
        crop_height = int(original_height * (crop_width / original_width))
    else:
        # 情况3:仅指定高度,宽度按比例计算
        crop_height = min(int(target_height), original_height)
        crop_width = int(original_width * (crop_height / original_height))

    return (crop_width, crop_height)

# 获取页面实际高度(包含滚动区域)
def get_page_height(driver, pages):
    last_scroll_height = 0
    # 获取浏览器可视窗口高度
    inner_height = driver.execute_script("return window.innerHeight")
    total_height = inner_height
    num = 0
    while True:
        if pages > 0 and num >= pages+1:
            break
        scroll_height = driver.execute_script("return window.scrollY") + inner_height
        if scroll_height == last_scroll_height:
            break
        last_scroll_height = scroll_height
        # 之所以每次滚动一屏,是因为某些网站的图片资源,只有在滚动到当前可视窗口时,才会加载出来
        driver.execute_script(f"window.scrollTo(0, {total_height})")
        total_height += inner_height
        num += 1
        time.sleep(2)
    return last_scroll_height

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8181)

 🏷️ 如有疑问,可以关注 我的知识库,直接提问即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2390631.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

知识图谱:AI时代语义认知的底层重构逻辑

在生成式人工智能&#xff08;GEO&#xff09;的技术架构中&#xff0c;知识图谱已从辅助性工具演变为驱动机器认知的核心神经中枢。它通过结构化语义网络的重构&#xff0c;正在突破传统数据处理的线性逻辑&#xff0c;建立机器对复杂业务场景的深度理解能力。 一、语义解构&a…

QGIS新手教程2:线图层与多边形图层基础操作指南(点线互转、中心点提取与WKT导出)

QGIS新手教程&#xff1a;线图层与多边形图层基础操作指南&#xff08;点线互转、中心点提取与WKT导出&#xff09; 目录 QGIS新手教程&#xff1a;线图层与多边形图层基础操作指南&#xff08;点线互转、中心点提取与WKT导出&#xff09;&#x1f4cc; 引言第一部分&#xff1…

Windows环境下Redis的安装使用与报错解决

最近在做项目的时候需要用到Redis&#xff0c;本来没觉得是什么麻烦&#xff0c;下载安装使用一步到位的事&#xff0c;但紧随而来的问题&#xff0c;让我开始怀疑人生&#xff0c;再加上代码跑不出来&#xff0c;我还专门找人给我看看怎么个是&#xff0c;结果就是单纯的Redis…

鸿蒙完整项目-仿盒马App(一)首页静态页面

跟着鸿蒙小林博主&#xff0c;练习下项目~记录下首页的搭建,后续继续完善和整体项目完成会进行布局修改&#xff0c;先按照博主的跟做&#xff0c;后续在改 1.分为底部整体框架搭建 2.首页布局&#xff08;顶部搜索、新人专享、金刚区&#xff08;两个不同集合数据&#xff09…

39-居住证管理系统(小程序)

技术栈: springBootVueMysqlUni-app 功能点: 群众端 警方端 管理员端 群众端: 1.首页: 轮播图展示、公告信息列表 2.公告栏: 公告查看及评论 3.我的: 联系我们: 可在线咨询管理员问题 实时回复 居住证登记申请 回执单查看 领证信息查看 4.个人中心: 个人信息查看及修改…

WPF【11_4】WPF实战-重构与美化(MVVM 架构)

11-9 【理论】MVVM 架构 在 WPF 项目中&#xff0c;我们主要采用的是一种类似 MVC 的架构&#xff0c;叫做 MVVM。 MVVM 继承了 MVC 的理念&#xff0c;是 Model-View-ViewModel 的缩写&#xff0c;中文意思是模型、视图、视图模型。这三个词分开看我们都能看懂&#xff0c;不…

计算逆时针夹角(有向角度)——CAD c# 实现两条线(向量)的逆时针夹角

效果如下&#xff1a; 附部分代码如下&#xff1a; public void 逆时针夹角Demo(){// 获取当前 CAD 文档和编辑器Document doc Application.DocumentManager.MdiActiveDocument;Editor ed doc.Editor;Database db doc.Database;try{Point3d vec1Start, vec1End;if (!GetTwoP…

【Linux】进程 信号的产生

&#x1f33b;个人主页&#xff1a;路飞雪吖~ &#x1f320;专栏&#xff1a;Linux 目录 一、掌握Linux信号的基本概念 &#x1f320;前台进程 VS 后台进程 &#x1f320; 小贴士&#xff1a; &#x1fa84;⼀个系统函数 --- signal() &#x1fa84;查看信号 --- man 7 sign…

机器学习中的维度、过拟合、降维

1. 维度灾难 当我们谈论机器学习模型在处理数据时遇到的困难&#xff0c;一个常常被提及的词便是“维度灾难”&#xff08;Curse of Dimensionality&#xff09;。这不是科幻小说里的情节&#xff0c;而是数学和计算世界里真实存在的困境。它指的正是&#xff1a;当数据集的特…

关于git的使用

下载git 可以去git的官网下载https://git-scm.com/downloads 也可以去找第三方的资源下载&#xff0c;下载后是一个exe应用程序&#xff0c;直接点开一直下一步就可以安装了 右键任意位置显示这两个就代表成功&#xff0c;第一个是git官方的图形化界面&#xff0c;第二个是用…

预约按摩小程序源码介绍

基于ThinkPHP、FastAdmin和UniApp开发的预约按摩小程序源码&#xff0c;ThinkPHP作为后端框架&#xff0c;以其高效稳定著称&#xff0c;能妥善处理数据逻辑与业务规则。FastAdmin作为后台管理框架&#xff0c;极大简化了后台管理系统的搭建与维护。UniApp则让小程序具备跨平台…

Elasticsearch创建快照仓库报错处理

创建快照仓库报错&#xff1a; 根据报错提示的信息&#xff0c;问题可能出在 Elasticsearch 的配置中。当你尝试创建一个文件系统&#xff08;fs&#xff09;类型的快照仓库时&#xff0c;虽然已经指定了 location 参数&#xff0c;但 Elasticsearch 仍然报错&#xff0c;这通…

使用DDR4控制器实现多通道数据读写(十三)

一、概述 在上一章节中使用仿真简单验证了interconnect的功能&#xff0c;使用四个axi4的主端口同时发起读写命令&#xff0c;经过interconnect后&#xff0c;将这些读写指令依次发给ddr4控制器。Ddr4控制器响应后再依次将响应发送到各个通道。从而实现多通道读写ddr4控制器的功…

谷歌Veo vs Sora:AI视频生成技术的巅峰对决

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 ——从架构到实践&#xff0c;解析音画同步、物理模拟与长视频生成的破局之战 一、技术架构&#xff1a;双雄对垒&#xff0c;殊途同归&#xff1f; 谷歌…

基于Spring boot+vue的中医养生系统的设计与实现(源码+论文+部署+安装+调试+售后)

感兴趣的可以先收藏起来&#xff0c;还有大家在毕设选题&#xff0c;项目以及论文编写等相关问题都可以给我留言咨询&#xff0c;我会一一回复&#xff0c;希望帮助更多的人。 系统背景 在健康中国战略持续推进与全民健康意识显著提升的时代背景下&#xff0c;中医养生作为中…

31.第二阶段x64游戏实战-封包-线程发包

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 上一个内容&#xff1a;30.第二阶段x64游戏实战-认识网络数据包发送流程 代码跳转 ws2_32.send跳转sen…

Unity数字人开发笔记

开源工程地址&#xff1a;https://github.com/zhangliwei7758/unity-AI-Chat-Toolkit 先致敬zhangliwei7758&#xff0c;开放这个源码 一、建立工程 建立Unity工程&#xff08;UnityAiChat&#xff09;拖入Unity-AI-Chat-Toolkit.unitypackage打开chatSample工程&#xff0c;可…

嵌入式开发--STM32G431无法正常运行程序,BOOT0与CAN冲突

故障现象 今天开发STM32G431时遇到一个问题&#xff0c;板子打样回来后&#xff0c;焊接完成&#xff0c;可以烧程序&#xff0c;可以读FLASH&#xff0c;却死活不能运行&#xff0c;也不能进仿真调试。 故障定位 经过排查&#xff0c;发现将隔离芯片π121M31拆除&#xff0…

程序环境与预处理

一、程序的翻译环境和执行环境 翻译环境&#xff1a;将源代码转化为可执行的机器指令 执行环境&#xff1a;执行代码 1、翻译环境 流程&#xff1a; 二、运行环境 程序执行过程&#xff1a; 三、预编译阶段 1、预定义符号 __FILE__ //进行编译的原文件名 __LINE__ //文…

《Java 单例模式:从类加载机制到高并发设计的深度技术剖析》

【作者简介】“琢磨先生”--资深系统架构师、985高校计算机硕士&#xff0c;长期从事大中型软件开发和技术研究&#xff0c;每天分享Java硬核知识和主流工程技术&#xff0c;欢迎点赞收藏&#xff01; 一、单例模式的核心概念与设计目标 在软件开发中&#xff0c;我们经常会遇…