在Python中使用request和BeautifulSoup来搜刮数据(附实例)

212 阅读12分钟

互联网上的数据量是相当惊人的。在一个网站上进行快速搜索并点击查看数据通常是很容易的。然而,如果你想在你的分析中实际使用这些数据,你必须能够获取它并将其转换为可用的格式。然而,网站的创建者和所有者可能不希望你这样做。他们可能更喜欢你只看数据,以及其周围的广告。你想使用这些数据进行分析的事实本身就使其具有价值。数据提供者很可能从你看数据时查看的广告中赚钱。他们甚至可能向你收取查看数据本身的费用。出于这个原因,他们有动力阻止你去获取数据。

在这篇文章中,我将向你展示一种非常基本的下载(或刮取)数据的方法,当最简单的方法可能不起作用。它并不是在每一种情况下都有效,但你可以把它加入你的工具箱,在你需要使用python来搜刮数据时考虑。

之前的一篇文章中,我使用pandas库从维基百科上下载了一个表格。它的效果相当好。Pandas会读取一个html页面,寻找页面中的表格,然后把它找到的每一个表格都变成一个DataFrame的列表:

import pandas as pd

fomc = pd.read_html("https://en.wikipedia.org/wiki/History_of_Federal_Open_Market_Committee_actions")

print(len(fomc))
fomc[1].head()
5
                 Date Fed. Funds Rate Discount Rate      Votes  \
0    November 5, 2020        0%–0.25%         0.25%       10-0   
1  September 16, 2020        0%–0.25%         0.25%        8-2   
2     August 27, 2020        0%–0.25%         0.25%  unanimous   
3       July 29, 2020        0%–0.25%         0.25%       10-0   
4       June 10, 2020        0%–0.25%         0.25%       10-0   

                                               Notes  Unnamed: 5  
0                                 Official statement         NaN  
1  Kaplan dissented, preferring "the Committee [t...         NaN  
2  No meeting, but announcement of approval of up...         NaN  
3                                 Official statement         NaN  
4                                 Official statement         NaN  

为什么我们需要其他东西来搜刮数据呢?

如果生活是简单的,这将适用于我们想要使用的所有网页。然而,有时它就是不工作,所以我们需要进一步挖掘这个工作的细节。例如,假设我们想从雅虎财经获取历史收益数据。我在之前的文章中谈到了雅虎财经的一个API,但是我在那里使用的API并不能给你提供雅虎财经收益页面上的细化历史收益数据。让我们看看我们是否能抓住一个符号的历史收益数据,就像你在这里看到的,AAPL的。你可以在浏览器中看到这个页面,它包含一个结果表,但当你试图用pandas加载它时,会发生什么?

url = "https://finance.yahoo.com/calendar/earnings/?symbol=AAPL"
try:
    pd.read_html(url)
except Exception as ex:
    print(ex)
HTTP Error 404: Not Found

为什么会出现404?

在运行这段代码的时候,我得到了一个404错误。这意味着该页面 "未找到"。但我们知道它确实存在,那么到底发生了什么?

这是雅虎告诉你滚蛋的方式,这里不欢迎你的屏幕刮擦尝试。事实证明,维基百科允许我们机械地下载网页,但雅虎不允许。我们或许还能尝试下载数据吗?

对于那些返回原始html请求的网站来说,只要你能让网络服务器相信你不是自动化软件,而是一个被人阅读的真正的网络浏览器,就应该可以读取数据。如果我们看一下read_html源代码,我们可以看到pandas是如何做到这一点的基本原理。这段代码有点复杂,但请随意阅读,但基本上它做了以下事情:

  1. 使用一个分析器获取原始HTMLurllib
  2. 使用一个解析器来解析原始html,然后获取所有的表格
  3. 将这些表格变成DataFrames
  4. 处理上述所有步骤的大量选项,包括使用不同的分析器和选项来创建表格。DataFrames

在第一步中,有一件事让人眼前一亮,那就是pandas没有设置任何HTTP头(或允许你把它们传入这个方法),所以雅虎可能只是拒绝了这个连接,因为它看起来像是自动的。为了写一些较低级别的代码,让我们考虑如何直接连接到雅虎的HTTP服务器,并对我们在请求中发送的内容有一些更多的控制。

请求库

有一个很容易使用的python库,叫做requests,可以用来自动处理HTTP请求。(如果你想研究一下HTTP的规格,它们都在这里列出)。让我们来看看,如果我们使用requests对同一网址做最简单的请求(HTTP GET请求)会发生什么。requests库对每一个HTTP动词都有相应的方法,我们可以直接把url传给它。它返回一个响应对象,其中包含被包装的服务器响应。如果你在响应上调用raise_for_status ,它将为遇到的任何错误引发一个HTTPError 。用pip install requests 安装请求:

