整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JavaScript SDK 設計指南

          JavaScript SDK 設計指南

          文地址: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?

          • 嵌入式組件 – 嵌入在出發布者的網頁中的交互式應用程序(Disqus, Google Maps, Facebook Widget)。
          • 分析與數據 – 搜集網站訪問者以及其與網站互動的數據信息。(GA, Flurry, Mixpanel)
          • web服務API封裝 -對于發展與外部Web服務通信的客戶端應用程序。(Facebook的圖形API)

          在什么情況下,我們應該在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你需要了解:

          • JSONP 只能通過GET請求。
          • JSONP 缺少錯誤處理機制, 意味著你不能檢測代碼是否404還是500等狀態。
          • JSONP 請求是異步的。
          • 當心 CSRF 攻擊。
          • 跨域通信。腳本響應端(服務器端)不需要關心CORS。

          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 調試等等。

          這里推薦一些代理工具:

          • FiddlerCore
          • Charles
          • Cellist

          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格式保存的原因。

          輸出結果中,各字段分別代表:

          • log_id : 唯一的log id,用于定位問題
          • direction : 圖像方向,傳入參數時定義為true表示檢測,0表示正向,1表示逆時針90度,2表示逆時針180度,3表示逆時針270度,-1表示未定義。
          • words_result_num : 識別的結果數,即word_result的元素個數
          • word_result : 定義和識別元素數組
          • words : 識別出的字符串
          • 還有一些非必選字段大家可以去文檔里熟悉一下。

          接下來,我們要做的,就是將我們之前爬取到的驗證碼用剛介紹的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 程序產生干擾的噪點。
          • 白色背景色與深色字母之間的對比度很高。

          這樣的驗證碼相對識別起來較容易,另外,像識別圖片時的白底黑字就屬于很標準的規范文本了,所以識別的準確度較高。至于更復雜的圖形驗證碼,就需要更深的圖像處理技術或者訓練好的OCR來完成了,如果只是簡單識別一個驗證碼的話,不如人工查看圖片輸入,更多一點的話,也可以交給打碼平臺來識別。


          主站蜘蛛池模板: 亚洲一区二区三区免费在线观看| 久久久久国产一区二区三区| 伊人激情AV一区二区三区| 国内国外日产一区二区| 国内精品视频一区二区三区| 一区二区免费在线观看| 影音先锋中文无码一区| 亚洲一区无码精品色| 无码乱码av天堂一区二区| 国产精品无码AV一区二区三区| 成人欧美一区二区三区在线视频| 精品久久一区二区三区| 麻豆va一区二区三区久久浪| 亚洲一区二区三区电影| 中文字幕精品一区二区三区视频| 亚洲一区二区三区香蕉| 一区国严二区亚洲三区| 午夜在线视频一区二区三区| 亚洲国产激情在线一区| 日韩电影一区二区三区| 一区二区三区www| 成人乱码一区二区三区av| 一区二区三区视频网站| 一区在线观看视频| 黄桃AV无码免费一区二区三区| 国产精品女同一区二区久久 | 国产一区二区三区高清视频| 国产一区二区三区无码免费 | 精品无码人妻一区二区三区不卡 | 肉色超薄丝袜脚交一区二区| 国产成人一区二区三区在线| 国产精品小黄鸭一区二区三区 | 日本一区二区在线免费观看| 在线日韩麻豆一区| 精品一区二区三区在线视频观看| 日韩av片无码一区二区三区不卡| 波多野结衣在线观看一区二区三区 | 秋霞鲁丝片一区二区三区| 国产一区二区三区无码免费| 亚洲乱码国产一区网址| 日韩一区二区在线观看视频|