js獲取圖片base64的正確實(shí)現(xiàn)方式
前言
最近遇到了一個需求,要求在python的selenium自動化打開的瀏覽器中,執(zhí)行js代碼,實(shí)現(xiàn)獲取圖片的base64,并返回給python變量。
看似很簡單的一個需求,實(shí)際上包含了很多js相關(guān)的知識點(diǎn)和開發(fā)上的技術(shù)點(diǎn)。
今天這篇文章,主要寫寫整個過程中有用的知識點(diǎn)。
原始方法
其實(shí)最開始我沒有考慮用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'])
這里面,主要運(yùn)用電腦上的快捷鍵,在選擇圖片后,右鍵,然后按v鍵,就會彈出保存窗口,再按enter,就可以按照默認(rèn)信息保存。
這種方式是模擬手動,原理簡單,但是,在多次重復(fù)這個操作時,會發(fā)現(xiàn)偶爾會出現(xiàn)一個bug,就是按v鍵會與保存沖突,使得電腦以為將圖片名命名為v,然后整個過程就無法繼續(xù)進(jìn)行了。
為了解決上述問題,我才采用js的方法,才有了后面的內(nèi)容。
第一版js代碼
var source = document.getElementsByTagName('img')[0]; // 在網(wǎng)頁元素中找到圖片資源 var img=new Image(); // 創(chuàng)建一個image元素 img.src=source.src; // 設(shè)置img的圖片鏈接為網(wǎng)頁圖片鏈接 img.crossOrigin='anonymous'; // 設(shè)置跨域的配置,這個是比較常見的,如果不了解,讀者可以自行百度 var xhr=new XMLHttpRequest(); // 初始化一個http請求 xhr.open('GET',source.src,false); // 構(gòu)造請求 xhr.send(null); // 發(fā)送請求 xhr.open('GET',source.src,false); // 重復(fù)上面步驟,為了確定加載到資源 xhr.send(null); var canvas = document.createElement('canvas'); // 創(chuàng)建canvas元素 var ctx=canvas.getContext('2d'); // 獲取canvas二維屬性 ctx.drawImage(img,0,0); // 將img畫到canvas上 var dataURL = canvas.toDataURL('image/png'); // 利用canvas轉(zhuǎn)換圖片為base64形式 return dataURL;
上面的代碼運(yùn)行后,發(fā)現(xiàn)獲取到的base64還原出的圖片是黑色的,運(yùn)行很多次都這樣。所以我又在瀏覽器console下一條一條執(zhí)行,結(jié)果發(fā)現(xiàn)能夠給出正常的base64,但是在瀏覽器直接解析后,發(fā)現(xiàn)圖片雖然能夠正常顯示,但是圖片的大小是被裁剪的。
仔細(xì)思考后,我感覺可能是因?yàn)楂@取的圖片的問題,但是看了network的加載記錄,發(fā)現(xiàn)好像并不是這樣,加載的圖片都是完整的。所以,可能是canvas畫圖的時候出了一些問題,所以就去了解canvas的drawImage函數(shù)。
研究一番,發(fā)現(xiàn),這個函數(shù)第一個參數(shù)是圖片信息,第二和三個參數(shù)是畫布的坐標(biāo),表示從哪個位置開始畫,然后還可以有第四和五個參數(shù),表示要畫出的圖的寬和高,所以我就修改了那句為
ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight); // 這里面的source是指網(wǎng)頁的圖片,然后naturalWidth是原寬,naturalHeight是原高
這里就把整個圖畫上去,但是經(jīng)過實(shí)驗(yàn)后,我發(fā)現(xiàn)畫出來的圖還是裁剪后的圖,而且,每次的圖的大小都是一樣,這就使我明白,會不會是畫布的大小出了問題。
經(jīng)過查閱資料,我發(fā)現(xiàn)canvas是有初始大小的,而且如果不改的話,是不會變的。所以,我又在畫圖前設(shè)置canvas的畫布大小。
canvas.width=source.naturalWidth; canvas.height=source.naturalHeight;
經(jīng)過這次,我在瀏覽器端一條一條執(zhí)行時,發(fā)現(xiàn)代碼執(zhí)行沒問題,最后結(jié)果都正確。我以為第一天的工作結(jié)束了,解決了這個問題,明天簡單弄弄就好了,但是一個更麻煩的問題在路上了。
注意:上面的在console下一條一條執(zhí)行代碼和在python中執(zhí)行js代碼的結(jié)果是不一樣的。因?yàn)橐粭l一條執(zhí)行,中間是有時間消耗的,不好異步任務(wù)可以在此期間完成。
第二版js代碼
var source = document.getElementsByTagName('img')[0]; // 在網(wǎng)頁元素中找到圖片資源 var img=new Image(); // 創(chuàng)建一個image元素 img.src=source.src; // 設(shè)置img的圖片鏈接為網(wǎng)頁圖片鏈接 img.crossOrigin='anonymous'; // 設(shè)置跨域的配置,這個是比較常見的,如果不了解,讀者可以自行百度 var xhr=new XMLHttpRequest(); // 初始化一個http請求 xhr.open('GET',source.src,false); // 構(gòu)造請求 xhr.send(null); // 發(fā)送請求 xhr.open('GET',source.src,false); // 重復(fù)上面步驟,為了確定加載到資源 xhr.send(null); var canvas = document.createElement('canvas'); // 創(chuàng)建canvas元素 canvas.width=source.naturalWidth; // 設(shè)置畫布寬 canvas.height=source.naturalHeight; // 設(shè)置畫布高 var ctx=canvas.getContext('2d'); // 獲取canvas二維屬性 ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight); // 將img畫到canvas上 var dataURL = canvas.toDataURL('image/png'); // 利用canvas轉(zhuǎn)換圖片為base64形式 return dataURL;
上面代碼是前面修改后、我以為正確的代碼,但是,第二天放到python中運(yùn)行后,發(fā)現(xiàn)結(jié)果還是不對,圖片仍然是黑色的,不正確。
我說,難道是我保存的昨天的代碼不對嗎?所以還是重復(fù)錯誤的步驟,在瀏覽器下一條一條的運(yùn)行。結(jié)果是正確的,所以我就很納悶,一樣的代碼怎么會有不同的結(jié)果?
我不理解,但是我感覺可以試試將代碼包裝從函數(shù)的形式,在瀏覽器端運(yùn)行。觀察一下結(jié)果,發(fā)現(xiàn)結(jié)果給出的內(nèi)容確實(shí)不是圖片正常的base64,只能轉(zhuǎn)為黑色的圖片。
為了驗(yàn)證我的結(jié)果,我還運(yùn)行了多次,結(jié)果都一樣。我還將瀏覽器的正常結(jié)果、錯誤結(jié)果和python端的結(jié)果進(jìn)行對比,我發(fā)現(xiàn)正確結(jié)果是很長的,而兩個錯誤結(jié)果一樣長度,都很短。
故事到這,陷入了僵局,我不知道該怎么辦了。
正好到晚飯點(diǎn)了,所以就先去吃飯吧,吃飯的過程中,看看canvas的文檔,了解一下具體的使用方法,說不定會有一些意外的收獲。
在晚飯期間,我看到canvas畫圖的一些注意事項(xiàng):若調(diào)用 drawImage 時,圖片沒裝載完,那什么都不會發(fā)生(在一些舊的瀏覽器中可能會拋出異常)。因此你應(yīng)該用 load 事件來保證不會在加載完畢之前使用這個圖片。(來源于https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images)
官方文檔給出了案例:
var img = new Image(); // 創(chuàng)建 img 元素 img.src = 'myImage.png'; // 設(shè)置圖片源地址 img.onload = function(){ // 執(zhí)行 drawImage 語句 }
我沒有直接將這個作為解決方案,我又多看了一些論壇,發(fā)現(xiàn)大部分都是這樣實(shí)現(xiàn)的。
后面,將這個代碼加到我的代碼中,我發(fā)現(xiàn)return語句在外面的話,還是會返回錯值;return語句里面的話,是可以正常執(zhí)行的,但是返回的值是無法返回到外層的。
于是,我靈機(jī)一動,直接return img.onload…
在瀏覽器下確實(shí)成功了,但是,在python這邊,卻還不對,返回的是一個函數(shù)對象。
到此,我明白,要等圖片裝載完成后,再執(zhí)行drawimage語句,然后再返回才行。但是,js代碼不會等,只要執(zhí)行了就會繼續(xù)往下執(zhí)行下一條,不管你是否執(zhí)行完成。
第三版js代碼
為了解決加載完成的問題,我在想是不是有可以休眠等待的函數(shù),查閱之后,發(fā)現(xiàn)可以自己構(gòu)造一個sleep函數(shù)。
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); sleep(1000); // 休眠1s
但是,加在上面代碼后面,返回結(jié)果還是那樣,感覺無法停止代碼執(zhí)行。
經(jīng)過查閱資料,發(fā)現(xiàn)還有一個setTimeout函數(shù),可以設(shè)置一段時間后再執(zhí)行函數(shù),但是輸入值只能是函數(shù),這就要求在函數(shù)里面返回最后的base64。但是,實(shí)際實(shí)驗(yàn)了一下,發(fā)現(xiàn)這個方式行不通,不返回內(nèi)容。
然后,繼續(xù)查閱資料,發(fā)現(xiàn)可以通過throw error拋出內(nèi)容,我當(dāng)然也嘗試了,發(fā)現(xiàn)都是uncaught。我當(dāng)時就納悶,然后把setTimeout函數(shù)也換成了img.onload,但是結(jié)果都不對。
中間經(jīng)過大量的實(shí)驗(yàn),省略n次嘗試。。。
最后,我明白了只有在try里面直接執(zhí)行throw error代碼,才能夠catch到。至于利用try包裹etTimeout和img.load等耗時異步語句,都不能正常catch,因?yàn)檎?zhí)行到throw error時,已經(jīng)結(jié)束了外面了try{}catch{}語句的執(zhí)行,所以根本catch不到。
上面的一大堆過程,讓我明白了,根本原因是在異步代碼的執(zhí)行完成前能否不繼續(xù)執(zhí)行下面的代碼。
最終的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加載的代碼,因?yàn)檫@個加載過程和最后的結(jié)果沒有關(guān)系,不影響最后的結(jié)果;然后,我添加了Promise異步處理機(jī)制,運(yùn)用這個,就可以等待異步執(zhí)行結(jié)束后,收集所有的結(jié)果,并且返回(我記得java也有類似的庫可以解決異步等待的問題);最后,我添加了判斷機(jī)制,防止img.onload因?yàn)榫彺娴脑虿豁憫?yīng),所以分成了兩種情況進(jìn)行處理,但都是將結(jié)果收集,在return語句處返回。
插一嘴:這里需要return語句,是因?yàn)閜ython里面的driver.excute_script函數(shù)是將我們的js代碼包裝成函數(shù),需要返回內(nèi)容進(jìn)行處理,否則返回值就直接為None。
上面就是我通過自己摸索,實(shí)現(xiàn)的這樣一個需求,雖然經(jīng)過了很多磕磕絆絆,但是結(jié)果還是好的。其實(shí),有一篇博客的代碼我覺得是優(yōu)于我的代碼。
大佬的代碼
// 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;
上面大佬的代碼,是直接獲取加載后的圖片元素,效果是優(yōu)于我的代碼,畢竟減少了一次加載的時間消耗。
總結(jié)
本文通過筆者在js獲取圖片base64的過程中,遇到的問題。
總的說來,可以分為以下幾點(diǎn):
- canvas繪圖需要修改畫布大小,否則無法畫完整。
- canvas的drawimage函數(shù),需要等待圖片加載完成,否則就會直接返回畫布。
- js代碼本身是同步執(zhí)行的,但是如果實(shí)現(xiàn)了一些異步函數(shù),如setTimeout和img.onload等,可能會出現(xiàn)返回結(jié)果不正確的情況,所以,如果需要返回值,最好利用funture構(gòu)造,最后接收返回值。
- 對于同一個需求,最好選擇更為方便的方法實(shí)現(xiàn),否則會浪費(fèi)時間和資源。(筆者在這個地方花費(fèi)時間有點(diǎn)多,但是對于js也有了更深的認(rèn)識)
最后,本文主要介紹了js獲取圖片base64的問題,中間過程有些曲折,內(nèi)容可能有點(diǎn)凌亂,但是技術(shù)點(diǎn)還是給出了,希望各位讀者能夠從中有所收獲。
附:js實(shí)現(xiàn)base64格式編碼圖片(通俗,易懂)
相關(guān)文章
JavaScript實(shí)現(xiàn)拖拽元素對齊到網(wǎng)格(每次移動固定距離)
最近在做一個拖拽元素的附加功能,就是對齊到網(wǎng)格,實(shí)際上就是確定好元素的初始位置,然后拖拽元素時,每次移動固定的距離。讓元素都可以在網(wǎng)格內(nèi)對齊2016-11-11JS實(shí)現(xiàn)下拉菜單列表與登錄注冊彈窗效果
下面小編就為大家?guī)硪黄狫S實(shí)現(xiàn)下拉菜單列表與登錄注冊彈窗效果。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08微信小程序連接服務(wù)器展示MQTT數(shù)據(jù)信息的實(shí)現(xiàn)
這篇文章主要介紹了微信小程序連接服務(wù)器展示MQTT數(shù)據(jù)信息的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07JavaScript實(shí)現(xiàn)日期格式化的操作詳解
在我們做業(yè)務(wù)開發(fā)的漫長歲月里,會多次跟時間打交道,相信大多數(shù)小伙伴對日期格式化也并不陌生,本文簡單記錄了JavaScript實(shí)現(xiàn)日期格式化的過程,以及一些拓展,希望對大家有所幫助2023-05-05