import requests

res = requests.get(url)
try:
    res.raise_for_status()
except requests.exceptions.HTTPError as err:
    print(err)
404 Client Error: Not Found for url: https://finance.yahoo.com/calendar/earnings/?symbol=AAPL

好吧,我们有同样的错误,所以这是一个好的开始。我们还可以做什么来试图说服雅虎我们是一个真正的浏览器?get 方法只是对request 方法的一个薄的包装,它需要一些参数。其中一个非常重要的参数是headers ,这是一个HTTP头信息的口令,可以在请求中传递。网络浏览器总是发送一个叫做User-Agent的标识符。最符合逻辑的头信息是一个有效的用户代理。获得一个可用的值的方法是,看看你当前的网络浏览器正在发送什么。一个方便的方法是使用DuckDuckGo,当你问它my user agent ,它就会给你这个信息,像这样

对我来说,这刚好是Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15 。让我们试着把它添加到我们的请求中:

headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                         "AppleWebKit/605.1.15 (KHTML, like Gecko) "
                         "Version/15.4 Safari/605.1.15"}
res = requests.get(url, headers=headers)
try:
    res.raise_for_status()
except requests.exceptions.HTTPError as err:
    print(err)

现在我们有一个有效的响应。它看起来像什么?浏览器渲染的实际HTML被包含在响应的content 。我们只看一下它的开头。这只是一个标准的HTML文档:

res.content[:50]
b'<!DOCTYPE html><html data-color-theme="light" id="'

现在,我如何读出搜刮来的数据中的HTML?

你的网络浏览器接收这个html并将其渲染成一个漂亮的网页。该页面将有完整的广告、表格、颜色和应用于视觉元素的样式。你只是想抓取下面的原始数据。为了做到这一点,我们需要对html进行解析。你可以使用BeautifulSoup来代替你自己编写的解析器,这是一个解析html的库,并提供有用的方法来从中提取你想要的东西。用pip install beautifulsoup4 安装它。你还需要安装 lxml -pip install lxml

from bs4 import BeautifulSoup

soup = BeautifulSoup(res.content)

现在,在我们开始尝试从汤中选择表格和数据之前,在你的网络浏览器中查看页面可能会有帮助,比如Firefox、Safari或Chrome,右击表格并选择 "检查元素 "或 "inpect "选项,前提是你启用了开发者工具。这将使你能够看到html文档的结构和表格本身。

在这种情况下,我们只有一个表(在写这篇文章的时候,雅虎总是可以改变一些东西!),所以我们将尝试从汤中选择它。select 方法将返回一个页面中所有table 元素的列表:

len(soup.select("table"))
1

现在我们已经确认只有一个,让我们看看我们是否能从表中获得标题 (th) 和数据行 (tr) 。

table = soup.select("table")[0]
columns = []
for th in table.select("th"):
    columns.append(th.text)
columns
['Symbol', 'Company', 'Earnings Date', 'EPS Estimate', 'Reported EPS', 'Surprise(%)']

现在我们来抓取这些行。我们只是循环浏览每个表格行(tr),然后每个数据元素(td),并制作一个列表:

data = []
for tr in table.select("tr"):
    row = []
    for td in tr.select("td"):
        row.append(td.text)
    if len(row):
        data.append(row)
# first and last row and length of table
data[0], data[-1], len(data)    
(['AAPL', 'Apple Inc', 'Oct 26, 2022, 4 PMEDT', '-', '-', '-'],
 ['AAPL', 'Apple Inc.', 'Jan 15, 1997, 12 AMEST', '-0.02', '-0.03', '-48.31'],
 100)

我们有六列数据:

  • 符号
  • 公司名称
  • 一个有奇怪的畸形时区的日期
  • 每股收益(EPS)估计值
  • EPS报告值
  • 一个叫做惊喜的百分比--与估计值相比,收益有多高或多低。

让我们做一个DataFrame

df = pd.DataFrame(data, columns=columns)
df.head()
  Symbol     Company           Earnings Date EPS Estimate Reported EPS  \
0   AAPL   Apple Inc   Oct 26, 2022, 4 PMEDT            -            -   
1   AAPL   Apple Inc   Jul 25, 2022, 4 PMEDT            -            -   
2   AAPL   Apple Inc   Apr 26, 2022, 4 PMEDT         1.43            -   
3   AAPL  Apple Inc.  Jan 27, 2022, 11 AMEST         1.89          2.1   
4   AAPL  Apple Inc.  Oct 28, 2021, 12 PMEDT         1.24         1.24   

  Surprise(%)  
0           -  
1           -  
2           -  
3      +11.17  
4       +0.32  

使用pandas清理数据

