爬取https://www.thenewboston.com/时出现“SSL: certificate_verify_failed”错误

29

我最近开始使用YouTube上的"The New Boston"视频学习Python,一切都进行得很顺利,直到我看他关于制作简单网络爬虫的教程时。虽然我没有理解上的问题,但当我运行代码时,我不断收到与“SSL: CERTIFICATE_VERIFY_FAILED”相关的错误提示。我从昨晚开始寻找答案,试图弄清楚如何修复它,似乎视频评论或他网站上没有其他人遇到跟我一样的问题,即使是使用他网站上其他人的代码,我也得到了相同的结果。下面是我从网站上获取的代码,因为它给我带来了同样的错误,而我自己编码的代码现在也是一团糟。

import requests
from bs4 import BeautifulSoup

def trade_spider(max_pages):
    page = 1
    while page <= max_pages:
        url = "https://www.thenewboston.com/forum/category.php?id=15&orderby=recent&page=" + str(page) #this is page of popular posts
        source_code = requests.get(url)
        # just get the code, no headers or anything
        plain_text = source_code.text
        # BeautifulSoup objects can be sorted through easy
        for link in soup.findAll('a', {'class': 'index_singleListingTitles'}): #all links, which contains "" class='index_singleListingTitles' "" in it.
            href = "https://www.thenewboston.com/" + link.get('href')
            title = link.string # just the text, not the HTML
            print(href)
            print(title)
            # get_single_item_data(href)
    page += 1
trade_spider(1)

完整的错误信息为:ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:645)

如果我的问题很愚蠢,我道歉,因为我还是个新手,但我真的无法解决这个问题,我想跳过这个教程,但是不能修复这个问题一直在困扰着我,谢谢!


当我试图导航到 https://www.thenewboston.com/tops.php?type=text&period=this-month&page=1 时,出现了“文件未找到”的消息。您能验证页面是否存在吗? - NuclearPeon
1
@NuclearPeon 嗯,我本来以为它昨天还能用,不过无论如何,我试着使用 https://www.thenewboston.com/forum/category.php?id=15&orderby=recent&page= 这个链接,应该可以带你到网站的 Python 部分,但是我用它仍然遇到了完全相同的错误。 - Bill Jenkins
我花了几个小时来解决这个问题,结果发现我一直在运行 Fiddler。 - JBourne
7个回答

27
问题不在你的代码中,而是在你尝试访问的网站上。查看SSLLabs的分析时,你会注意到:

该服务器的证书链不完整。等级被限制为B。

这意味着服务器配置有误,不仅Python,还有其他几个应用程序也会在访问该网站时出现问题。一些桌面浏览器通过尝试从互联网加载缺失的证书或填充缓存证书来解决此配置问题。但其他浏览器或应用程序也会失败,类似于Python。
为了解决破损的服务器配置,你可以明确提取缺失的证书并将其添加到您的信任存储中。或者你可以在verify参数中给出证书作为信任。从文档中了解详情。

You can pass verify the path to a CA_BUNDLE file or directory with certificates of trusted CAs:

>>> requests.get('https://github.com', verify='/path/to/certfile') 

This list of trusted CAs can also be specified through the REQUESTS_CA_BUNDLE environment variable.


谢谢你的回答!这就是我大部分无法确定是否是我的问题的地方。再次感谢你的帮助! - Bill Jenkins
1
@BillJenkins:通常的做法不是添加一条感谢的评论,而是在答案正确时接受它。 - Steffen Ullrich
我在Yelp上也遇到了同样的错误,该网站拥有A+评级。 - derp92
@derp92:如果这里的答案不能解决您的问题,那可能是另一个问题。在这种情况下,请查看其他可能有帮助的问题,如果找不到任何问题,请提出一个新问题,并添加所有必要的信息以重现该问题。评论功能不适用于寻求帮助。 - Steffen Ullrich

25

您可以告诉请求不验证SSL证书:

>>> url = "https://www.thenewboston.com/forum/category.php?id=15&orderby=recent&page=1"
>>> response = requests.get(url, verify=False)
>>> response.status_code
200

requests文档中查看更多信息。


11
关闭验证通常是一个不好的建议,因为它通过使应用程序变得不安全来解决问题。证书被验证的原因是防止中间人攻击。因此,虽然这可能是一个仅用于测试的解决方法,但在生产代码中永远不要这样做。最好学习如何正确地修复问题,而不是以不安全的方式绕过它。 - Steffen Ullrich

