【日常】爬虫学习进阶:百度翻译的秘密(2021版)
序言
許久不更,省身自愧。假期里事情沒做成幾件,跑些步也把膝蓋搞得殘廢,年關將至,且以陋文一篇辭舊迎新。
近期想到可以積累一些雙語語料以備后用,于是去嘗試去一些在線翻譯尋求資源,總結下來還是百度翻譯的查詢結果相對完全(相對于Google翻譯和有道翻譯),除了能提供相當數量的雙語例句外,還有同義詞辨析以及來自WordNet的完整詞義列表。
-
以查詢單詞take為例:👇
-
英英釋義:一共42種不同的釋義結果,可用于語義消歧任務,其數據來源于WordNet,也是語義消歧任務的常用外部知識源。👇
-
雙語例句:最直接的想法可以作為機器翻譯任務的數據源,注意到這里的雙語例句中的take是帶有詞義標注的,所以用途可能會更為廣泛。👇
-
其他幾個欄目下的數據筆者簡單概括,不再截圖贅述:
-
詞語用例即一些常用搭配,如take after,take in等,雖然take有非常多的常用搭配,而且有些搭配還有很多的不同釋義,筆者認為這些常用搭配的短語在英文語句分詞時應當作為整體考慮,因為拆分下來可能并不能找到適合的take語義與其匹配,并且短語后的介詞可能也不是其本身的含義。
- 以短語搭配take in為例,常用釋義為收留,其他還有 吸收,理解,改小 等含義,顯然拆分為take與in后并不能體現這些釋義,因此take in就應當視為一個單詞考慮。
- 好在并不是所有單詞都有如此多的常見搭配,可能通過枚舉解決此類問題,通過更加合理的預訓練,這樣可能會使得模型在一些下游任務的表現得到提升。
-
同反義詞以及同義詞辨析是百度翻譯與其他幾個在線翻譯最大的突出點,有道翻譯沒有這一項數據,Google翻譯則過于簡略缺少例句參考,百度翻譯在這一項中除了有同義詞的例舉釋義外,也給出了雙語例句作為參考。假設某種任務是讓機器辨析某個英文句子中的單詞(如take)是否可以用其他類似單詞替代(如常見的同義詞grasp,capture,hold等),這就可以作為一個可能的數據增強來源。
-
-
既然有如此多具有潛在利用價值的數據可供挖掘,那么如何獲取就是關鍵問題了,當然百度作為巨頭自然會對公開數據進行一些加密,筆者通過半天的摸索,基本弄得非常明白,以為爬取思路很有趣味,非常值得借鑒與參考,不辭繁瑣且與眾友分享一二。
目錄
- 序言
- 思路解析
- 1 樸素的頁面源代碼爬取
- 2 試試抓取數據包
- 3 如何生成字段sign與token?
- 3.1 生成token的邏輯
- 3.2 生成sign的邏輯
- 源碼
思路解析
1 樸素的頁面源代碼爬取
讓我們回頭再來看一下查詢take單詞的頁面:👇
可以看到網址上清楚得記錄了#en/zh/take,顯然這表示我們在從英文(en)翻譯到中文(zh),需要翻譯的文本是take,且所有需要爬取的數據都在這個頁面上(以雙語例句為例可以看到包含在標簽sample-source中):👇
似乎問題非常簡單,直接獲取該頁面上的頁面源代碼即可解決:👇
# -*- coding: UTF-8 -*- # @author: caoyang # @email: caoyang@163.sufe.edu.cnimport requestsword = 'take' url = 'https://fanyi.baidu.com/#en/zh/' + word headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0'} r = requests.get(url, headers=headers) html = r.textwith open('baidufanyi_{}.html'.format(word), 'w', encoding='utf8') as f:f.write(html)我們將得到的頁面源代碼寫出到外部文件,查找sample-source標簽后失望的發現這里面寫得都是一些模板語言,缺乏數據填充,并沒有需要的東西:👇
此路不通,須當另辟蹊徑,也許之后還會回到這段樸素的頁面源代碼上呢?所謂返璞歸真,約莫如此罷。
2 試試抓取數據包
既然頁面源代碼上顯示為需要數據填充的模板語言,那么前端必然是向后端發起了數據請求,通過抓包應當可以獲得需要的數據。👇
不出所料在XHR監聽中我們看到了v2transapi?from=en&to=zh這個數據包,通過上圖對應照勉強可以看出右邊紅框中的json數據就是左邊的例句 I’ll take any you don’t want.。
查看消息頭可以發現這是一個POST請求(左圖),表單數據(右圖)也非常簡單:👇
- 注:Cookie沒有打碼大家也別深究了,截屏中沒有登錄百度賬號,所以Cookie里面沒有什么有用的信息,事實上百度翻譯爬取中Cookie是必要的,后文中將會在頁面Javascript中看到這一點,為了便于后續代碼運行,本文使用明文Cookie。
問題似乎又解決了,讓我們來試試是否可行:👇
# -*- coding: UTF-8 -*- # @author: caoyang # @email: caoyang@163.sufe.edu.cnimport json import requestsword = 'take' url = 'https://fanyi.baidu.com/v2transapi' formdata = {'from' : 'en','to' : 'zh','query' : word,'simple_means_flag' : '3','sign' : '183948.404925','token' : '36fffe666423ac015ff58d7f3a9bc433','domain' : 'common', } headers = {'Host': 'fanyi.baidu.com','User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0','Accept': '*/*','Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2','Accept-Encoding': 'gzip, deflate, br','Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8','X-Requested-With': 'XMLHttpRequest','Content-Length': '116','Origin': 'https://fanyi.baidu.com','Connection': 'keep-alive','Referer': 'https://fanyi.baidu.com/','Cookie': 'BAIDUID=DBBD7E00FF1E064D7FC01E585DC97E13:FG=1; BIDUPSID=DBBD7E00FF1E064D7FC01E585DC97E13; PSTM=1612755445; BDRCVFR[gltLrB7qNCt]=mk3SLVN4HKm; delPer=0; PSINO=5; H_PS_PSSID=33425_33442_33260_33272_33571_33585_33318_33268; BA_HECTOR=0184al2la1ak8421rh1g21cfm0r; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; BCLID=10815360212121519601; BDSFRCVID=z2kOJexroG3VnU3eKBZghcyL2LweG7bTDYLEOwXPsp3LGJLVJeC6EG0Pts1-dEu-EHtdogKK0gOTH6KF_2uxOjjg8UtVJeC6EG0Ptf8g0M5; H_BDCLCKID_SF=tR3aQ5rtKRTffjrnhPF326DfXP6-hnjy3b7pWfKb5lvIoR3d-nrdDxAWbttf5q3RymJ42-39LPO2hpRjyxv4y4Ldj4oxJpOJ-bCL0p5aHl51fbbvbURvD--g3-AqBM5dtjTO2bc_5KnlfMQ_bf--QfbQ0hOhqP-jBRIE3-oJqCLaMItR3f; __yjs_duid=1_386a6866632fadafb73dffc74e18bbf91612755447272; Hm_lvt_64ecd82404c51e03dc91cb9e8c025574=1612755447; Hm_lpvt_64ecd82404c51e03dc91cb9e8c025574=1612755466; ab_sr=1.0.0_M2E2ZWQ0NDdkMTQyMTFiYTRjY2Y1NDIxOGNhZmVmZmEyMjE3YTY0MmE0OTdiNWQ4NjQxMDQzNDYxMzVjNDA2NzY5MWU4NTRiMjY1MDdlYWUzYjk4YjNmZDRhYmI4MGQw; __yjsv5_shitong=1.0_7_9ec8cc04516309efce46e669dcc80c158b7f_300_1612755466482_114.230.179.127_9723e5b1; REALTIME_TRANS_SWITCH=1; FANYI_WORD_SWITCH=1; HISTORY_SWITCH=1; SOUND_SPD_SWITCH=1; SOUND_PREFER_SWITCH=1', } r = requests.post(url, data=formdata, headers=headers) with open('transapi_{}.json'.format(word), 'w', encoding='utf8') as f:json.dump(r.json(), f)這里同樣將POST請求得到的json數據導出到外部文件中,可以看到非常完整的頁面數據,雖然看起來很亂,但是筆者可以肯定的說頁面上所有有用的信息,包括雙語例句,同義詞辨析等等條目都包含在這段json中了,至于如何解析出可用的數據,那就是后話了。👇
大功告成!讓我們用上面的代碼再試試其他單詞的查詢結果吧,將上述代碼第8行的word = 'take'修正為word = 'get'試試:👇
顯然問題沒有那么簡單,替換成新的單詞就無法適用這個方法,回頭我們再來看看這張表單數據,里面有兩個字段非常令人在意:👇
這個sign和token我們并不知道它們是如何生成的,但是我們可以推斷這是用于認證而加密得到的字符串,因此必須弄明白這兩個字段從何而來,才能徹底解決百度翻譯結果的爬取問題。
3 如何生成字段sign與token?
我們繼續做抓包工作,不過這次我們試試監聽JS中的數據:👇
很不幸地,我們又看到一大堆的JS文件,而且從數據包的大小來看還都是些又臭又長的JS代碼,如何從這些定位到我們需要的JS文件,再從文件中定位到字段sign與token的生成邏輯?這里筆者分享自己的定位思路:
- ① 首先先看這里21個JS文件的文件名一列,像NotePanel.js,LangPanel.js一看就是頁面風格設計的JS文件,ai_captcha.js是一個智能驗證(顯然這里的sign與token并不是很智能),直接可以排除;
- ② 接下來看JS文件的文件大小一列,一般來說,那種幾kb都不到的JS文件里都是一些功能性的小工具,或是存放一些小規模的靜態數據,基本可以忽略;
- ③ 然后看JS文件的加載時間一列,注意到這個頁面上的數據當時就完全加載了,如果JS文件都加載得這么慢,那么利用這段JS還要接著去請求后端數據豈不是會慢得離譜,因此可以只考察那些0毫秒的JS文件。
- ④ 最后,上面三個策略只是縮小篩選范圍,最重要的是這一點,即便不用上面三點的方法也能迅速確定到上圖紅框中的JS文件:試想,那個包含了向后端請求數據的JS文件中一定會有什么?必然會包含表單數據!表單數據中有什么?表單數據中有'sign'和'token'這兩個字符串!所以只要下載(或直接復制)每個JS文件的代碼,然后全文搜索sign或token,即可確定哪個JS文件中有我們想看的邏輯。
至此,可以定位到這個index_f4d8a7d.js文件(文件名的后綴f4d8a7d可能隨時間推移會變化),注意到走遍紅框中有sign和token兩個字段:👇
我們將紅框中的這段代碼復制到下面的框中優化格式查看:👇
w = {from : p.fromLang,to : p.toLang,query : n,transtype : r, simple_means_flag : 3, sign : f(n),token : window.common.token,domain : y.getCurDomain() }這與上文中的表單數據基本吻合,可能多了一個transtype字段,不過這并不重要,因為我們只關心sign和token兩個字段的生成邏輯,接下來我們分別就兩個字段的生成邏輯代碼進行定位。
3.1 生成token的邏輯
首先從較為簡單的token開始,它的值為window.common.token,顯然不是所有的變量都叫window,所謂window就是頁面的全局變量,通常可以在頁面源代碼中找到它的定義,即使不能,它的位置也一般在像這種以index為前綴的JS文件中(如果不是當我沒說,最壞的結果應該是藏在之前JS抓包的其他JS文件中了)。
此時我們回到1 樸素的頁面源代碼爬取章節中的頁面源代碼里,用相同的代碼拿到頁面源代碼再看看:👇(完全相同,當然其實這里已經可以直接請求https://fanyi.baidu.com/即可,無需帶上后面的“查詢字符串”了。)
# -*- coding: UTF-8 -*- # @author: caoyang # @email: caoyang@163.sufe.edu.cnimport requestsword = 'take' url = 'https://fanyi.baidu.com/#en/zh/' + word headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0'} r = requests.get(url, headers=headers) html = r.textwith open('baidufanyi_{}.html'.format(word), 'w', encoding='utf8') as f:f.write(html)在導出的外部文件中搜索common即可定位到下面的頁面源代碼中的<script>部分:👇
似乎運氣不太好,window.common.token竟然是一個空字符串,別急,再往下面拉幾行看看:👇
token為空表示第一次訪問百度網站服務器端沒有收到baiduid cookie,會導致翻譯接口校驗不通過,通過刷新解決
原來這里需要帶上Cookie訪問才能得到token的值,值得注意的是,這里的請求頭與上文POST請求時請求頭并不完全相同,是不可以直接套用的,通過訪問https://fanyi.baidu.com/后抓包HTML,取得下圖中的請求頭即可:👇
此時我們加入完整的請求頭再來一遍:👇
# -*- coding: UTF-8 -*- # @author: caoyang # @email: caoyang@163.sufe.edu.cnimport requestsword = 'take' url = 'https://fanyi.baidu.com/#en/zh/' + word headers = {'Host' : 'fanyi.baidu.com','User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0','Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8','Accept-Language' : 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2','Accept-Encoding' : 'gzip, deflate, br','Connection' : 'keep-alive','Cookie' : 'BAIDUID=57D8DECD1001EDF4A260905A983072A9:FG=1; BIDUPSID=57D8DECD1001EDF4A260905A983072A9; PSTM=1612680499; BDRCVFR[gltLrB7qNCt]=mk3SLVN4HKm; delPer=0; PSINO=5; H_PS_PSSID=33425_33355_33273_33585; BA_HECTOR=2g8g240g0h00ak24451g1v39k0r; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; Hm_lvt_64ecd82404c51e03dc91cb9e8c025574=1612680504,1612680509; Hm_lpvt_64ecd82404c51e03dc91cb9e8c025574=1612680509; __yjs_duid=1_57795229af6fbff1bde0f88f5beda8381612680504436; ab_sr=1.0.0_ZGQxNTEyYzNmYmM3YzA3ODgxMTIzNzhkNTQ2MDg4ODU2ZDAxODNlODQxZjJlYzdkNDNhNjhlYjIyNWNlZjIxNmIzOTE2YzgxNjJjMTExMzlkMWY5NWQzOTUxMTkzYWZi; __yjsv5_shitong=1.0_7_f89862c9f80b86296408413c2a5c443713a1_300_1612680509895_49.95.205.54_60776bff; REALTIME_TRANS_SWITCH=1; FANYI_WORD_SWITCH=1; HISTORY_SWITCH=1; SOUND_SPD_SWITCH=1; SOUND_PREFER_SWITCH=1','Upgrade-Insecure-Requests' : '1', } r = requests.get(url, headers=headers) html = r.textwith open('baidufanyi_{}.html'.format(word), 'w', encoding='utf8') as f:f.write(html)重新查看導出的外部文件中的頁面源代碼,已經有token的值了:👇
另外如果直接在瀏覽器的控制臺中輸入變量名也可以直接獲取全局變量的值:👇
至此如何獲取token的方法已經明晰,下文中筆者將給出如何取出這個token值的一種相對比較魯棒的腳本方法(即如果頁面源代碼發生一些變化也能正確定位這個隨機變化的token值)。
3.2 生成sign的邏輯
w = {from : p.fromLang,to : p.toLang,query : n,transtype : r, simple_means_flag : 3, sign : f(n),token : window.common.token,domain : y.getCurDomain() }sign的值為f(n),想要在這個200多kb的JS文件中找到某個函數邏輯,似乎全文閱讀一遍是不現實的,而且f(n)一看就是局部定義的函數,是無法通過控制臺輸出看到結果的。
不過好消息是我們可以直接看出這個函數f的參數是什么:參數為n,而n恰好是字段query的值,即查詢的單詞(take),這可能算是迷茫中的慰藉了。
接下來的工作就相對偏于經驗了,而這本身也是爬蟲的魅力所在,因為每一個爬蟲都可能是不一樣的,同樣,在這里適用的邏輯溯源思路并不一定能用在其他復雜爬蟲上。筆者僅將自己的思路作為分享。
- ① 首先往找到的表單上方溯尋,找到最近的一個f的位置(這里如果你是用的相對高級的編輯器,只需要用鼠標框住f,則所有的整詞f都會高亮出來,非常便于尋找),如下圖中上邊兩個紅框所示:👇
- ② 上圖中可以看到f函數是由一個t函數定義得到的,一段JS代碼中這里亂七八糟的t函數會有很多,不過這個t函數的參數并不多見:translation:widget/translate/input/pGrab,似乎是一段路由,試著全文搜索這段路由字符串,于是從700多行的表單數據我們找到了200多行處的特征路由字符串:👇(紅框中為一個完整的函數體)
-
③ 顯然translation:widget/translate/input/pGrab被定義為一個方法的接口路由,推測所謂的t函數可能是調用這個接口方法,上圖紅框中是一個完整的函數體,可以推斷f(n)的邏輯就在這個紅框中了。簡單整理一下這段JS代碼:👇
;define("translation:widget/translate/input/pGrab",function(r,o,t){"use strict";function a(r){if(Array.isArray(r)){for(var o=0,t=Array(r.length);o<r.length;o++) t[o]=r[o];return t}return Array.from(r)}function n(r,o){for(var t=0;t<o.length-2;t+=3){var a=o.charAt(t+2);a=a>="a"?a.charCodeAt(0)-87:Number(a), a="+"===o.charAt(t+1)?r>>>a:r<<a, r="+"===o.charAt(t)?r+a&4294967295:r^a}return r}function e(r){var o=r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);if(null===o){var t=r.length;t>30&&(r=""+r.substr(0,10)+r.substr(Math.floor(t/2)-5,10)+r.substr(-10,10))}else{for(var e=r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/),C=0,h=e.length,f=[];h>C;C++)""!==e[C]&&f.push.apply(f,a(e[C].split(""))),C!==h-1&&f.push(o[C]);var g=f.length;g>30&&(r=f.slice(0,10).join("")+f.slice(Math.floor(g/2)-5,Math.floor(g/2)+5).join("")+f.slice(-10).join(""))}var u=void 0,l=""+String.fromCharCode(103)+String.fromCharCode(116)+String.fromCharCode(107);u=null!==i?i:(i=window[l]||"")||"";for(var d=u.split("."),m=Number(d[0])||0,s=Number(d[1])||0,S=[],c=0,v=0;v<r.length;v++){var A=r.charCodeAt(v);128>A?S[c++]=A:(2048>A?S[c++]=A>>6|192:(55296===(64512&A)&&v+1<r.length&&56320===(64512&r.charCodeAt(v+1))?(A=65536+((1023&A)<<10)+(1023&r.charCodeAt(++v)),S[c++]=A>>18|240,S[c++]=A>>12&63|128):S[c++]=A>>12|224,S[c++]=A>>6&63|128),S[c++]=63&A|128)}for(var p=m,F=""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(97)+(""+String.fromCharCode(94)+String.fromCharCode(43)+String.fromCharCode(54)),D=""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(51)+(""+String.fromCharCode(94)+String.fromCharCode(43)+String.fromCharCode(98))+(""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(102)),b=0;b<S.length;b++)p+=S[b],p=n(p,F);return p=n(p,D),p^=s,0>p&&(p=(2147483647&p)+2147483648),p%=1e6,p.toString()+"."+(p^m)}var i=null;t.exports=e} ); -
④ 這里面有三個函數a,n,e,你猜猜看哪個函數是加密的主函數?
- 筆者的JS水平實屬半吊子,我雖然不太清楚define這個函數到底是什么意思,但是最后一句t.exports=e已經給出提示,導出的是e!,那么肯定是把e函數作為路由的調用函數唄;
- 回頭再來看最外層的三個參數r,o,t:
- t就只用在t.exports=e處,其余地方用到的都是局部變量;
- o壓根就沒用到過,所用之處都是局部變量;
- r就更好了,內部的三個函數a,n,e都用參數r,所以根本也是沒用到過的;
- 所以只需要搞明白e的參數r到底是什么即可,這玩意兒猜都能猜出來,剛剛f(n)的參數n是啥來著,是查詢的單詞呀,那么這里的r還能是什么,只能也是查詢的單詞了;
至此分析結束,我們來檢驗一下分析的結論是否正確。這里筆者稍微打個岔,如果要將JS邏輯復現成Python相對費時費力,而且一旦JS邏輯改變將需要從頭分析,因此這里推薦使用execjs庫來直接執行JS代碼,簡單使用pip安裝即可:👇
pip install PyExecJS然后我們將上面三個函數a,n,e復制成字符串來執行一下試試看(別忘了尾巴上有個var i=null;):👇
# -*- coding: UTF-8 -*- # @author: caoyang # @email: caoyang@163.sufe.edu.cnimport execjsjavascript_code = '''function a(r){if(Array.isArray(r)){for(var o=0,t=Array(r.length);o<r.length;o++)t[o]=r[o]; return t}return Array.from(r)}function n(r,o){for(var t=0;t<o.length-2;t+=3){var a=o.charAt(t+2);a=a>="a"?a.charCodeAt(0)-87:Number(a),a="+"===o.charAt(t+1)?r>>>a:r<<a,r="+"===o.charAt(t)?r+a&4294967295:r^a }return r}function e(r){var o=r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);if(null===o){var t=r.length;t>30&&(r=""+r.substr(0,10)+r.substr(Math.floor(t/2)-5,10)+r.substr(-10,10)) }else{for(var e=r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/),C=0,h=e.length,f=[];h>C;C++)""!==e[C]&&f.push.apply(f,a(e[C].split(""))),C!==h-1&&f.push(o[C]); var g=f.length;g>30&&(r=f.slice(0,10).join("")+f.slice(Math.floor(g/2)-5,Math.floor(g/2)+5).join("")+f.slice(-10).join("")) }var u=void 0,l=""+String.fromCharCode(103)+String.fromCharCode(116)+String.fromCharCode(107);u=null!==i?i:(i=window[l]||"")||""; for(var d=u.split("."),m=Number(d[0])||0,s=Number(d[1])||0,S=[],c=0,v=0;v<r.length;v++){var A=r.charCodeAt(v);128>A?S[c++]=A:(2048>A?S[c++]=A>>6|192:(55296===(64512&A)&&v+1<r.length&&56320===(64512&r.charCodeAt(v+1))?(A=65536+((1023&A)<<10)+(1023&r.charCodeAt(++v)),S[c++]=A>>18|240,S[c++]=A>>12&63|128):S[c++]=A>>12|224,S[c++]=A>>6&63|128),S[c++]=63&A|128) }for(var p=m,F=""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(97)+(""+String.fromCharCode(94)+String.fromCharCode(43)+String.fromCharCode(54)),D=""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(51)+(""+String.fromCharCode(94)+String.fromCharCode(43)+String.fromCharCode(98))+(""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(102)),b=0;b<S.length;b++)p+=S[b],p=n(p,F); return p=n(p,D),p^=s,0>p&&(p=(2147483647&p)+2147483648),p%=1e6,p.toString()+"."+(p^m)}var i=null;'''print(execjs.compile(javascript_code).call('e', 'take'))報錯顯示:execjs._exceptions.ProgramError: TypeError: 'window' 未定義
好家伙,又是window變量,觀察一下window出現在e函數中:
u=null!==i?i:(i=window[l]||"")||"";
顯然window[l]是全局變量window的一個屬性值,但是l是什么呢?往上翻一行就可以看到:
l=""+String.fromCharCode(103)+String.fromCharCode(116)+String.fromCharCode(107);
讓我們到瀏覽器的控制臺里看看這是什么東西:👇
gtk!!!,一切都破案了,讓我們回到1 樸素的頁面源代碼爬取中的頁面源代碼里,我們找到了window.gtk的值👇
最后我們修改一下這段JS代碼中的e函數,為它添加一個參數gtk,并將其中的window[l]替換成gtk即可:👇
# -*- coding: UTF-8 -*- # @author: caoyang # @email: caoyang@163.sufe.edu.cnimport execjsjavascript_code = '''function a(r){if(Array.isArray(r)){for(var o=0,t=Array(r.length);o<r.length;o++)t[o]=r[o]; return t}return Array.from(r)}function n(r,o){for(var t=0;t<o.length-2;t+=3){var a=o.charAt(t+2);a=a>="a"?a.charCodeAt(0)-87:Number(a),a="+"===o.charAt(t+1)?r>>>a:r<<a,r="+"===o.charAt(t)?r+a&4294967295:r^a }return r}function e(r,gtk){var o=r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);if(null===o){var t=r.length;t>30&&(r=""+r.substr(0,10)+r.substr(Math.floor(t/2)-5,10)+r.substr(-10,10)) }else{for(var e=r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/),C=0,h=e.length,f=[];h>C;C++)""!==e[C]&&f.push.apply(f,a(e[C].split(""))),C!==h-1&&f.push(o[C]); var g=f.length;g>30&&(r=f.slice(0,10).join("")+f.slice(Math.floor(g/2)-5,Math.floor(g/2)+5).join("")+f.slice(-10).join("")) }var u=void 0,l=""+String.fromCharCode(103)+String.fromCharCode(116)+String.fromCharCode(107);u=null!==i?i:(i=gtk||"")||""; for(var d=u.split("."),m=Number(d[0])||0,s=Number(d[1])||0,S=[],c=0,v=0;v<r.length;v++){var A=r.charCodeAt(v);128>A?S[c++]=A:(2048>A?S[c++]=A>>6|192:(55296===(64512&A)&&v+1<r.length&&56320===(64512&r.charCodeAt(v+1))?(A=65536+((1023&A)<<10)+(1023&r.charCodeAt(++v)),S[c++]=A>>18|240,S[c++]=A>>12&63|128):S[c++]=A>>12|224,S[c++]=A>>6&63|128),S[c++]=63&A|128) }for(var p=m,F=""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(97)+(""+String.fromCharCode(94)+String.fromCharCode(43)+String.fromCharCode(54)),D=""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(51)+(""+String.fromCharCode(94)+String.fromCharCode(43)+String.fromCharCode(98))+(""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(102)),b=0;b<S.length;b++)p+=S[b],p=n(p,F); return p=n(p,D),p^=s,0>p&&(p=(2147483647&p)+2147483648),p%=1e6,p.toString()+"."+(p^m)}var i=null;'''print(execjs.compile(javascript_code).call('e', 'take', '320305.131321201'))輸出結果恰為POST表單數據中的sign字段值(形式類似,但值是不會相同的):
至此,所有問題已經得到解決,總結來看我們主要是解決了sign與token兩個字段的生成邏輯,只需要做最后的代碼整合即可。
源碼
雖然分析的過程較為漫長,但是代碼本身是非常簡潔的:👇
# -*- coding: UTF-8 -*- # @author: caoyang # @email: caoyang@163.sufe.edu.cnimport json import execjs import requests from bs4 import BeautifulSoup from urllib.parse import urlencodeclass BaiduFanyi(object):""""""def __init__(self) -> None:javascript_code = '''function n(r,o){for(var t=0;t<o.length-2;t+=3){var a=o.charAt(t+2);a=a>="a"?a.charCodeAt(0)-87:Number(a),a="+"===o.charAt(t+1)?r>>>a:r<<a,r="+"===o.charAt(t)?r+a&4294967295:r^a}return r}var i=null;function e(r,gtk){var o=r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);if(null===o){var t=r.length;t>30&&(r=""+r.substr(0,10)+r.substr(Math.floor(t/2)-5,10)+r.substr(-10,10))}else{for(var e=r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/),C=0,h=e.length,f=[];h>C;C++)""!==e[C]&&f.push.apply(f,a(e[C].split(""))),C!==h-1&&f.push(o[C]);var g=f.length;g>30&&(r=f.slice(0,10).join("")+f.slice(Math.floor(g/2)-5,Math.floor(g/2)+5).join("")+f.slice(-10).join(""))}var u=void 0,l=""+String.fromCharCode(103)+String.fromCharCode(116)+String.fromCharCode(107);u=null!==i?i:(i=gtk||"")||"";for(var d=u.split("."),m=Number(d[0])||0,s=Number(d[1])||0,S=[],c=0,v=0;v<r.length;v++){var A=r.charCodeAt(v);128>A?S[c++]=A:(2048>A?S[c++]=A>>6|192:(55296===(64512&A)&&v+1<r.length&&56320===(64512&r.charCodeAt(v+1))?(A=65536+((1023&A)<<10)+(1023&r.charCodeAt(++v)),S[c++]=A>>18|240,S[c++]=A>>12&63|128):S[c++]=A>>12|224,S[c++]=A>>6&63|128),S[c++]=63&A|128)}for(var p=m,F=""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(97)+(""+String.fromCharCode(94)+String.fromCharCode(43)+String.fromCharCode(54)),D=""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(51)+(""+String.fromCharCode(94)+String.fromCharCode(43)+String.fromCharCode(98))+(""+String.fromCharCode(43)+String.fromCharCode(45)+String.fromCharCode(102)),b=0;b<S.length;b++)p+=S[b],p=n(p,F);return p=n(p,D),p^=s,0>p&&(p=(2147483647&p)+2147483648),p%=1e6,p.toString()+"."+(p^m)}'''self.javascript_lambda = execjs.compile(javascript_code)self.headers = {'Host' : 'fanyi.baidu.com','User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0','Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8','Accept-Language' : 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2','Accept-Encoding' : 'gzip, deflate, br','Connection' : 'keep-alive','Cookie' : 'BAIDUID=57D8DECD1001EDF4A260905A983072A9:FG=1; BIDUPSID=57D8DECD1001EDF4A260905A983072A9; PSTM=1612680499; BDRCVFR[gltLrB7qNCt]=mk3SLVN4HKm; delPer=0; PSINO=5; H_PS_PSSID=33425_33355_33273_33585; BA_HECTOR=2g8g240g0h00ak24451g1v39k0r; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; Hm_lvt_64ecd82404c51e03dc91cb9e8c025574=1612680504,1612680509; Hm_lpvt_64ecd82404c51e03dc91cb9e8c025574=1612680509; __yjs_duid=1_57795229af6fbff1bde0f88f5beda8381612680504436; ab_sr=1.0.0_ZGQxNTEyYzNmYmM3YzA3ODgxMTIzNzhkNTQ2MDg4ODU2ZDAxODNlODQxZjJlYzdkNDNhNjhlYjIyNWNlZjIxNmIzOTE2YzgxNjJjMTExMzlkMWY5NWQzOTUxMTkzYWZi; __yjsv5_shitong=1.0_7_f89862c9f80b86296408413c2a5c443713a1_300_1612680509895_49.95.205.54_60776bff; REALTIME_TRANS_SWITCH=1; FANYI_WORD_SWITCH=1; HISTORY_SWITCH=1; SOUND_SPD_SWITCH=1; SOUND_PREFER_SWITCH=1','Upgrade-Insecure-Requests' : '1',}self.mainpage_url = 'https://fanyi.baidu.com/'self.transapi_url = 'https://fanyi.baidu.com/v2transapi'self.result = Nonedef v2transapi(self, keyword: str, source: str='en', target: str='zh') -> dict: session = requests.Session()session.headers = self.headers.copy()response = session.get(self.mainpage_url, headers=self.headers)html = response.textwith open('baidufanyi.html', 'w', encoding='utf8') as f:f.write(html)def _find_token_and_gtk(html: str) -> (str, str):soup = BeautifulSoup(html, 'lxml')scripts = soup.find_all('script')script_code = '''var window={};try{'''for script in scripts:_script_code = str(script.string).strip('\n')if _script_code.startswith('window'):script_code += _script_code + ''';'''script_code += '''}catch(e){}'''window = execjs.compile(script_code).eval('window')token = window['common']['token']gtk = window['gtk']return token, gtktoken, gtk = _find_token_and_gtk(html)formdata = {'from' : source,'to' : target,'query' : keyword,'simple_means_flag' : '3','sign' : self.javascript_lambda.call('e', keyword, gtk),'token' : token,'domain' : 'common',}# print(urlencode(formdata))response = session.post(self.transapi_url, data=formdata)result = response.json()with open('transapi_{}.json'.format(keyword), 'w', encoding='utf8') as f:json.dump(result, f)self.result = result.copy()return resultif __name__ == '__main__':baidufanyi = BaiduFanyi()baidufanyi.v2transapi('put')特別地,筆者在提取頁面源代碼的window變量值的邏輯寫在_find_token_and_gtk函數中,筆者的思路是提取頁面源代碼上所有的<script>標簽中的內容,然后使用execjs庫進行執行,再取window的變量值即可,這相對于較硬的搜索邏輯找到gtk和token要相對魯棒一些,不過為了減少抓取的<script>標簽數量,筆者做了一些限制,并且沒加入一段都使用異常拋出,因為本身來說window這個全局變量是包含了一些自有屬性的,直接復制并不能正常運行。
請求頭中的Cookie可能需要定時更換,不過其有效期應該是比較持久的,不必過于擔憂,此外可以加入代理IP的手段,不過可能會出一些問題,那是后話了。
最后關于得到的json數據的解析方法,其實得到的json數據是相當大的,筆者在BaiduFanyi類中另寫了三個測試方法,有需要的可以添加到上面的類代碼中:👇
def parse_example_sentence(self) -> list:if self.result is None:with open('transapi_{}.json'.format('take'), 'r', encoding='utf8') as f:self.result = json.load(f) dict_result = self.result.get('dict_result')liju_result = self.result.get('liju_result')for synonym in dict_result.get('synonym'):for _synonym in synonym['synonyms']:for example in _synonym['ex']:print(example['enText'], example['chText'])def parse_translation_result(self) -> list: if self.result is None:with open('transapi_{}.json'.format('take'), 'r', encoding='utf8') as f:self.result = json.load(f) trans_result = self.result.get('trans_result')translation_result = trans_result['data'][0]['dst'] return translation_resultdef _parse_json(self) -> None:if self.result is None:with open('transapi_{}.json'.format('take'), 'r', encoding='utf8') as f:self.result = json.load(f)trans_result = self.result.get('trans_result')dict_result = self.result.get('dict_result')liju_result = self.result.get('liju_result')trans_result_simple = trans_result['data'][0]['dst']print(trans_result_simple)for synonym in dict_result.get('synonym'):for _synonym in synonym['synonyms']:for example in _synonym['ex']:print(example['enText'], example['chText'])if dict_result is not None:for key, value in dict_result.items():print(key, type(value)) print('#' * 64)print('--sanyms--')if dict_result.get('sanyms') is not None:for sanyms in dict_result['sanyms']:print(sanyms['tit'])for data in sanyms['data']:print(data['p'], data['d'])print('-' * 64)print('#' * 64) print('--synonym--') if dict_result.get('synonym') is not None:for synonym in dict_result['synonym']:for key, value in synonym.items():print(key, value)print('-' * 64)print('#' * 64)print('--usecase--')if dict_result.get('usecase') is not None:for key, value in dict_result['usecase'].items():print(key, value)print('#' * 64)print('--collins--')if dict_result.get('collins') is not None:for key, value in dict_result['collins'].items():print(key, value)print('#' * 64)print('--edict--')if dict_result.get('edict') is not None:for key, value in dict_result['edict'].items():print(key, value)print('#' * 64)print('--simple_means--')if dict_result.get('simple_means') is not None:for key, value in dict_result['simple_means'].items():print(key, value)print('#' * 64)print('--queryExplainVideo--')if dict_result.get('queryExplainVideo') is not None:for key, value in dict_result['queryExplainVideo'].items():print(key, value)if dict_result is not None:pass本文完,之后筆者將更新一些paper閱讀筆記,目前想看一些文本摘要方向的內容,不過可能還會繼續看text-to-SQL方向的東西,誰知道呢~
分享學習,共同進步!望諸君新年安好。
總結
以上是生活随笔為你收集整理的【日常】爬虫学习进阶:百度翻译的秘密(2021版)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 成都android培训成都java培训成
- 下一篇: 杭州地铁首末站周边停车场正酝酿停车收费优