互联网上的数据量是相当惊人的。在一个网站上进行快速搜索并点击查看数据通常是很容易的。然而,如果你想在你的分析中实际使用这些数据,你必须能够获取它并将其转换为可用的格式。然而,网站的创建者和所有者可能不希望你这样做。他们可能更喜欢你只看数据,以及其周围的广告。你想使用这些数据进行分析的事实本身就使其具有价值。数据提供者很可能从你看数据时查看的广告中赚钱。他们甚至可能向你收取查看数据本身的费用。出于这个原因,他们有动力阻止你去获取数据。
在这篇文章中,我将向你展示一种非常基本的下载(或刮取)数据的方法,当最简单的方法可能不起作用。它并不是在每一种情况下都有效,但你可以把它加入你的工具箱,在你需要使用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是如何做到这一点的基本原理。这段代码有点复杂,但请随意阅读,但基本上它做了以下事情:
- 使用一个分析器获取原始HTML
urllib - 使用一个解析器来解析原始html,然后获取所有的表格
- 将这些表格变成
DataFrames - 处理上述所有步骤的大量选项,包括使用不同的分析器和选项来创建表格。
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对数据进行了清理。
在这一点上,有必要给你一些警告。首先,这个方法在很多网站上都不可行。在这种情况下,雅虎只是阻止了使用自动软件下载数据的明显企图。这项技术对那些有以下情况的网站不起作用:
- 要求认证,你将需要对你的请求进行认证。这可能很容易做到,也可能不容易,取决于认证方法。
- 使用JavaScript进行渲染,如果一个网站是用JavaScript渲染的,你的请求将只是返回原始JavaScript代码。由于这个原因,表格(和数据)将不会出现在响应中。有可能刮取JavaScript网站,但这些方法更复杂,涉及到运行一个浏览器实例。
- 积极地阻止自动代码,网站可以选择阻止任何看起来不像是真正的浏览器的用户代理。网站有很多方法可以做到这一点,所以你可能会发现这种技术不起作用。
在上述所有的基础上,你应该做一个好的网络公民。不要从一个网站积极下载,即使他们没有阻止你。大多数网站期望处理典型的人类用户,他们会缓慢地穿越一个网站。你至少应该像人类一样访问该网站。这意味着访问速度要慢,在获取数据之间要有长时间的停顿。
最后,需要注意的是,网站会经常改变其格式、设计和布局。这可能会破坏你写的任何下载数据的代码。出于这个原因,关键的代码和基础设施应该依靠支持的API。
希望你现在对如何从一个基本的网站检索、解析和清理数据有了更好的理解。