add by zhj: 我使用方法2“更新系統的certificate”解決了問題
原文:https://www.jianshu.com/p/8deb13738d2c
這兩天在Linux上爬Google Play的app列表時,發現之前的腳本不能用了,總是報SSLError。花了一天的時間進行定位,雖然還是不明白為什么腳本之前能用,現在卻不能用,但總算找到了解決辦法。記錄一下解決思路和過程,以供他人遇到類似問題時參考。
問題
腳本是用Python 3.4寫的,用到了一個開源的庫play-scraper,調用其collection
API來獲取Google Play的Top App列表。該庫使用了requests作為客戶端來對Google Play進行操作。當腳本執行時,會報如下錯誤:certificate verify failed。
Traceback (most recent call last):
File "/home/me/py3.4/lib/python3.4/site-packages/urllib3/connectionpool.py", line 600, in urlopen chunked=chunked) File "/home/me/py3.4/lib/python3.4/site-packages/urllib3/connectionpool.py", line 345, in _make_request self._validate_conn(conn) File "/home/me/py3.4/lib/python3.4/site-packages/urllib3/connectionpool.py", line 844, in _validate_conn conn.connect() File "/home/me/py3.4/lib/python3.4/site-packages/urllib3/connection.py", line 326, in connect ssl_context=context) 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上,該解決方法是有效的。
總結
使用開源軟件的好處是可以看實現源碼,花點時間讀源碼,調試定位,問題基本不難解決。但是文檔有可能不是那么完備,需要進行Google或仔細讀源碼。希望自己的分析思路對別人有所幫助吧。
作者:PythonDeveloper
鏈接:https://www.jianshu.com/p/8deb13738d2c
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。