如何通過 Serverless 輕鬆識別驗證碼?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者 | 江昱","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源 | ","attrs":{}},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s/ltHLbC_ZlpfCpwgmRxU35g","title":null},"content":[{"type":"text","text":"Serverless 公衆號","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Serverless 概念自被提出就倍受關注,尤其是近些年來 Serverless 煥發出了前所未有的活力,各領域的工程師都在試圖將 Serverless 架構與自身工作相結合,以獲取到 Serverless 架構所帶來的“技術紅利”。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"驗證碼(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自動區分計算機和人類的圖靈測試)的縮寫,是一種區分用戶是計算機還是人的公共全自動程序。可以防止惡意破解密碼、刷票、論壇灌水,有效防止某個黑客對某一個特定註冊用戶用特定程序暴力破解方式進行不斷地登陸嘗試。實際上驗證碼是現在很多網站通行的方式,我們利用比較簡易的方式實現了這個功能。CAPTCHA 的問題由計算機生成並評判,但是這個問題只有人類才能解答,計算機是無法解答的,所以回答出問題的用戶就可以被認爲是人類。說白了,驗證碼就是用來驗證的碼,驗證是人訪問的還是機器訪問的“碼”。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"那麼人工智能領域中的驗證碼識別與 Serverless 架構會碰撞出哪些火花呢?","attrs":{}},{"type":"text","text":"本文將通過 Serverless 架構和卷積神經網絡(CNN)算法,實現驗證碼識別功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"淺談驗證碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"驗證碼的發展,可以說是非常迅速的,從開始的單純數字驗證碼,到後來的數字+字母驗證碼,再到後來的數字+字母+中文的驗證碼以及圖形圖像驗證碼,單純的驗證碼素材已經越來越多了。從驗證碼的形態來看,也是各不相同,輸入、點擊、拖拽以及短信驗證碼、語音驗證碼……","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Bilibili 的登錄驗證碼就包括了多種模式,例如滑動滑塊進行驗證:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/00/008ad47258ad8e02ade6f3eec0f498a4.png","alt":"1.png","title":"1.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,通過依次點擊文字進行驗證:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/11/11ab29f676705976b8af2efe7465d94f.png","alt":"2.png","title":"2.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而百度貼吧、知乎、以及 Google 等相關網站的驗證碼又各不相同,例如選擇正着寫的文字、選擇包括指定物體的圖片以及按順序點擊圖片中的字符等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"驗證碼的識別可能會根據驗證碼的類型而不太一致,當然最簡單的驗證碼可能就是最原始的文字驗證碼了:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c7/c7d58bea0b23af48b81003e7007e1703.png","alt":"3.png","title":"3.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即便是文字驗證碼,也是存在很多差異的,例如簡單的數字驗證碼、簡單的數字+字母驗證碼、文字驗證碼、驗證碼中包括計算、簡單驗證碼中增加一些干擾成爲複雜驗證碼等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"驗證碼識別","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 簡單驗證碼識別","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"驗證碼識別是一個古老的研究領域,簡單說就是把圖片上的文字轉化爲文本的過程。最近幾年,隨着大數據的發展,廣大爬蟲工程師在對抗反爬策略時,對驗證碼的識別要求也越來越高。在簡單驗證碼的時代,驗證碼的識別主要是針對文本驗證碼,通過圖像的切割,對驗證碼每一部分進行裁剪,然後再對每個裁剪單元進行相似度對比,獲得最可能的結果,最後進行拼接,例如將驗證碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d5/d53d8a439ee0ef89902f93a9285ffb25.png","alt":"4.png","title":"4.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進行二值化等操作:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/48/481f4048b672b7f984fe45ab5cd996b9.png","alt":"5.png","title":"5.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成之後再進行切割:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/82/825cb2e5558e94b4e608f9c3889fc245.png","alt":"6.png","title":"6.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"切割完成再進行識別,最後進行拼接,這樣的做法是,針對每個字符進行識別,相對來說是比較容易的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是隨着時間的發展,在這種簡單驗證碼逐漸無法滿足判斷“是人還是機器”的問題時,驗證碼進行了一次小升級,即驗證碼上面增加了一些干擾線,或者驗證碼進行了嚴重的扭曲,增加了強色塊干擾,例如 Dynadot 網站的驗證碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/10/10d472a578f933fcc1f3d51fc5d46b63.png","alt":"7.png","title":"7.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不僅有圖像扭曲重疊,還有干擾線和色塊干擾。這個時候想要識別驗證碼,簡單的切割識別就很難獲得良好的效果了,這時通過深度學習反而可以獲得不錯的效果。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. 基於 CNN 的驗證碼識別","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"卷積神經網絡(Convolutional Neural Network,簡稱 CNN),是一種前饋神經網絡,人工神經元可以響應周圍單元,進行大型圖像處理。卷積神經網絡包括卷積層和池化層。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f4/f4a39c5398160e5044b14fd5879dcbde.png","alt":"8.png","title":"8.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖所示,左圖是傳統的神經網絡,其基本結構是:輸入層、隱含層、輸出層。右圖則是卷積神經網絡,其結構由輸入層、輸出層、卷積層、池化層、全連接層構成。卷積神經網絡其實是神經網絡的一種拓展,而事實上從結構上來說,樸素的 CNN 和樸素的 NN 沒有任何區別(當然,引入了特殊結構的、複雜的 CNN 會和 NN 有着比較大的區別)。相對於傳統神經網絡,CNN 在實際效果中讓我們的網絡參數數量大大地減少,這樣我們可以用較少的參數,訓練出更加好的模型,典型的事半功倍,而且可以有效地避免過擬合。同樣,由於 filter 的參數共享,即使圖片進行了一定的平移操作,我們照樣可以識別出特徵,這叫做 “平移不變性”。因此,模型就更加穩健了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1)驗證碼生成","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"驗證碼的生成是非常重要的一個步驟,因爲這一部分的驗證碼將會作爲我們的訓練集和測試集,同時最終我們的模型可以識別什麼類型的驗證碼,也是和這部分有關。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"# coding:utf-8\nimport random\nimport numpy as np\nfrom PIL import Image\nfrom captcha.image import ImageCaptcha\nCAPTCHA_LIST = [eve for eve in \"0123456789abcdefghijklmnopqrsruvwxyzABCDEFGHIJKLMOPQRSTUVWXYZ\"]\nCAPTCHA_LEN = 4 # 驗證碼長度\nCAPTCHA_HEIGHT = 60 # 驗證碼高度\nCAPTCHA_WIDTH = 160 # 驗證碼寬度\nrandomCaptchaText = lambda char=CAPTCHA_LIST, size=CAPTCHA_LEN: \"\".join([random.choice(char) for _ in range(size)])\ndef genCaptchaTextImage(width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT, save=None):\n image = ImageCaptcha(width=width, height=height)\n captchaText = randomCaptchaText()\n if save:\n image.write(captchaText, './img/%s.jpg' % captchaText)\n return captchaText, np.array(Image.open(image.generate(captchaText)))\nprint(genCaptchaTextImage(save=True))","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過上述代碼,可以生成簡單的中英文驗證碼:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/35/35800716e266902e4fe68ab558d981fb.png","alt":"image.gif","title":"image.gif","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6f/6f0383671ad8e157f571e17a60bdd953.png","alt":"9.png","title":"9.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2)模型訓練","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模型訓練的代碼如下(部分代碼來自網絡)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"util.py 文件,主要是一些提取出來的公有方法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"# -*- coding:utf-8 -*-\nimport numpy as np\nfrom captcha_gen import genCaptchaTextImage\nfrom captcha_gen import CAPTCHA_LIST, CAPTCHA_LEN, CAPTCHA_HEIGHT, CAPTCHA_WIDTH\n# 圖片轉爲黑白,3維轉1維\nconvert2Gray = lambda img: np.mean(img, -1) if len(img.shape) > 2 else img\n# 驗證碼向量轉爲文本\nvec2Text = lambda vec, captcha_list=CAPTCHA_LIST: ''.join([captcha_list[int(v)] for v in vec])\ndef text2Vec(text, captchaLen=CAPTCHA_LEN, captchaList=CAPTCHA_LIST):\n \"\"\"\n 驗證碼文本轉爲向量\n \"\"\"\n vector = np.zeros(captchaLen * len(captchaList))\n for i in range(len(text)):\n vector[captchaList.index(text[i]) + i * len(captchaList)] = 1\n return vector\ndef getNextBatch(batchCount=60, width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT):\n \"\"\"\n 獲取訓練圖片組\n \"\"\"\n batchX = np.zeros([batchCount, width * height])\n batchY = np.zeros([batchCount, CAPTCHA_LEN * len(CAPTCHA_LIST)])\n for i in range(batchCount):\n text, image = genCaptchaTextImage()\n image = convert2Gray(image)\n # 將圖片數組一維化 同時將文本也對應在兩個二維組的同一行\n batchX[i, :] = image.flatten() / 255\n batchY[i, :] = text2Vec(text)\n return batchX, batchY\n# print(getNextBatch(batch_count=1))","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"model_train.py 文件,主要是進行模型訓練。在該文件中,定義了模型的基本信息,例如該模型是三層卷積神經網絡,原始圖像大小是 60","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"160,在第一次卷積後變爲 60","attrs":{}},{"type":"text","text":"160, 第一池化後變爲 30","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"80;第二次卷積後變爲 30","attrs":{}},{"type":"text","text":"80 ,第二次池化後變爲 15","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"40;第三次卷積後變爲  15","attrs":{}},{"type":"text","text":"40 ,第三次池化後變爲7","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"20。經過三次卷積和池化後,原始圖片數據變爲 7","attrs":{}},{"type":"text","text":"20 的平面數據,同時項目在進行訓練的時候,每隔 100 次進行一次數據測試,計算一次準確度:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"# -*- coding:utf-8 -*-\nimport tensorflow.compat.v1 as tf\nfrom datetime import datetime\nfrom util import getNextBatch\nfrom captcha_gen import CAPTCHA_HEIGHT, CAPTCHA_WIDTH, CAPTCHA_LEN, CAPTCHA_LIST\ntf.compat.v1.disable_eager_execution()\nvariable = lambda shape, alpha=0.01: tf.Variable(alpha * tf.random_normal(shape))\nconv2d = lambda x, w: tf.nn.conv2d(x, w, strides=[1, 1, 1, 1], padding='SAME')\nmaxPool2x2 = lambda x: tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')\noptimizeGraph = lambda y, y_conv: tf.train.AdamOptimizer(1e-3).minimize(\n tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=y_conv)))\nhDrop = lambda image, weight, bias, keepProb: tf.nn.dropout(\n maxPool2x2(tf.nn.relu(conv2d(image, variable(weight, 0.01)) + variable(bias, 0.1))), keepProb)\ndef cnnGraph(x, keepProb, size, captchaList=CAPTCHA_LIST, captchaLen=CAPTCHA_LEN):\n \"\"\"\n 三層卷積神經網絡\n \"\"\"\n imageHeight, imageWidth = size\n xImage = tf.reshape(x, shape=[-1, imageHeight, imageWidth, 1])\n hDrop1 = hDrop(xImage, [3, 3, 1, 32], [32], keepProb)\n hDrop2 = hDrop(hDrop1, [3, 3, 32, 64], [64], keepProb)\n hDrop3 = hDrop(hDrop2, [3, 3, 64, 64], [64], keepProb)\n # 全連接層\n imageHeight = int(hDrop3.shape[1])\n imageWidth = int(hDrop3.shape[2])\n wFc = variable([imageHeight * imageWidth * 64, 1024], 0.01) # 上一層有64個神經元 全連接層有1024個神經元\n bFc = variable([1024], 0.1)\n hDrop3Re = tf.reshape(hDrop3, [-1, imageHeight * imageWidth * 64])\n hFc = tf.nn.relu(tf.matmul(hDrop3Re, wFc) + bFc)\n hDropFc = tf.nn.dropout(hFc, keepProb)\n # 輸出層\n wOut = variable([1024, len(captchaList) * captchaLen], 0.01)\n bOut = variable([len(captchaList) * captchaLen], 0.1)\n yConv = tf.matmul(hDropFc, wOut) + bOut\n return yConv\ndef accuracyGraph(y, yConv, width=len(CAPTCHA_LIST), height=CAPTCHA_LEN):\n \"\"\"\n 偏差計算圖,正確值和預測值,計算準確度\n \"\"\"\n maxPredictIdx = tf.argmax(tf.reshape(yConv, [-1, height, width]), 2)\n maxLabelIdx = tf.argmax(tf.reshape(y, [-1, height, width]), 2)\n correct = tf.equal(maxPredictIdx, maxLabelIdx) # 判斷是否相等\n return tf.reduce_mean(tf.cast(correct, tf.float32))\ndef train(height=CAPTCHA_HEIGHT, width=CAPTCHA_WIDTH, ySize=len(CAPTCHA_LIST) * CAPTCHA_LEN):\n \"\"\"\n cnn訓練\n \"\"\"\n accRate = 0.95\n x = tf.placeholder(tf.float32, [None, height * width])\n y = tf.placeholder(tf.float32, [None, ySize])\n keepProb = tf.placeholder(tf.float32)\n yConv = cnnGraph(x, keepProb, (height, width))\n optimizer = optimizeGraph(y, yConv)\n accuracy = accuracyGraph(y, yConv)\n saver = tf.train.Saver()\n with tf.Session() as sess:\n sess.run(tf.global_variables_initializer()) # 初始化\n step = 0 # 步數\n while True:\n batchX, batchY = getNextBatch(64)\n sess.run(optimizer, feed_dict={x: batchX, y: batchY, keepProb: 0.75})\n # 每訓練一百次測試一次\n if step % 100 == 0:\n batchXTest, batchYTest = getNextBatch(100)\n acc = sess.run(accuracy, feed_dict={x: batchXTest, y: batchYTest, keepProb: 1.0})\n print(datetime.now().strftime('%c'), ' step:', step, ' accuracy:', acc)\n # 準確率滿足要求,保存模型\n if acc > accRate:\n modelPath = \"./model/captcha.model\"\n saver.save(sess, modelPath, global_step=step)\n accRate += 0.01\n if accRate > 0.90:\n break\n step = step + 1\ntrain()","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當完成了這部分之後,我們可以通過本地機器對模型進行訓練,爲了提升訓練速度,我將代碼中的 accRate 部分設置爲:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"if accRate > 0.90:\n break","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是說,當準確率超過 90% 之後,系統就會自動停止,並且保存模型。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來可以進行訓練:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/87/8736cffe182bdc5f0b685eed181a21a7.png","alt":"10.png","title":"10.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"訓練時間可能會比較長,訓練完成之後,可以根據結果繪圖,查看隨着 Step 的增加,準確率的變化曲線:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b2/b2e055909061df29927f000154263bd8.png","alt":"11.png","title":"11.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"橫軸表示訓練的 Step,縱軸表示準確率","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. 基於 Serverless 架構的驗證碼識別","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將上面的代碼部分進行進一步整合,按照函數計算的規範進行編碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"# -*- coding:utf-8 -*-\n# 核心後端服務\nimport base64\nimport json\nimport uuid\nimport tensorflow as tf\nimport random\nimport numpy as np\nfrom PIL import Image\nfrom captcha.image import ImageCaptcha\n# Response\nclass Response:\n def __init__(self, start_response, response, errorCode=None):\n self.start = start_response\n responseBody = {\n 'Error': {\"Code\": errorCode, \"Message\": response},\n } if errorCode else {\n 'Response': response\n }\n # 默認增加uuid,便於後期定位\n responseBody['ResponseId'] = str(uuid.uuid1())\n print(\"Response: \", json.dumps(responseBody))\n self.response = json.dumps(responseBody)\n def __iter__(self):\n status = '200'\n response_headers = [('Content-type', 'application/json; charset=UTF-8')]\n self.start(status, response_headers)\n yield self.response.encode(\"utf-8\")\nCAPTCHA_LIST = [eve for eve in \"0123456789abcdefghijklmnopqrsruvwxyzABCDEFGHIJKLMOPQRSTUVWXYZ\"]\nCAPTCHA_LEN = 4 # 驗證碼長度\nCAPTCHA_HEIGHT = 60 # 驗證碼高度\nCAPTCHA_WIDTH = 160 # 驗證碼寬度\n# 隨機字符串\nrandomStr = lambda num=5: \"\".join(random.sample('abcdefghijklmnopqrstuvwxyz', num))\nrandomCaptchaText = lambda char=CAPTCHA_LIST, size=CAPTCHA_LEN: \"\".join([random.choice(char) for _ in range(size)])\n# 圖片轉爲黑白,3維轉1維\nconvert2Gray = lambda img: np.mean(img, -1) if len(img.shape) > 2 else img\n# 驗證碼向量轉爲文本\nvec2Text = lambda vec, captcha_list=CAPTCHA_LIST: ''.join([captcha_list[int(v)] for v in vec])\nvariable = lambda shape, alpha=0.01: tf.Variable(alpha * tf.random_normal(shape))\nconv2d = lambda x, w: tf.nn.conv2d(x, w, strides=[1, 1, 1, 1], padding='SAME')\nmaxPool2x2 = lambda x: tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')\noptimizeGraph = lambda y, y_conv: tf.train.AdamOptimizer(1e-3).minimize(\n tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=y_conv)))\nhDrop = lambda image, weight, bias, keepProb: tf.nn.dropout(\n maxPool2x2(tf.nn.relu(conv2d(image, variable(weight, 0.01)) + variable(bias, 0.1))), keepProb)\ndef genCaptchaTextImage(width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT, save=None):\n image = ImageCaptcha(width=width, height=height)\n captchaText = randomCaptchaText()\n if save:\n image.write(captchaText, save)\n return captchaText, np.array(Image.open(image.generate(captchaText)))\ndef text2Vec(text, captcha_len=CAPTCHA_LEN, captcha_list=CAPTCHA_LIST):\n \"\"\"\n 驗證碼文本轉爲向量\n \"\"\"\n vector = np.zeros(captcha_len * len(captcha_list))\n for i in range(len(text)):\n vector[captcha_list.index(text[i]) + i * len(captcha_list)] = 1\n return vector\ndef getNextBatch(batch_count=60, width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT):\n \"\"\"\n 獲取訓練圖片組\n \"\"\"\n batch_x = np.zeros([batch_count, width * height])\n batch_y = np.zeros([batch_count, CAPTCHA_LEN * len(CAPTCHA_LIST)])\n for i in range(batch_count):\n text, image = genCaptchaTextImage()\n image = convert2Gray(image)\n # 將圖片數組一維化 同時將文本也對應在兩個二維組的同一行\n batch_x[i, :] = image.flatten() / 255\n batch_y[i, :] = text2Vec(text)\n return batch_x, batch_y\ndef cnnGraph(x, keepProb, size, captchaList=CAPTCHA_LIST, captchaLen=CAPTCHA_LEN):\n \"\"\"\n 三層卷積神經網絡\n \"\"\"\n imageHeight, imageWidth = size\n xImage = tf.reshape(x, shape=[-1, imageHeight, imageWidth, 1])\n hDrop1 = hDrop(xImage, [3, 3, 1, 32], [32], keepProb)\n hDrop2 = hDrop(hDrop1, [3, 3, 32, 64], [64], keepProb)\n hDrop3 = hDrop(hDrop2, [3, 3, 64, 64], [64], keepProb)\n # 全連接層\n imageHeight = int(hDrop3.shape[1])\n imageWidth = int(hDrop3.shape[2])\n wFc = variable([imageHeight * imageWidth * 64, 1024], 0.01) # 上一層有64個神經元 全連接層有1024個神經元\n bFc = variable([1024], 0.1)\n hDrop3Re = tf.reshape(hDrop3, [-1, imageHeight * imageWidth * 64])\n hFc = tf.nn.relu(tf.matmul(hDrop3Re, wFc) + bFc)\n hDropFc = tf.nn.dropout(hFc, keepProb)\n # 輸出層\n wOut = variable([1024, len(captchaList) * captchaLen], 0.01)\n bOut = variable([len(captchaList) * captchaLen], 0.1)\n yConv = tf.matmul(hDropFc, wOut) + bOut\n return yConv\ndef captcha2Text(image_list):\n \"\"\"\n 驗證碼圖片轉化爲文本\n \"\"\"\n with tf.Session() as sess:\n saver.restore(sess, tf.train.latest_checkpoint('model/'))\n predict = tf.argmax(tf.reshape(yConv, [-1, CAPTCHA_LEN, len(CAPTCHA_LIST)]), 2)\n vector_list = sess.run(predict, feed_dict={x: image_list, keepProb: 1})\n vector_list = vector_list.tolist()\n text_list = [vec2Text(vector) for vector in vector_list]\n return text_list\nx = tf.placeholder(tf.float32, [None, CAPTCHA_HEIGHT * CAPTCHA_WIDTH])\nkeepProb = tf.placeholder(tf.float32)\nyConv = cnnGraph(x, keepProb, (CAPTCHA_HEIGHT, CAPTCHA_WIDTH))\nsaver = tf.train.Saver()\ndef handler(environ, start_response):\n try:\n request_body_size = int(environ.get('CONTENT_LENGTH', 0))\n except (ValueError):\n request_body_size = 0\n requestBody = json.loads(environ['wsgi.input'].read(request_body_size).decode(\"utf-8\"))\n imageName = randomStr(10)\n imagePath = \"/tmp/\" + imageName\n print(\"requestBody: \", requestBody)\n reqType = requestBody.get(\"type\", None)\n if reqType == \"get_captcha\":\n genCaptchaTextImage(save=imagePath)\n with open(imagePath, 'rb') as f:\n data = base64.b64encode(f.read()).decode()\n return Response(start_response, {'image': data})\n if reqType == \"get_text\":\n # 圖片獲取\n print(\"Get pucture\")\n imageData = base64.b64decode(requestBody[\"image\"])\n with open(imagePath, 'wb') as f:\n f.write(imageData)\n # 開始預測\n img = Image.open(imageName)\n img = img.resize((160, 60), Image.ANTIALIAS)\n img = img.convert(\"RGB\")\n img = np.asarray(img)\n image = convert2Gray(img)\n image = image.flatten() / 255\n return Response(start_response, {'result': captcha2Text([image])})","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個函數部分,主要包括兩個接口:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• 獲取驗證碼:用戶測試使用,生成驗證碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• 獲取驗證碼識別結果:用戶識別使用,識別驗證碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這部分代碼,所需要的依賴內容如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tensorflow==1.13.1\nnumpy==1.19.4\nscipy==1.5.4\npillow==8.0.1\ncaptcha==0.3","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外,爲了更加簡單的來體驗,提供測試頁面,測試頁面的後臺服務使用 Python Web Bottle 框架:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"# -*- coding:utf-8 -*-\nimport os\nimport json\nfrom bottle import route, run, static_file, request\nimport urllib.request\nurl = \"http://\" + os.environ.get(\"url\")\n@route('/')\ndef index():\n return static_file(\"index.html\", root='html/')\n@route('/get_captcha')\ndef getCaptcha():\n data = json.dumps({\"type\": \"get_captcha\"}).encode(\"utf-8\")\n reqAttr = urllib.request.Request(data=data, url=url)\n return urllib.request.urlopen(reqAttr).read().decode(\"utf-8\")\n@route('/get_captcha_result', method='POST')\ndef getCaptcha():\n data = json.dumps({\"type\": \"get_text\", \"image\": json.loads(request.body.read().decode(\"utf-8\"))[\"image\"]}).encode(\n \"utf-8\")\n reqAttr = urllib.request.Request(data=data, url=url)\n return urllib.request.urlopen(reqAttr).read().decode(\"utf-8\")\nrun(host='0.0.0.0', debug=False, port=9000)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該後端服務,所需依賴:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"bottle==0.12.19","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前端頁面代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\n\n\n \n 驗證碼識別測試系統\n \n \n\n\n
\n
\n
\n
\n

\n 驗證碼識別測試系統\n

\n
\n
\n
\n
\n
\n
\n
\n
\n \n

\n

\n
\n
\n 操作:\n \n \n
\n
\n
\n
\n
\n
\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"準備好代碼之後,開始編寫部署文件:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"Global:\n Service:\n Name: ServerlessBook\n Description: Serverless圖書案例\n Log: Auto\n Nas: Auto\nServerlessBookCaptchaDemo:\n Component: fc\n Provider: alibaba\n Access: release\n Extends:\n deploy:\n - Hook: s install docker\n Path: ./\n Pre: true\n Properties:\n Region: cn-beijing\n Service: ${Global.Service}\n Function:\n Name: serverless_captcha\n Description: 驗證碼識別\n CodeUri:\n Src: ./src/backend\n Excludes:\n - src/backend/.fun\n - src/backend/model\n Handler: index.handler\n Environment:\n - Key: PYTHONUSERBASE\n Value: /mnt/auto/.fun/python\n MemorySize: 3072\n Runtime: python3\n Timeout: 60\n Triggers:\n - Name: ImageAI\n Type: HTTP\n Parameters:\n AuthType: ANONYMOUS\n Methods:\n - GET\n - POST\n - PUT\n Domains:\n - Domain: Auto\nServerlessBookCaptchaWebsiteDemo:\n Component: bottle\n Provider: alibaba\n Access: release\n Extends:\n deploy:\n - Hook: pip3 install -r requirements.txt -t ./\n Path: ./src/website\n Pre: true\n Properties:\n Region: cn-beijing\n CodeUri: ./src/website\n App: index.py\n Environment:\n - Key: url\n Value: ${ServerlessBookCaptchaDemo.Output.Triggers[0].Domains[0]}\n Detail:\n Service: ${Global.Service}\n Function:\n Name: serverless_captcha_website","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整體的目錄結構:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"| - src # 項目目錄\n | | - backend # 項目後端,核心接口\n | | - index.py # 後端核心代碼\n | | - requirements.txt # 後端核心代碼依賴\n | | - website # 項目前端,便於測試使用\n | | - html # 項目前端頁面\n | | - index.html # 項目前端頁面\n | | - index.py # 項目前端的後臺服務(bottle框架)\n | | - requirements.txt # 項目前端的後臺服務依賴","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成之後,我們可以在項目目錄下,進行項目的部署:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"s deploy","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"部署完成之後,打開返回的頁面地址:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/02/025abce8ea85ac1853e332042f7b329a.png","alt":"12.png","title":"12.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"點擊獲取驗證碼,即可在線生成一個驗證碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f5/f52164c7afdd2a5ce4bb501cfa1529eb.png","alt":"13.png","title":"13.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時點擊識別驗證碼,即可進行驗證碼識別:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/96/9663dc678ec14ae0ffc48b0d451def34.png","alt":"14.png","title":"14.png","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於模型在訓練的時候,填寫的目標準確率是 90%,所以可以認爲在海量同類型驗證碼測試之後,整體的準確率在 90% 左右。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Serverless 發展迅速,通過 Serverless 做一個驗證碼識別工具,我覺得這是一個非常酷的事情。在未來的數據採集等工作中,有一個優美的驗證碼識別工具是非常必要的。當然驗證碼種類很多,針對不同類型的驗證碼識別,也是一項非常有挑戰性的工作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章