js获取图片base64的正确实现方式
作者:wtcl_wtcl
前言
最近遇到了一个需求,要求在python的selenium自动化打开的浏览器中,执行js代码,实现获取图片的base64,并返回给python变量。
看似很简单的一个需求,实际上包含了很多js相关的知识点和开发上的技术点。
今天这篇文章,主要写写整个过程中有用的知识点。
原始方法
其实最开始我没有考虑用js获取图片,所以用pyautogui键盘操作下载图片,代码如下:
import pyautogui from selenium.webdriver import ActionChains driver.get(url) ActionChains(driver).move_to_element(driver.find_element_by_tag_name('img')).context_click().perform() pyautogui.typewrite(['v']) time.sleep(1) pyautogui.typewrite(['enter'])
这里面,主要运用电脑上的快捷键,在选择图片后,右键,然后按v键,就会弹出保存窗口,再按enter,就可以按照默认信息保存。
这种方式是模拟手动,原理简单,但是,在多次重复这个操作时,会发现偶尔会出现一个bug,就是按v键会与保存冲突,使得电脑以为将图片名命名为v,然后整个过程就无法继续进行了。
为了解决上述问题,我才采用js的方法,才有了后面的内容。
第一版js代码
var source = document.getElementsByTagName('img')[0]; // 在网页元素中找到图片资源 var img=new Image(); // 创建一个image元素 img.src=source.src; // 设置img的图片链接为网页图片链接 img.crossOrigin='anonymous'; // 设置跨域的配置,这个是比较常见的,如果不了解,读者可以自行百度 var xhr=new XMLHttpRequest(); // 初始化一个http请求 xhr.open('GET',source.src,false); // 构造请求 xhr.send(null); // 发送请求 xhr.open('GET',source.src,false); // 重复上面步骤,为了确定加载到资源 xhr.send(null); var canvas = document.createElement('canvas'); // 创建canvas元素 var ctx=canvas.getContext('2d'); // 获取canvas二维属性 ctx.drawImage(img,0,0); // 将img画到canvas上 var dataURL = canvas.toDataURL('image/png'); // 利用canvas转换图片为base64形式 return dataURL;
上面的代码运行后,发现获取到的base64还原出的图片是黑色的,运行很多次都这样。所以我又在浏览器console下一条一条执行,结果发现能够给出正常的base64,但是在浏览器直接解析后,发现图片虽然能够正常显示,但是图片的大小是被裁剪的。
仔细思考后,我感觉可能是因为获取的图片的问题,但是看了network的加载记录,发现好像并不是这样,加载的图片都是完整的。所以,可能是canvas画图的时候出了一些问题,所以就去了解canvas的drawImage函数。
研究一番,发现,这个函数第一个参数是图片信息,第二和三个参数是画布的坐标,表示从哪个位置开始画,然后还可以有第四和五个参数,表示要画出的图的宽和高,所以我就修改了那句为
ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight); // 这里面的source是指网页的图片,然后naturalWidth是原宽,naturalHeight是原高
这里就把整个图画上去,但是经过实验后,我发现画出来的图还是裁剪后的图,而且,每次的图的大小都是一样,这就使我明白,会不会是画布的大小出了问题。
经过查阅资料,我发现canvas是有初始大小的,而且如果不改的话,是不会变的。所以,我又在画图前设置canvas的画布大小。
canvas.width=source.naturalWidth; canvas.height=source.naturalHeight;
经过这次,我在浏览器端一条一条执行时,发现代码执行没问题,最后结果都正确。我以为第一天的工作结束了,解决了这个问题,明天简单弄弄就好了,但是一个更麻烦的问题在路上了。
注意:上面的在console下一条一条执行代码和在python中执行js代码的结果是不一样的。因为一条一条执行,中间是有时间消耗的,不好异步任务可以在此期间完成。
第二版js代码
var source = document.getElementsByTagName('img')[0]; // 在网页元素中找到图片资源 var img=new Image(); // 创建一个image元素 img.src=source.src; // 设置img的图片链接为网页图片链接 img.crossOrigin='anonymous'; // 设置跨域的配置,这个是比较常见的,如果不了解,读者可以自行百度 var xhr=new XMLHttpRequest(); // 初始化一个http请求 xhr.open('GET',source.src,false); // 构造请求 xhr.send(null); // 发送请求 xhr.open('GET',source.src,false); // 重复上面步骤,为了确定加载到资源 xhr.send(null); var canvas = document.createElement('canvas'); // 创建canvas元素 canvas.width=source.naturalWidth; // 设置画布宽 canvas.height=source.naturalHeight; // 设置画布高 var ctx=canvas.getContext('2d'); // 获取canvas二维属性 ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight); // 将img画到canvas上 var dataURL = canvas.toDataURL('image/png'); // 利用canvas转换图片为base64形式 return dataURL;
上面代码是前面修改后、我以为正确的代码,但是,第二天放到python中运行后,发现结果还是不对,图片仍然是黑色的,不正确。
我说,难道是我保存的昨天的代码不对吗?所以还是重复错误的步骤,在浏览器下一条一条的运行。结果是正确的,所以我就很纳闷,一样的代码怎么会有不同的结果?
我不理解,但是我感觉可以试试将代码包装从函数的形式,在浏览器端运行。观察一下结果,发现结果给出的内容确实不是图片正常的base64,只能转为黑色的图片。
为了验证我的结果,我还运行了多次,结果都一样。我还将浏览器的正常结果、错误结果和python端的结果进行对比,我发现正确结果是很长的,而两个错误结果一样长度,都很短。
故事到这,陷入了僵局,我不知道该怎么办了。
正好到晚饭点了,所以就先去吃饭吧,吃饭的过程中,看看canvas的文档,了解一下具体的使用方法,说不定会有一些意外的收获。
在晚饭期间,我看到canvas画图的一些注意事项:若调用 drawImage 时,图片没装载完,那什么都不会发生(在一些旧的浏览器中可能会抛出异常)。因此你应该用 load 事件来保证不会在加载完毕之前使用这个图片。(来源于https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images)
官方文档给出了案例:
var img = new Image(); // 创建 img 元素 img.src = 'myImage.png'; // 设置图片源地址 img.onload = function(){ // 执行 drawImage 语句 }
我没有直接将这个作为解决方案,我又多看了一些论坛,发现大部分都是这样实现的。
后面,将这个代码加到我的代码中,我发现return语句在外面的话,还是会返回错值;return语句里面的话,是可以正常执行的,但是返回的值是无法返回到外层的。
于是,我灵机一动,直接return img.onload…
在浏览器下确实成功了,但是,在python这边,却还不对,返回的是一个函数对象。
到此,我明白,要等图片装载完成后,再执行drawimage语句,然后再返回才行。但是,js代码不会等,只要执行了就会继续往下执行下一条,不管你是否执行完成。
第三版js代码
为了解决加载完成的问题,我在想是不是有可以休眠等待的函数,查阅之后,发现可以自己构造一个sleep函数。
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); sleep(1000); // 休眠1s
但是,加在上面代码后面,返回结果还是那样,感觉无法停止代码执行。
经过查阅资料,发现还有一个setTimeout函数,可以设置一段时间后再执行函数,但是输入值只能是函数,这就要求在函数里面返回最后的base64。但是,实际实验了一下,发现这个方式行不通,不返回内容。
然后,继续查阅资料,发现可以通过throw error抛出内容,我当然也尝试了,发现都是uncaught。我当时就纳闷,然后把setTimeout函数也换成了img.onload,但是结果都不对。
中间经过大量的实验,省略n次尝试。。。
最后,我明白了只有在try里面直接执行throw error代码,才能够catch到。至于利用try包裹etTimeout和img.load等耗时异步语句,都不能正常catch,因为正常执行到throw error时,已经结束了外面了try{}catch{}语句的执行,所以根本catch不到。
上面的一大堆过程,让我明白了,根本原因是在异步代码的执行完成前能否不继续执行下面的代码。
最终的js代码
var source = document.getElementsByTagName('img')[0]; var img=new Image(); img.src=source.src; img.crossOrigin='anonymous'; var canvas = document.createElement('canvas'); canvas.width=source.naturalWidth; canvas.height=source.naturalHeight; var ctx=canvas.getContext('2d'); function httpPromise(){ return new Promise((resolve,reject) => { if(img.complete || img.naturalHeight!==0) { ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight); var dataURL = canvas.toDataURL('image/png'); resolve(dataURL); } else{ img.onload=function(){ ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight); var dataURL = canvas.toDataURL('image/png'); resolve(dataURL); } } }) } return httpPromise().then((dataURL)=>{return dataURL;}
最后这版代码做了一些改动,首先我删除了xhr加载的代码,因为这个加载过程和最后的结果没有关系,不影响最后的结果;然后,我添加了Promise异步处理机制,运用这个,就可以等待异步执行结束后,收集所有的结果,并且返回(我记得java也有类似的库可以解决异步等待的问题);最后,我添加了判断机制,防止img.onload因为缓存的原因不响应,所以分成了两种情况进行处理,但都是将结果收集,在return语句处返回。
插一嘴:这里需要return语句,是因为python里面的driver.excute_script函数是将我们的js代码包装成函数,需要返回内容进行处理,否则返回值就直接为None。
上面就是我通过自己摸索,实现的这样一个需求,虽然经过了很多磕磕绊绊,但是结果还是好的。其实,有一篇博客的代码我觉得是优于我的代码。
大佬的代码
// https://blog.csdn.net/yaxuan88521/article/details/122794055 let c = document.createElement('canvas'); let ctx = c.getContext('2d'); let img = document.getElementsByTagName('img')[0]; c.height=img.naturalHeight; c.width=img.naturalWidth; ctx.drawImage(img, 0, 0,img.naturalWidth, img.naturalHeight); let base64String = c.toDataURL(); return base64String;
上面大佬的代码,是直接获取加载后的图片元素,效果是优于我的代码,毕竟减少了一次加载的时间消耗。
总结
本文通过笔者在js获取图片base64的过程中,遇到的问题。
总的说来,可以分为以下几点:
- canvas绘图需要修改画布大小,否则无法画完整。
- canvas的drawimage函数,需要等待图片加载完成,否则就会直接返回画布。
- js代码本身是同步执行的,但是如果实现了一些异步函数,如setTimeout和img.onload等,可能会出现返回结果不正确的情况,所以,如果需要返回值,最好利用funture构造,最后接收返回值。
- 对于同一个需求,最好选择更为方便的方法实现,否则会浪费时间和资源。(笔者在这个地方花费时间有点多,但是对于js也有了更深的认识)
最后,本文主要介绍了js获取图片base64的问题,中间过程有些曲折,内容可能有点凌乱,但是技术点还是给出了,希望各位读者能够从中有所收获。