7
你的系统可能缺少股票证书。例如,如果在Ubuntu上运行,请检查是否安装了ca-certificates软件包。

1
我在Linux上缺少了这个包,添加它解决了问题。问题是我不确定如何在Windows上修复它。 - Daniel F

5
如果您想使用Python dmg安装程序,则还需要阅读Python 3的ReadMe并运行bash命令以获取新证书。
尝试运行:
/Applications/Python\ 3.6/Install\ Certificates.command

4

值得再多讲一些关于这里发生的事情的“实践”光,补充@Steffen Ullrich在这里和其他地方的答案:

注:

  • 我将使用另一个网站而不是OP,因为OP的网站目前没有问题。
  • 我使用Ubunto运行以下命令(curlopenssl)。我尝试在我的Windows 10上运行curl,但输出不同且没有帮助。

通过使用以下curl命令可以“复制”OP遇到的错误:

curl -vvI https://www.vimmi.net

输出结果为(请注意最后一行):

* TCP_NODELAY set
* Connected to www.vimmi.net (82.80.192.7) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, Server hello (2):
* SSL certificate problem: unable to get local issuer certificate
* stopped the pause stream!
* Closing connection 0
curl: (60) SSL certificate problem: unable to get local issuer certificate

现在让我们使用--insecure标志运行它,这将显示有问题的证书:
curl --insecure -vvI https://www.vimmi.net

输出(注意最后两行):

* Rebuilt URL to: https://www.vimmi.net/
*   Trying 82.80.192.7...
* TCP_NODELAY set
* Connected to www.vimmi.net (82.80.192.7) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* [...]
* Server certificate:
*  subject: OU=Domain Control Validated; CN=vimmi.net
*  start date: Aug  5 15:43:45 2019 GMT
*  expire date: Oct  4 16:16:12 2020 GMT
*  issuer: C=US; ST=Arizona; L=Scottsdale; O=GoDaddy.com, Inc.; OU=http://certs.godaddy.com/repository/; CN=Go Daddy Secure Certificate Authority - G2
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.

同样的结果可以使用openssl来查看,值得一提的是,它被Python内部使用:

echo | openssl s_client -connect vimmi.net:443

输出:

CONNECTED(00000005)
depth=0 OU = Domain Control Validated, CN = vimmi.net
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 OU = Domain Control Validated, CN = vimmi.net
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
 0 s:OU = Domain Control Validated, CN = vimmi.net
   i:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
---
Server certificate
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
[...]
---
DONE

那么为什么curl和openssl都无法验证Go Daddy为该网站颁发的证书呢?

嗯,"验证证书"(使用openssl的错误消息术语)意味着验证证书包含受信任源签名(换句话说:证书是由受信任源签名的),从而验证vimmi.net的身份(这里的“身份”严格意义上是指“证书中包含的公钥属于证书中注明的个人、组织、服务器或其他实体”)。

如果我们可以建立其"信任链",则源是"受信任的", 具有以下属性

  1. 每个证书的颁发者(除了最后一个)与列表中下一个证书的主题相匹配
  2. 每个证书(除了最后一个)都由对应于链中下一个证书的密钥签名(即,可以使用包含在以下证书中的公钥验证一个证书的签名)
  3. 列表中的最后一个证书是信任锚点:您信任它,因为它是通过某些可信赖的程序交付给您的证书

在我们的情况下,颁发者是“Go Daddy Secure Certificate Authority - G2”。也就是说,名为“Go Daddy Secure Certificate Authority - G2”的实体签署了该证书,因此它应该是一个受信任的源。

要建立这个实体的信任度,我们有两个选择:

假设“Go Daddy Secure Certificate Authority - G2”是一个“信任锚点”(见上面的第3个清单)。实际上,curlopenssl试图根据这个假设进行操作:它们在默认路径(称为CA路径)上搜索该实体的证书,这些路径分别是:

  • curl的路径是/etc/ssl/certs
  • openssl的路径是/use/lib/ssl(运行openssl version -a查看)。

但是没有找到那个证书,留下了我们的第二个选择:

  1. 按照上面列出的步骤1和2进行操作;为了实现这一点,我们需要为该实体颁发证书。 可以通过从其来源下载或使用浏览器来实现。
    • 例如,使用Chrome浏览器打开vimmi.net,单击挂锁 > “证书” > “认证路径”选项卡,选择实体 > “查看证书”,然后在打开的窗口中转到“详细信息”选项卡 > “复制到文件” > 基于64位编码 > 保存文件)