在这一点上,我们只想做一点数据清理。由于此时所有的东西都只是文本,我们需要首先将东西转换为正确的数据类型。如果你有兴趣学习更多关于数据转换的知识,请查看这篇文章coerce首先,让我们把所有的数值都变成数字,如果没有数据(比如未来的日期),我们就通过设置错误来把它们设置为NaN

for column in ['EPS Estimate', 'Reported EPS', 'Surprise(%)']:
    df[column] = pd.to_numeric(df[column], errors='coerce')

现在,Earnings Date 列有点奇怪,因为它里面有一个时区感知的日期时间。我碰巧知道,AAPL总是在东部时间16:00收市后公布收益。历史上的收益时间并不准确,只是日期。但是,让我们假设我们想把这些转换为日期时间对象,而不仅仅是日期。我们需要将其转化为一种可以被pd.to_datetime 的格式。我们现在所做的并不奏效。

try:
    pd.to_datetime(df['Earnings Date'])
except Exception as ex:
    print(ex)
Unknown string format: Oct 26, 2022, 4 PMEDT

现在,理想的情况是,我们可以通过传入格式来解析这个数据(使用这个不错的参考)。我可以尝试用AM/PM指标和时区连接起来,让我们看看这是否有效。

try:
    pd.to_datetime(df['Earnings Date'], format='%b %d, %Y, %I %p%Z')
except Exception as ex:
    print(ex)
time data 'Oct 26, 2022, 4 PMEDT' does not match format '%b %d, %Y, %I %p%Z' (match)

EDT/EST不是一个完整的时区名称,不会被to_datetime (或甚至直接使用datetime.datetime.strptime )解析。既然我们知道数值是在美国东部时区,我们就可以直接设置它。我们将从原始数据中删除时区,将其解析为一个日期时间,然后设置时区。

注意,我使用str 访问器来进行字符串替换操作,因为该字段开始时是一个字符串。datetime 然后,当它被转换为pd.to_datetime ,我使用dt 访问器来进行时区定位。

# remove the timezone part of the date
df['Earnings Date'] = df['Earnings Date'].str.replace("EDT|EST", "", regex=True)
df['Earnings Date'] = pd.to_datetime(df['Earnings Date'])

# set the timezone manually
import pytz
eastern = pytz.timezone('US/Eastern')
df['Earnings Date'] = df['Earnings Date'].dt.tz_localize(eastern)

df.head()
  Symbol     Company             Earnings Date  EPS Estimate  Reported EPS  \
0   AAPL   Apple Inc 2022-10-26 16:00:00-04:00           NaN           NaN   
1   AAPL   Apple Inc 2022-07-25 16:00:00-04:00           NaN           NaN   
2   AAPL   Apple Inc 2022-04-26 16:00:00-04:00          1.43           NaN   
3   AAPL  Apple Inc. 2022-01-27 11:00:00-05:00          1.89          2.10   
4   AAPL  Apple Inc. 2021-10-28 12:00:00-04:00          1.24          1.24   

   Surprise(%)  
0          NaN  
1          NaN  
2          NaN  
3        11.17  
4         0.32  

总结

在这个例子中,我们首先尝试使用pandas从一个网页中获取数据,然后在设置了User-Agent 头之后,使用request库来获取数据。然后我们使用BeautifulSoup来解析html,提取一个表格。最后,我们用pandas对数据进行了清理。

在这一点上,有必要给你一些警告。首先,这个方法在很多网站上都不可行。在这种情况下,雅虎只是阻止了使用自动软件下载数据的明显企图。这项技术对那些有以下情况的网站不起作用:

  1. 要求认证,你将需要对你的请求进行认证。这可能很容易做到,也可能不容易,取决于认证方法。
  2. 使用JavaScript进行渲染,如果一个网站是用JavaScript渲染的,你的请求将只是返回原始JavaScript代码。由于这个原因,表格(和数据)将不会出现在响应中。有可能刮取JavaScript网站,但这些方法更复杂,涉及到运行一个浏览器实例。
  3. 积极地阻止自动代码,网站可以选择阻止任何看起来不像是真正的浏览器的用户代理。网站有很多方法可以做到这一点,所以你可能会发现这种技术不起作用。

在上述所有的基础上,你应该做一个好的网络公民。不要从一个网站积极下载,即使他们没有阻止你。大多数网站期望处理典型的人类用户,他们会缓慢地穿越一个网站。你至少应该像人类一样访问该网站。这意味着访问速度要慢,在获取数据之间要有长时间的停顿。

最后,需要注意的是,网站会经常改变其格式、设计和布局。这可能会破坏你写的任何下载数据的代码。出于这个原因,关键的代码和基础设施应该依靠支持的API。

希望你现在对如何从一个基本的网站检索、解析和清理数据有了更好的理解。