問題:腳本是用Python寫的,用到開源庫play-scraper,調用其collectionAPI來獲取Google Play的Top App列表。該庫使用了requests作為客戶端來對Google Play進行操作。當腳本執行時,會報如下錯誤:certificate verify failed。
File "/home/me/py3.4/lib/python3.4/site-packages/urllib3/util/ssl_.py", line 325, in ssl_wrap_socket
return context.wrap_socket(sock, server_hostname=server_hostname)
File "/usr/local/lib/python3.4/ssl.py", line 365, in wrap_socket
_context=self)
File "/home/me/py3.4/lib/python3.4/site-packages/gevent/_ssl3.py", line 232, in __init__
raise x
File "/home/me/py3.4/lib/python3.4/site-packages/gevent/_ssl3.py", line 228, in __init__
self.do_handshake()
File "/home/me/py3.4/lib/python3.4/site-packages/gevent/_ssl3.py", line 545, in do_handshake
self._sslobj.do_handshake()
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:600)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/me/py3.4/lib/python3.4/site-packages/requests/adapters.py", line 440, in send
timeout=timeout
File "/home/me/py3.4/lib/python3.4/site-packages/urllib3/connectionpool.py", line 630, in urlopen
raise SSLError(e)
urllib3.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:600)
定位過程
仔細分析Traceback,發現問題出在def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None)中。注意verify參數,默認為True。在play-scraper中也是將其設為True的,說明在SSL握手過程中要驗證certificate的。
Google了一下錯誤信息,大致有以下幾種解決方法:
1. 將verify設為False,不驗證certificate
參考:https://stackoverflow.com/a/30373147/2510797
簡單粗暴,但是有效。不報錯誤了,但總是有Insecure request的告警。對于有代碼潔癖的本人來說,這顯然是不能接受的,除非時間非常緊迫。繼續定位。
2. 更新系統的certificate。
參考:https://stackoverflow.com/a/24212501/2510797
sudo apt-get install ca-certificates
看了一下所用Linux系統的ca-certificates package,確實比較老了,但之前一直沒有問題。死馬當活馬醫試試吧,但問題依舊。
3. 指定系統certificate的路徑
參考: https://stackoverflow.com/a/16085737/2510797
Linux系統certificate的certificate路徑在/etc/ssl/certs。使用verify="/etc/ssl/certs"試試,發現確實不報錯誤了。但是這個方法的弊端也是顯而易見:play-scraper并沒有在API中提供傳入參數verify,必須要修改其代碼才行。不同的操作系統,其certificate存放的位置肯定不一樣,要是代碼支持跨平臺,就需要判斷操作系統的類型,然后傳入相應的verify值。對于一個相對使用比較廣泛的requests庫來說,這么做顯然不太合理。
4. 使用certifi的certitificate路徑
參考:https://stackoverflow.com/a/35791445/2510797
看了一下requests的文檔,發現它使用了certifi package。然后再去看certifi的文檔,發現其certificate路徑有兩個:certifi.where()和certifi.old_where()。快速瀏覽了一下requests的源碼,發現如果verify=True的話,所用的certificate就是certifi.where(),所以就試了一下old_where(),居然不報錯了。但看到certifi的文檔中建議盡量不要用old_where(),所以還是不甘心,繼續定位。
5. 安裝requests的security extras
參考:https://stackoverflow.com/a/39580231/2510797
pip install -U requests[security]
注意后面的方括號,pip會安裝三個security相關的package:pyopenssl cryptography idna。
試了一下,果然有效,不再報錯。再去讀requests和urllib3的源碼,發現確實使用了pyopenssl。具體是怎么用的,還沒有來得及分析。
至此個人覺得比較好的解決方法基本成型:修改play-scraper的dependency,使用requests[security]來安裝那三個安全相關的包。
另外,系統的openssl版本太舊或太新也可能會造成問題。在目前最新版本的openssl上,該解決方法是有效的。
6.anaconda版的python問題
如果你在第一次使用requests時出現SSL錯誤:SSLError("Can’t connect to HTTPS URL because the SSL " urllib3.exceptions.SS,如果你是用的anaconda版的python,那么只要裝python原版就好了,原帖附上:
大致就是說anaconda版的python是用的anaconda自己的SSL庫,所以會報錯,換回原版python就不會有這個問題。
總結:使用開源軟件的好處是可以看實現源碼,花點時間讀源碼,調試定位,問題基本不難解決。但是文檔有可能不是那么完備,需要進行Google或仔細讀源碼。希望自己的分析思路對別人有所幫助吧。