使用 beautifulsoup 进行网页抓取的问题

时间:2021-04-10 11:18:47

标签: python html web-scraping beautifulsoup python-requests

我正在尝试通过网络抓取 who 网站 (https://www.who.int/emergencies/diseases/novel-coronavirus-2019) 以了解当前 COVID 的死亡人数。我遇到了一个问题,它在哪里找到元素,但它给了我不同的内容。我的代码:

import requests 
from bs4 import BeautifulSoup

WORLDWIDE_URL = "https://www.who.int/emergencies/diseases/novel-coronavirus-2019"

page = requests.get(WORLDWIDE_URL)

soup = BeautifulSoup(page.content, "lxml")

print(soup.find(id="confirmedDeaths"))

它没有给我一个字符串,它没有给我任何东西。

1 个答案:

答案 0 :(得分:2)

您没有得到网站上显示的结果的原因是这些结果实际上是由 JavaScript 函数填充的,而不是硬编码到网站中。
如果您只是检查源代码(Firefox 中的 Ctrl+U),您会得到相同的结果。

仅使用 requests.get,您只需检索不存在信息的源代码。

大多数情况下,您现在会陷入困境,不得不求助于诸如使用 selenium 在访问信息之前渲染Javascript等解决方案,但我找到了一个不同的解决方案,您甚至不需要 BeautifulSoup:

在查看页面的源代码时,我发现负责设置这些值的 JavaScript 在内部只是调用一个又长又丑的 URL 来检索这些信息:

https://services.arcgis.com/5T5nSi527N4F7luB/arcgis/rest/services/COVID_19_Historic_cases_by_country_pt_v7_view/FeatureServer/0/query?where=CumCase+%3E+0&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&resultType=none&distance=0.0&units=esriSRUnit_Meter&returnGeodetic=false&outFields=OBJECTID%2CISO_2_CODE%2CISO_3_CODE%2CADM0_NAME%2Cdate_epicrv%2CNewCase%2CCumCase%2CNewDeath%2CCumDeath&returnGeometry=true&featureEncoding=esriDefault&multipatchOption=xyFootprint&maxAllowableOffset=&geometryPrecision=&outSR=4326&datumTransformation=&applyVCSProjection=false&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnExtentOnly=false&returnQueryGeometry=false&returnDistinctValues=false&cacheHint=false&orderByFields=date_epicrv+desc&groupByFieldsForStatistics=date_epicrv&outStatistics=[{%27statisticType%27%3A+%27sum%27%2C+%27onStatisticField%27%3A+%27NewCase%27}%2C+{%27statisticType%27%3A+%27sum%27%2C+%27onStatisticField%27%3A+%27NewDeath%27}%2C+{%27statisticType%27%3A+%27sum%27%2C+%27onStatisticField%27%3A+%27CumCase%27}%2C+{%27statisticType%27%3A+%27sum%27%2C+%27onStatisticField%27%3A+%27CumDeath%27}%2C+{%27statisticType%27%3A+%27Count%27%2C+%27onStatisticField%27%3A+%27ADM0_NAME%27}]&having=&resultOffset=&resultRecordCount=&returnZ=false&returnM=false&returnExceededLimitFeatures=true&quantizationParameters=&sqlFormat=none&f=pjson&token=

调用此 URL,您可以检索包含您需要的所有信息的 JSON 字典。

我决定分解这个网址,以实际向您展示发生了什么。这是我的完整代码:

import requests
import json
import urllib.parse

payload = {
    'where': 'CumCase+>+0',
    'objectIds': '',
    'time': '',
    'geometry': '',
    'geometryType': 'esriGeometryEnvelope',
    'inSR': '',
    'spatialRel': 'esriSpatialRelIntersects',
    'resultType': None,
    'distance': '0.0',
    'units': 'esriSRUnit_Meter',
    'returnGeodetic': False,
    'outFields': ','.join([
        'OBJECTID',
        'ISO_2_CODE',
        'ISO_3_CODE',
        'ADM0_NAME',
        'date_epicrv',
        'NewCase',
        'CumCase',
        'NewDeath',
        'CumDeath'
    ]),
    'returnGeometry': True,
    'featureEncoding': 'esriDefault',
    'multipatchOption': 'xyFootprint',
    'maxAllowableOffset': '',
    'geometryPrecision': '',
    'outSR': 4326,
    'datumTransformation': '',
    'applyVCSProjection': False,
    'returnIdsOnly': False,
    'returnUniqueIdsOnly': False,
    'returnCountOnly': False,
    'returnExtentOnly': False,
    'returnQueryGeometry': False,
    'returnDistinctValues': False,
    'cacheHint': False,
    'orderByFields': 'date_epicrv+desc',
    'groupByFieldsForStatistics': 'date_epicrv',
    'outStatistics': [
        {"statisticType": "sum", "onStatisticField": "NewCase"},
        {"statisticType": "sum", "onStatisticField": "NewDeath"},
        {"statisticType": "sum", "onStatisticField": "CumCase"},
        {"statisticType": "sum", "onStatisticField": "CumDeath"},
        {"statisticType": "Count", "onStatisticField": "ADM0_NAME"}
    ],
    'having': '',
    'resultOffset': '',
    'resultRecordCount': '',
    'returnZ': False,
    'returnM': False,
    'returnExceededLimitFeatures': True,
    'quantizationParameters': '',
    'sqlFormat': None,
    'f': 'pjson',
    'token': ''

}

payload_str = urllib.parse.urlencode(payload, safe='+[]{}')

# Replace True, False, None
payload_str = payload_str.replace('False', 'false')
payload_str = payload_str.replace('True', 'true')
payload_str = payload_str.replace('None', 'none')

r = requests.get(
    'https://services.arcgis.com/5T5nSi527N4F7luB/arcgis/rest/services/COVID_19_Historic_cases_by_country_pt_v7_view/FeatureServer/0/query',
    params=payload_str
)
json_dict = json.loads(r.text)

total_deaths = json_dict['features'][0]['attributes']['SUM_CumDeath']

说明

  1. 除了单纯的 URL 之外,requests.get 还接受(除其他外)另一个名为 params 的参数。
    在 URL 字符串中,封装在 & 字符之间的每个元素实际上是一个与请求一起传递的单独参数。因此,您可以使用 requests.get 符号之前的 URL 部分调用 ?,并将 params 设置为包含所有其他需要的参数的字典,而不是只有一个长而难看的字符串也包括在内。
    从我的回答中可以看出,这使得实际理解请求变得更加容易。

    如果您查看我的代码,您会意识到我实际上并没有这样做。 为什么?
    当通过 params 指定参数时,requests 不会将它们传递给原始请求,而是对它们进行编码,即 + 变为 %2B 和等等。
    在这种情况下,问题是如果请求编码 404 not found-signs,服务器将返回 +,所以我需要另一种方法来编码 payload,而不会丢失 {{1 }}-标志。
    解决方案是使用 +,它接受​​要从编码中排除的字符串,在这种情况下,我使用了以下字符串:urllib.parse.
    所以我的解决方案是对有效负载进行预编码,然后将字符串传递给 '+[]{}' 而不是字典。

  2. 由于服务器太挑剔,我还不得不用小写版本替换 python requestsTrueFalse,否则参数将无法识别。

  3. 当你发出请求时,你会得到一个 JSON-dict 而不是网站的 html-source,所以你不需要 None,你只需解析json 并留下一个简单的 Python BeautifulSoup

    这本词典可能包含对您有用的其他信息。如果您想仔细查看它,只需在浏览器中打开上面的长 URL。大多数浏览器会自动为您“美化” JSON。