文地址:http://sdk-design.js.org/
介紹
本指南為您介紹了在臺式機和移動網絡在不同的平臺和瀏覽器( < 99.99 %我可能會跳過一些瀏覽器)開發的JavaScript SDK ,對于那些非瀏覽器開發的支持(硬件,嵌入式,節點/ IO JS )被排除在本文檔之外,在未來予以考慮。
因為我沒有找到一個關于設計JavaScript SDK的比較好的文檔,所以我在這里收集并記下了我個人的經驗。這份文檔已經寫了好幾個月,有一點我們需要知道,JavaScript的SDK-設計不僅僅是設計SDK本身,這也是有關于開發者與設備瀏覽器中間的聯系。我們寫的越多,越會更多的思考我們真正關心的是不同平臺和瀏覽器之間的性能和兼容問題。你可以根據情況自由的更改或者完全放棄我在文章里列出的建議。
什么是SDK
我知道它確實是很普通很常見。一般是一些軟件工程師為特定的軟件包、軟件框架、硬件平臺、操作系統等建立應用軟件時的開發工具的集合。通常一個SDK包含一個或多個API,編程工具和檔。
設計理念
這取決于你的SDK用來干什么的,但是它必須具備原生的,短,速度快,干凈,可讀可測試特性。用原生javascript寫,不要用像Livescript, Coffeescript, Typescript和其它的編譯語言。必須有更好的方法來編寫自己的javascript原生代碼比別人更快。請不要在你的SDK里用JQuery,除非它非常有必要。你可以使用其它的類似jQuery的庫,譬如zetpo.js,用于DOM操作,如果你需要用到HTTP Ajax請求,可以使用另外一種輕量庫像window.fetch。
每一次的SDK版本發布,確保它不僅適用于舊版本而且適應于未來的新版本。所以,記得為你的SDK寫文檔,代碼要寫注釋,同時做好單元測試和用戶場景測試。
適應范圍
基于《Third-Party JavaScript》這本書。在何種情況下,你應該為你的應用設計一個JavaScript SDK?
在什么情況下,我們應該在JavaScript環境中使用SDK呢?大家可以想想還有其它情沒?
引入SDK
建議你采用異步加載腳本的方式。我們要優化網站的用戶體驗,所以不希望我們的SDK庫阻塞其它主要進程。
異步加載
(function() {vars=document.createElement('script');s.type='text/javascript';s.async=true;s.src='http://xxx.com/sdk.js';varx=document.getElementsByTagName('script')[0];x.parentNode.insertBefore(s, x);})();**
在新的現代瀏覽器(chrome)你可以使用
<script asyncsrc="http://xxx.com/sdk.js"></script>
傳統加載方法
<script type="text/javascript"src="http://xxx.com/sdk.js"></script>
對比:
下面是簡單的圖形顯示異步加載和傳統同步加載方式之間的區別
異步:
|----A-----| |-----B-----------| |-------C------|
同步:
|----A-----||-----B-----------||-------C------|
異步和延遲腳本執行解釋
異步的問題
當你使用異步加載的時候,將會出現,頁面中的函數無法正常調用SDK方法的情況。
<script> (function () { var s=document.createElement('script'); s.type='text/javascript'; s.async=true; s.src='http://xxx.com/sdk.js'; var x=document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })(); // execute your script immediately hereSDKName('some arguments'); </script>
結果會報undefined錯誤,因為SDKName()在腳本加載之前執行了。所以我們應該使用點技巧讓腳本正確執行。把事件保存在SDKName.q數組里,SDK初始化的時候執行SDKName.q。
<script> (function () { // add a queue event here SDKName=SDKName ||function () { (SDKName.q=SDKName.q|| []).push(arguments); }; var s=document.createElement('script'); s.type='text/javascript'; s.async=true; s.src='http://xxx.com/sdk.js'; var x=document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })(); // execute your script immediately hereSDKName('some arguments'); </script>
或者用 [ ].push
<script> (function () { // add a queue event here SDKName=window.SDKName|| (window.SDKName=[]); var s=document.createElement('script'); s.type='text/javascript'; s.async=true; s.src='http://xxx.com/sdk.js'; var x=document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })(); // execute your script immediately hereSDKName.push(['some arguments']); </script>
其他方式
還有其它不同方式加載腳本
Import in ES2015
import"your-sdk";
模塊加載
這里有完整的源碼和非常棒的教程. Loading JavaScript Modules
module('sdk.js',['sdk-track.js', 'sdk-beacon.js'],function(track, beacon) { // sdk definitions, split into local and global/exported definitions// local definitions// exports }); // you should contain this "module" method (function () { var modules={}; // private record of module data// modules are functions with additional informationfunctionmodule(name,imports,mod) { // record module informationwindow.console.log('found module '+name); modules[name]={name:name, imports: imports, mod: mod}; // trigger loading of import dependenciesfor (var imp in imports) loadModule(imports[imp]); // check whether this was the last module to be loaded// in a given dependency grouploadedModule(name); } // function loadModule// function loadedModulewindow.module=module; })();
SDK版本
避免使用自己的特例作為版本名稱像
標識-v<時間戳>.js 標識-v<日期>.js 標識-v1-v2.js
它可能導致使用SDK的開發者很混亂不知道哪個是最新版本。
使用 Semantic Versioning (語義化版本規范)去定義SDK的版本號以”大.小.補丁”形式。
版本以v1.0.0 v1.5.0 v2.0.0的形式,會讓使用者搜索跟蹤日志文件更容易。
通常情況下,我們會有不同的方式去聲明SDK的版本,這取決于具體針對的業務和設計。
使用查詢字符串路徑
http://xxx.com/sdk.js?v=1.0.0
使用文件夾命名
http://xxx.com/v1.0.0/sdk.js
使用主機名或者子域名
http://v1.xxx.com/sdk.js
為了以后版本的升級迭代,建議用stable unstable alpha latest experimental 版本。
http://xxx.com/sdk-stable.js http://xxx.com/sdk-unstable.js http://xxx.com/sdk-alpha.js http://xxx.com/sdk-latest.js http://xxx.com/sdk-experimental.js
更新日志文件
你應該注意到如果你升級你的SDK卻沒通知用戶,用戶不會知道。記得寫更新日志來記錄無論是主要、次要甚至bug修復等修改。這將是一個好的開發經驗,我們能快速的跟蹤到SDK某個API的修改。所以保持更新日志 – Keep a Changelog, Github Repo
每個版本的日志應該有:
[新增] 新功能.
[更新] 修改現有的更能
[廢棄] 在即將發布的版本中刪除某個功能.
[刪除] 在這個版本中刪除棄用的功能.
[修正] bug修復
[安全] 邀請用戶對安全進行升級
命名空間
在你的SDK里只定義一個全局命名空間,并且不要用太過通用的名字,避免和其它類庫名發生沖突。SDK的主體用(function () { … })()包裹。這種做法越來越普遍的應用于各種流行的javascript類庫譬如jQuery,Node.js等等。這種創建私有的命名空間的技術很重要,有助于避免各種類庫之間命名的沖突。
為了避免命名空間沖突
學習Google Analytics的做法,你可以通過改變 ga的值來定義你自己的命名空間。
(function(i,s,o,g,r,a,m) {i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o) [0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google- analytics.com/analytics.js','ga');
下面的是 openX的做法,支持通過給地址傳遞參數定義命名空間。
<script src="http://your_domain/sdk?namespace=yourcompany"></script>
存儲機制
cookie
使用cookie就會面臨復雜的作用域范圍問題,而且涉及到子域和路徑問題。
比如在路徑 path=/下, cookie first=value1 在域名 http://github.com下, 另外一個 cookie second=value2 在域名 http://sub.github.com下
http://github.comhttp://sub.github.comfirst=value1??second=value2??
有個 cookie first=value1 在 http://github.com下, cookie second=value2 在 http://github.com/path1 另外一個 cookie third=value3 在 http://sub.github.com下,
http://github.comhttp://github.com/path1http://sub.github.comfirst=value1???second=value2???third=value3???
檢查 Cookie 可讀寫
給定一個域 (默認當前主機域名), 檢查cookie是否可讀寫。
var checkCookieWritable=function(domain) { try { // Create cookie document.cookie='cookietest=1' + (domain ? '; domain=' + domain : ''); var ret=document.cookie.indexOf('cookietest=') !=-1; // Delete cookie document.cookie='cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT' + (domain ? '; domain=' + domain : ''); return ret; } catch (e) { return false; } };
檢查第三方 Cookie 可讀寫
檢查第三方cookie僅僅通過客戶端js是辦不到的,需要服務器端配合。
寫 讀 刪除 Cookie 代碼
代碼片段寫/讀/刪除cookie的腳本。
var cookie={ write: function(name, value, days, domain, path) { var date=new Date(); days=days || 730; // two years path=path || '/'; date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires='; expires=' + date.toGMTString(); var cookieValue=name + '=' + value + expires + '; path=' + path; if (domain) { cookieValue +='; domain=' + domain; } document.cookie=cookieValue; }, read: function(name) { var allCookie='' + document.cookie; var index=allCookie.indexOf(name); if (name===undefined || name==='' || index===-1) return ''; var ind1=allCookie.indexOf(';', index); if (ind1==-1) ind1=allCookie.length; return unescape(allCookie.substring(index + name.length + 1, ind1)); }, remove: function(name) { if (this.read(name)) { this.write(name, '', -1, '/'); } } };
Session
js寫不了session,需要服務器端寫。
一個頁面的session會一直保存著只要瀏覽器是開著的即使頁面重新加載。打開一個新頁面會生成一個新的session。子窗口會和父窗口共享一個session。
LocalStorage
存儲的數據沒有時間限制。存儲數據量大(至少5MB)并且信息不會傳送到服務器。而且同一個域名從http和https訪問localStorage是不共享的。你可以在你的網頁上創建個iframe,然后用postMessage方法去傳值到父頁面。HOW TO?
檢查 LocalStorage 可寫
window.localStorage 并不是任何瀏覽器都支持,SDK在用之前要檢查是否可用。
var testCanLocalStorage=function() { var mod='modernizr'; try { localStorage.setItem(mod, mod); localStorage.removeItem(mod); return true; } catch (e) { return false; } };
SessionStorage
針對一個 session 的數據存儲(當用戶關閉瀏覽器窗口后,數據會被刪除).
檢查 SessionStorage 可寫
var checkCanSessionStorage=function() { var mod='modernizr'; try { sessionStorage.setItem(mod, mod); sessionStorage.removeItem(mod); return true; } catch (e) { return false; } }
事件
在客戶端瀏覽器有很多事件加載、卸載、綁定等會存在兼容問題。polyfills是個解決不同平臺事件綁定的不錯的解決方案。
Document Ready
確保整個頁面完成加載了再執行SDK方法。
// handle IE8+ function ready (fn) { if (document.readyState !='loading') { fn(); } else if (window.addEventListener) { // window.addEventListener('load', fn); window.addEventListener('DOMContentLoaded', fn); } else { window.attachEvent('onreadystatechange', function() { if (document.readyState !='loading') fn(); }); } }
DOMContentLoaded - 所有DOM解析完會觸發整個事件 不需要等到樣式表、圖片等加載完。
load 頁面完整加載。
Message Event
這里是實現iframe和父頁面之間的數據通信, 這里有文檔 API documentation.
// in the iframe parent.postMessage("Hello"); // string //==========================================// in the iframe's parent // Create IE + others compatible event handler var eventMethod=window.addEventListener ? "addEventListener" : "attachEvent"; var eventer=window[eventMethod]; var messageEvent=eventMethod=="attachEvent" ? "onmessage" : "message"; // Listen to message from child window eventer(messageEvent,function(e) { // e.origin , check the message origin console.log('parent received message!: ',e.data); },false);
發送的數據是字符串, 對于使用更高級的json字符串. 不是所有的瀏覽器對支持 Structured Clone Algorithm on the parameter, (參數的結構化克隆)。
Orientation Change 橫屏事件
檢測設備橫屏
window.addEventListener('orientationchange', fn);
獲取旋轉方向和角度
window.orientation; //=> 90, -90, 0
Screen portrait-primary(豎屏正方向), portrait-secondary(豎屏反方向), landscape-primary(橫屏正方向), landscape-secondary (橫屏反方向)(Experimental)
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation var orientation=screen.orientation || screen.mozOrientation || screen.msOrientation;
Request
我們的SDK和服務器之間通信通過Ajax請求,因為我們知道我們可以使用jQuery的Ajax 方法。但是有更好的方案來實現它。
圖片預加載
通過創建一個Image對象預加載一張圖片。為了防止瀏覽器緩存記得加上時間戳。
(new Image()).src='http://xxxxx.com/collect?id=1111';
要注意通過GET方式傳輸參數最大長度是2048個字節(取決于不同的瀏覽器和服務器)。這里要做一些處理如果超過長度。
if (length > 2048) { // do Multiple Post (form) } else { // do Image Beacon }
你可能遇到問題在使用encodeURI 還是 encodeURIComponent的時候,最好理解它們的區別。 See below.
對于圖像加載成功/錯誤回調
var img=new Image(); img.src='http://xxxxx.com/collect?id=1111'; img.onload=successCallback; img.onerror=errorCallback;
單個 Post 請求
普通表單發送一個對應元素和值
var form=document.createElement('form'); var input=document.createElement('input'); form.style.display='none'; form.setAttribute('method', 'POST'); form.setAttribute('action', 'http://xxxx.com/track'); input.name='username'; input.value='attacker'; form.appendChild(input); document.getElementsByTagName('body')[0].appendChild(form); form.submit();
多個 Post 請求
服務通常比較復雜,需要通過POST方法發送更多數據。
function requestWithoutAjax( url, params, method ){ params=params || {}; method=method || "post"; // function to remove the iframe var removeIframe=function( iframe ){ iframe.parentElement.removeChild(iframe); }; // make a iframe... var iframe=document.createElement('iframe'); iframe.style.display='none'; iframe.onload=function(){ var iframeDoc=this.contentWindow.document; // Make a invisible form var form=iframeDoc.createElement('form'); form.method=method; form.action=url; iframeDoc.body.appendChild(form); // pass the parameters for( var name in params ){ var input=iframeDoc.createElement('input'); input.type='hidden'; input.name=name; input.value=params[name]; form.appendChild(input); } form.submit(); // remove the iframe setTimeout( function(){ removeIframe(iframe); }, 500); }; document.body.appendChild(iframe); } requestWithoutAjax('url/to', { id: 2, price: 2.5, lastname: 'Gamez'});
Iframe
當你在需要在頁面中生成內容時候,你可以通過iframe嵌入。
var iframe=document.createElement('iframe'); var body=document.getElementsByTagName('body')[0]; iframe.style.display='none'; iframe.src='http://xxxx.com/page'; iframe.onreadystatechange=function () { if (iframe.readyState !=='complete') { return; } }; iframe.onload=loadCallback; body.appendChild(iframe);
清除iframe的邊框,內部margin值。
<iframe src="..." marginwidth="0" marginheight="0" hspace="0" vspace="0" frameborder="0" scrolling="no"> </iframe>
iframe中插入html
<iframe id="iframe"></iframe> <script> var html_string="content <script>alert(location.href); </script>"; document.getElementById('iframe').src="data:text/html;charset=utf-8," + escape(html_string); // alert data:text/html;charset=utf-8..... // access cookie get ERROR var doc=document.getElementById('iframe').contentWindow.document; doc.open(); doc.write('<body>Test<script>alert(location.href);</script></body>'); doc.close(); // alert "top window url" var iframe=document.createElement('iframe'); iframe.src='javascript:;\\\\'' + encodeURI('<html><body> <script>alert(location.href);</body></html>') + '\\\\''; // iframe.src='javascript:;"' + encodeURI((html_tag).replace(/\\\\"/g, '\\\\\\\\\\\\"')) + '"'; document.body.appendChild(iframe); // alert "about:blank" </script>
jsonp
這種情況下,你的服務器需要響應JavaScript 代碼,并讓瀏覽器執行它,僅僅通過js腳本鏈接。
(function () { var s=document.createElement('script'); s.type='text/javascript'; s.async=true; s.src='/yourscript? some=parameter&callback=jsonpCallback'; var x=document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })();
關于jsonp你需要了解:
XMLHttpRequest
自己寫XMLHttpRequest不是個好主意,因為你要浪費很多時間去做IE或者其它瀏覽器的兼容。這里提供一些現成的解決方案供大家參考:
1 - window.fetch - A window.fetch JavaScript polyfill.
2 - got - Simplified HTTP/HTTPS requests
3 - microjs - list of ajax lib
4 – more
Maximum Number of Connection
檢查不同瀏覽器的最大連接數 browserscope
調試
模擬多個域
你不需要注冊多個域名來模擬域,在本地搭建個虛擬服務器,綁定host的方式就可以:
$ sudo vim /etc/hosts
添加以下條目
#refer to localhost 127.0.0.1 publisher.net 127.0.0.1 sdk.net
然后你就可以訪問該頁面http://publisher.net和http://sdk.net
Developer Tools
用瀏覽器自帶的調試工具,Chrome Developer Tool 、Safari Developer Tools、Firebug都是不錯的選擇。
開發工具也簡稱為工具。
工具提供Web開發者深進入瀏覽器和Web應用程序的內部。使用工具來有效地追蹤布局問題,將JavaScript打斷點,并獲得代碼優化的建議。
控制臺日志
用于測試和輸出文本和其他一般的調試, 控制臺日志可通過瀏覽器的API log()輸出顯示。有各種各樣的方法和格式輸出你的信息,了解更多API: Console API.
調試代理
代理在你調試SDK的很多時候都很有用。 修改cookies, headers, cache, 編輯 http request/response, SSL Proxying, ajax 調試等等。
這里推薦一些代理工具:
BrowserSync
Browsersync能讓瀏覽器實時、快速響應您的文件更改(html、js、css、sass、less等)并自動刷新頁面。更重要的是 Browsersync可以同時在PC、平板、手機等設備下進項調試。它真的很有幫助如果你需要跨平臺測試你的SDK)。
提示和小技巧
Console Logs Polyfill(Polyfilling 是由 RemySharp 提出的一個術語,它是用來描述復制缺少的 API 和API 功能的行為)
這不是一個真正的polyfill,只是保證在調用console.log API的時候不拋出錯誤。
if (typeof console==="undefined") { var f=function() {}; console={ log: f, debug: f, error: f, info: f };}
EncodeURI or EncodeURIComponent
理解三者的不同 escape()、encodeURI()、encodeURIComponent()
here.
記住使用 encodeURI()和encodeURIComponent()有11個字符不同。 它們是: # $ & + , / : ;=? @ more discussion。
你可能真的不需要JQuery
正如標題所說, 你可能真的不需要JQuery。如果你正在找一些公共的代碼那下面這些會很有用:- AJAX EFFECTS, ELEMENTS, EVENTS, UTILS
你不需要 jQuery
Free yourself from the chains of jQuery by embracing and understanding the modern Web API and discovering various directed libraries to help you fill in the gaps.
http://blog.garstasio.com/you-dont-need-jquery/
有用的 Tips
Selecting Elements
DOM Manipulation
回調函數加載腳本
類似于 異步加載腳本 增加回調函數。
function loadScript(url, callback) { var script=document.createElement('script'); script.async=true; script.src=url; var entry=document.getElementsByTagName('script')[0]; entry.parentNode.insertBefore(script, entry); script.onload=script.onreadystatechange=function () { var rdyState=script.readyState; if (!rdyState || /complete|loaded/.test(script.readyState)) { callback(); // detach the event handler to avoid memory leaks in IE (http://mng.bz/W8fx) script.onload=null; script.onreadystatechange=null; } }; }
執行一次函數
這里展示了如何實現函數只執行一次。
每當你想有一個只運行一次的函數。通常這些函數是以事件監聽的方式,很難管理。當然如果很容易管理,你只需要刪除監聽事件,但是這是個理想的狀態,很多時候你只需要允許一個函數執行一次。下面的代碼可以實現:
// Copy from DWB // http://davidwalsh.name/javascript-once function once(fn, context) { var result; return function() { if(fn) { result=fn.apply(context || this, arguments); fn=null; } return result; }; } // Usagevar canOnlyFireOnce=once(function() { console.log('Fired!');}); canOnlyFireOnce(); // "Fired!"canOnlyFireOnce(); // nada
獲取樣式
獲取行間樣式
<span id="black" style="color: black"> This is black color span </span> <script> document.getElementById('black').style.color; //=> black</script>
獲取真正的樣式
<style> #black { color: red !important;} </style> <span id="black" style="color: black"> This is black color span </span> <script> document.getElementById('black').style.color; //=> black // real var black=document.getElementById('black'); window.getComputedStyle(black, null).getPropertyValue('color'); //=> rgb(255, 0, 0) </script>
ref:https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle
檢測當前窗口
了解更多: here。
chrome中使用F12獲取的nerwork中的網絡請求,可以看到request headers,直接復制下來是一個字符串,可以手動修改為python requests可用的字典,但是費時費力,下面這段代碼,可以直接把復制下來的字符串類型,轉換為requests可用的python字典。
天要給大家介紹的是驗證碼的爬取和識別,不過只涉及到最簡單的圖形驗證碼,也是現在比較常見的一種類型。
運行平臺:Windows
Python版本:Python3.6
IDE: Sublime Text
其他:Chrome瀏覽器
想要學習更多驗證、破解網站驗證碼的知識點,關注小編后私信學習資料,領取相關學習資料
簡述流程:
步驟1:簡單介紹驗證碼
步驟2:爬取少量驗證碼圖片
步驟3:介紹百度文字識別OCR
步驟4:識別爬取的驗證碼
步驟5:簡單圖像處理
目前,很多網站會采取各種各樣的措施來反爬蟲,驗證碼就是其中一種,比如當檢測到訪問頻率過高時會彈出驗證碼讓你輸入,確認訪問網站的不是機器人。但隨著爬蟲技術的發展,驗證碼的花樣也越來越多,從最開始簡單的幾個數字或字母構成的圖形驗證碼(也就是我們今天要涉及的)發展到需要點擊倒立文字字母的、與文字相符合的圖片的點觸型驗證碼,需要滑動到合適位置的極驗滑動驗證碼,以及計算題驗證碼等等,總之花樣百出,讓人頭禿。驗證碼其他的相關知識大家可以看下這個網站:captcha.org
再來簡單說下圖形驗證碼吧,就像這張:
由字母和數字組成,再加上一些噪點,但為了防止被識別,簡單的圖形驗證碼現在也變得復雜,有的加了干擾線,有的加噪點,有的加上背景,字體扭曲、粘連、鏤空、混用等等,甚至有時候人眼都難以識別,只能默默點擊“看不清,再來一張”。
驗證碼難度的提高隨之帶來的就是識別的成本也需要提高,在接下來的識別過程中,我會先直接使用百度文字識別OCR,來測試識別準確度,再確認是否選擇轉灰度、二值化以及去干擾等圖像操作優化識別率。
接下來我們就來爬取少量驗證碼圖片存入文件。
首先打開Chrome瀏覽器,訪問剛剛介紹的網站,里面有一個captcha圖像樣本鏈接:https://captcha.com/captcha-examples.html?cst=corg,網頁里有60張不同類型的圖形驗證碼,足夠我們用來識別試驗了。
直接來看代碼吧:
import requests import os import time from lxml import etree def get_Page(url,headers): response=requests.get(url,headers=headers) if response.status_code==200: # print(response.text) return response.text return None def parse_Page(html,headers): html_lxml=etree.HTML(html) datas=html_lxml.xpath('.//div[@class="captcha_images_left"]|.//div[@class="captcha_images_right"]') item={} # 創建保存驗證碼文件夾 file='D:/******' if os.path.exists(file): os.chdir(file) else: os.mkdir(file) os.chdir(file) for data in datas: # 驗證碼名稱 name=data.xpath('.//h3') # print(len(name)) # 驗證碼鏈接 src=data.xpath('.//div/img/@src') # print(len(src)) count=0 for i in range(len(name)): # 驗證碼圖片文件名 filename=name[i].text + '.jpg' img_url='https://captcha.com/' + src[i] response=requests.get(img_url,headers=headers) if response.status_code==200: image=response.content with open(filename,'wb') as f: f.write(image) count +=1 print('保存第{}張驗證碼成功'.format(count)) time.sleep(1) def main(): url='https://captcha.com/captcha-examples.html?cst=corg' headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36'} html=get_Page(url,headers) parse_Page(html,headers) if __name__=='__main__': main()
仍然使用Xpath爬取,在右鍵檢查圖片時可以發現,網頁分為兩欄,如下圖紅框所示,根據class分為左右兩欄,驗證碼分別位于兩欄中。
datas=html_lxml.xpath('.//div[@class="captcha_images_left"]|.//div[@class="captcha_images_right"]')
這里我使用了Xpath中的路徑選擇,在路徑表達式中使用“|”表示選取若干路徑,例如這里表示的就是選取class為"captcha_images_left"或者"captcha_images_right"的區塊。再來看下運行結果:
由于每爬取一張驗證碼圖片都強制等待了1秒,最后這個運行時間確實讓人絕望,看樣子還是需要多線程來加快速度的,關于多進程多線程我們下次再說,這里我們先來看下爬取到的驗證碼圖片。
圖片到手了,接下來就是調用百度文字識別的OCR來識別這些圖片了,在識別之前,先簡單介紹一下百度OCR的使用方法,因為很多識別驗證碼的教程用的都是tesserocr庫,所以一開始我也嘗試過,安裝過程中就遇到了很多坑,后來還是沒有繼續使用,而是選擇了百度OCR來識別。百度OCR接口提供了自然場景下圖片文字檢測、定位、識別等功能。文字識別的結果可以用于翻譯、搜索、驗證碼等代替用戶輸入的場景。另外還有其他視覺、語音技術方面的識別功能,大家可以直接閱讀文檔了解:百度OCR-API文檔https://ai.baidu.com/docs#/OCR-API/top
使用百度OCR的話,首先注冊用戶,然后下載安裝接口模塊,直接終端輸入pip install baidu-aip即可。然后創建文字識別應用,獲取相關Appid,API Key以及Secret Key,需要了解一下的是百度AI每日提供50000次免費調用通用文字識別接口的使用次數,足夠我們揮霍了。
然后就可以直接調用代碼了。
from aip import AipOcr # 你的 APPID AK SK APP_ID='你的 APP_ID ' API_KEY='你的API_KEY' SECRET_KEY='你的SECRET_KEY' client=AipOcr(APP_ID, API_KEY, SECRET_KEY) # 讀取圖片 def get_file_content(filePath): with open(filePath, 'rb') as fp: return fp.read() image=get_file_content('test.jpg') # 調用通用文字識別, 圖片參數為本地圖片 result=client.basicGeneral(image) # 定義參數變量 options={ # 定義圖像方向 'detect_direction' : 'true', # 識別語言類型,默認為'CHN_ENG'中英文混合 'language_type' : 'CHN_ENG', } # 調用通用文字識別接口 result=client.basicGeneral(image,options) print(result) for word in result['words_result']: print(word['words'])
這里我們識別的是這張圖
可以看一下識別結果
上面是識別后直接輸出的結果,下面是單獨提取出來的文字部分。可以看到,除了破折號沒有輸出外,文字部分都全部正確輸出了。這里我們使用的圖片是jpg格式,文字識別傳入的圖像支持jpg/png/bmp格式,但在技術文檔中有提到,使用jpg格式的圖片上傳會提高一定準確率,這也是我們爬取驗證碼時使用jpg格式保存的原因。
輸出結果中,各字段分別代表:
接下來,我們要做的,就是將我們之前爬取到的驗證碼用剛介紹的OCR來識別,看看究竟能不能得到正確結果。
from aip import AipOcr import os i=0 j=0 APP_ID='你的 APP_ID ' API_KEY='你的API_KEY' SECRET_KEY='你的SECRET_KEY' client=AipOcr(APP_ID, API_KEY, SECRET_KEY) # 讀取圖片 file_path='D:\******\驗證碼圖片' filenames=os.listdir(file_path) # print(filenames) for filename in filenames: # 將路徑與文件名結合起來就是每個文件的完整路徑 info=os.path.join(file_path,filename) with open(info, 'rb') as fp: # 獲取文件夾的路徑 image=fp.read() # 調用通用文字識別, 圖片參數為本地圖片 result=client.basicGeneral(image) # 定義參數變量 options={ 'detect_direction' : 'true', 'language_type' : 'CHN_ENG', } # 調用通用文字識別接口 result=client.basicGeneral(image,options) # print(result) if result['words_result_num']==0: print(filename + ':' + '----') i +=1 else: for word in result['words_result']: print(filename + ' : ' +word['words']) j +=1 print('共識別驗證碼{}張'.format(i+j)) print('未識別出文本{}張'.format(i)) print('已識別出文本{}張'.format(j))
和識別圖片一樣,這里我們將文件夾驗證碼圖片里的圖片全部讀取出來,依次讓OCR識別,并依據“word_result_num”字段判斷是否成功識別出文本,識別出文本則打印結果,未識別出來的用“----”代替,并結合文件名對應識別結果 。最后統計識別結果數量,再來看下識別結果。
看到結果,只能說Amazing!60張圖片居然識別出了65張,并且還有27張為未識別出文本的,這不是我想要的結果~先來簡單看下問題出在哪里,看到“Vertigo Captcha Image.jpg"這張圖名出現了兩次,懷疑是在識別過程中由于被干擾,所以識別成兩行文字輸出了,這樣就很好解釋為什么多出來5張驗證碼圖片了。可是!為什么會有這么多未識別出文本呢,而且英文數字組成的驗證碼識別成中文了,看樣子,不對驗證碼圖片進行去干擾處理,僅靠OCR來識別的想法果然還是行不通啊。那么接下來我們便使用圖像處理的方法來重新識別驗證碼吧。
還是介紹驗證碼時用的這張圖
這張圖也沒能被識別出來,讓人頭禿。接下來就對這張圖片進行一定處理,看能不能讓OCR正確識別
from PIL import Image filepath='D:\******\驗證碼圖片\AncientMosaic Captcha Image.jpg' image=Image.open(filepath) # 傳入'L'將圖片轉化為灰度圖像 image=image.convert('L') # 傳入'1'將圖片進行二值化處理 image=image.convert('1') image.show()
這樣子轉化后再來看下圖片變成什么樣了?
確實有些不同了,趕緊拿去試試能不能識別,還是失敗了~~繼續修改
from PIL import Image filepath='D:\******\驗證碼圖片\AncientMosaic Captcha Image.bmp' image=Image.open(filepath) # 傳入'L'將圖片轉化為灰度圖像 image=image.convert('L') # 傳入'l'將圖片進行二值化處理,默認二值化閾值為127 # 指定閾值進行轉化 count=170 table=[] for i in range(256): if i < count: table.append(0) else: table.append(1 ) image=image.point(table,'1') image.show()
這里我將圖片保存成了bmp模式,然后指定二值化的閾值,不指定的話默認為127,我們需要先轉化原圖為灰度圖像,不能直接在原圖上轉化。然后將構成驗證碼的所需像素添加到一個table中,然后再使用point方法構建新的驗證碼圖片。
現在已經識別到文字了,雖然我不知道為啥識別成了“珍”,分析之后發現是因為z我在設置參數設置了“language_type”為“CHN_ENG”,中英文混合模式,于是我修改成“ENG”英文類型,發現可以識別成字符了,但依然沒有識別成功,嘗試其他我所知道的方法后,我表示很無語,我決定繼續嘗試PIL庫的其他方法試試。
# 找到邊緣 image=image.filter(ImageFilter.FIND_EDGES) # image.show() # 邊緣增強 image=image.filter(ImageFilter.EDGE_ENHANCE) image.show()
還是不能正確識別,我決定換個驗證碼試試。。。。。。
我找了這張帶有陰影的
from PIL import Image,ImageFilter filepath='D:\******\驗證碼圖片\CrossShadow2 Captcha Image.jpg' image=Image.open(filepath) # 傳入'L'將圖片轉化為灰度圖像 image=image.convert('L') # 傳入'l'將圖片進行二值化處理,默認二值化閾值為127 # 指定閾值進行轉化 count=230 table=[] for i in range(256): if i < count: table.append(1) else: table.append(0) image=image.point(table,'1') image.show()
簡單處理后,得到這樣的圖片:
識別結果為:
識別成功了,老淚縱橫!!!看樣子百度OCR還是可以識別出驗證碼的,不過識別率還是有點低,需要對圖像進行一定處理,才能增加識別的準確率。不過百度OCR對規范文本的識別還是很準確的。
那么與其他驗證碼相比,究竟是什么讓這個驗證碼更容易被OCR讀懂呢?
這樣的驗證碼相對識別起來較容易,另外,像識別圖片時的白底黑字就屬于很標準的規范文本了,所以識別的準確度較高。至于更復雜的圖形驗證碼,就需要更深的圖像處理技術或者訓練好的OCR來完成了,如果只是簡單識別一個驗證碼的話,不如人工查看圖片輸入,更多一點的話,也可以交給打碼平臺來識別。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。