太好了!现在我们有了那个证书(它可以是任何文件格式:cer, pem;您甚至可以将其保存为txt文件),让我们告诉curl如何使用它:

curl --cacert test.cer https://vimmi.net

回到Python

一旦我们拥有:

  1. "Go Daddy Secure Certificate Authority - G2"证书
  2. "Go Daddy Root Certificate Authority - G2"证书(上面没有提到,但可以通过类似的方式实现)。

我们需要将它们的内容复制到一个单独的文件中,让我们称之为combined.cer,并将其放置在当前目录中。然后,只需要执行以下操作:

import requests

res = requests.get("https://vimmi.net", verify="./combined.cer")
print (res.status_code) # 200
  • 顺便提一下,“Go Daddy 证书颁发机构 - G2”已被浏览器和各种工具列为可信任的机构;这就是为什么我们不需要为curl指定它。

更多阅读:


2

我将此作为答案发布,因为我已经解决了你的问题,但你的代码仍然存在问题(修复后,我可以更新)。

长话短说:你可能正在使用旧版本的requests或ssl证书无效。在这个SO问题中有更多信息:Python requests "certificate verify failed"

我已经将代码更新到我的bsoup.py文件中:

#!/usr/bin/env python3

import requests
from bs4 import BeautifulSoup

def trade_spider(max_pages):
    page = 1
    while page <= max_pages:
        url = "https://www.thenewboston.com/forum/category.php?id=15&orderby=recent&page=" + str(page) #this is page of popular posts
        source_code = requests.get(url, timeout=5, verify=False)
        # just get the code, no headers or anything
        plain_text = source_code.text
        # BeautifulSoup objects can be sorted through easy
        for link in BeautifulSoup.findAll('a', {'class': 'index_singleListingTitles'}): #all links, which contains "" class='index_singleListingTitles' "" in it.
            href = "https://www.thenewboston.com/" + link.get('href')
            title = link.string # just the text, not the HTML
            print(href)
            print(title)
            # get_single_item_data(href)

        page += 1

if __name__ == "__main__":
    trade_spider(1)

当我运行脚本时,它会给我这个错误:
https://www.thenewboston.com/forum/category.php?id=15&orderby=recent&page=1
Traceback (most recent call last):
  File "./bsoup.py", line 26, in <module>
    trade_spider(1)
  File "./bsoup.py", line 16, in trade_spider
    for link in BeautifulSoup.findAll('a', {'class': 'index_singleListingTitles'}): #all links, which contains "" class='index_singleListingTitles' "" in it.
  File "/usr/local/lib/python3.4/dist-packages/bs4/element.py", line 1256, in find_all
    generator = self.descendants
AttributeError: 'str' object has no attribute 'descendants'

你的 findAll 方法出现了问题。我已经尝试了 Python3 和 Python2,其中 Python2 报告了以下错误:

TypeError: unbound method find_all() must be called with BeautifulSoup instance as first argument (got str instance instead)

看起来你需要修复那个方法才能继续。

谢谢回复!我还是有点困惑 SSL 问题,因为我在另一个论坛上成功解决了这个问题,所以我不认为这是请求的问题。我猜这意味着 SSL 证书无效?但我不知道这实际上意味着什么。我去了你链接的帖子,将 requests.get(url) 更改为 requests.get(url, verify = False),现在我只收到一个关于 BeautifulSoup 中未指定解析器的错误。虽然我不太理解其中任何一部分,但 verify = False 让它工作了,或者解析器没有被指定。 - Bill Jenkins
@BillJenkins 我也不是很理解。我听说微软最近作废了一堆旧证书,而且不同的浏览器对证书的处理方式也有所不同(Firefox与Chrome)。希望我能提供更多帮助,抱歉。祝你好运! - NuclearPeon

0

我花了几个小时来修复一些Python代码并更新虚拟机上的证书。在我的情况下,我是在与别人设置的服务器进行交互。结果证明,错误的证书已经上传到了服务器上。我在另一个SO答案中找到了这个命令。

root@ubuntu:~/cloud-tools# openssl s_client -connect abc.def.com:443
CONNECTED(00000005)
depth=0 OU = Domain Control Validated, CN = abc.def.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 OU = Domain Control Validated, CN = abc.def.com
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
0 s:OU = Domain Control Validated, CN = abc.def.com
   i:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接