在网站上查找隐藏的API - 以flightaware.com为例(+使用该数据创建Plotly动画图表)

The idea is that you don't always need Selenium or BeautifulSoup to scrape data from websites. You can find references to data sources in plain HTML.

The problem with scraping is that HTML selectors are hard to pinpoint. They are also prone to changing if the interface changes and if identifiers such as IDs and classes are dynamically generated.

Today I will show you an alternative approach. Of course, this approach doesn't work all the time, it depends on the site in question. Many sites protect their data sources and offer paid access through official APIs. Some however do not, either by negligence or because they thought nobody would ever look.

Let's explore one of the examples - Flightaware.com. Normally, the flight information data that the site provides is hidden behind a paywall, but is accessible in the processed form on the main page.

If you scroll down the main page, you will see an interactive map of flights in your area.

If you click on one of the planes, you will be taken to the flight information page. The data it offers although informative but is very limited. Some of it very much scrapable with the aforementioned tools, some of it is abstracted away in charts and cannot be accessed programatically.

What if we explore the HTML source code of the page?

Scrolling through the DOM, we can see some TOKEN references, references to the template engine...

And unexpectedly, a long string of what looks like JSON data stored in a variable trackpollBootstrap. Interesting!

Let's look at it in Python.

python 复制代码
import requests
import re
import json

# FLIGHT_NUMBER = "CBJ606"
# FLIGHT_NUMBER = "TYA3422"
FLIGHT_NUMBER = "ETD826"


def get_flight_data(flight_id: str) -> dict:
    try:
        url = f"https://flightaware.com/live/flight/{flight_id}"

        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36"
        }
        page_source = requests.get(url, headers=headers).text

        scripts = re.findall(r"<script>(.*)</script>", page_source)

        script = scripts[-1].strip(";").removeprefix("var trackpollBootstrap = ")

        data = json.loads(script)

        return data

    except Exception as e:
        print(e)


data = get_flight_data(FLIGHT_NUMBER)

We will use ETD826 flight ID in this tutorial. You can try other IDs as well.

First, we prepare the target URL with the following structure: url = f"https://flightaware.com/live/flight/{flight_id}".

Second, we prepare some headers so as to pretend we are a real browser and get the source HTML of the page.

Because, the data lives in one of the <script> tags, we search for all the script tags with RegEx. This gives us the contents of those tags:
scripts = re.findall(r"<script>(.*)</script>", page_source)

The target script is actually the last one in the list. We process it as text, stripping all of the JavaScript bits:
script = scripts[-1].strip(";").removeprefix("var trackpollBootstrap = ")

And finally load it as JSON: data = json.loads(script).

What we are looking for is the flights key. It contains all flight information, aircraft and airline information, origin and destination information, even the amount of fuel burned in flight.

It also contains the track information - the coordinates of all the checkpoints on the map.

Let's use this data to recreate the flight path.

In order to get to the track data, we need to traverse the data dictionary, which requires 3 steps.

python 复制代码
track_data = data["flights"]["ETD826-1736484856-schedule-194p:0"]["track"]

I discovered that this the key ETD826-1736484856-schedule-194p:0 is dynamic and changes after each new flight, but we need to know the value of this key in order to get to the track data.

We can use the following expression in place of the key, which will get us to the track data, even if the value changes. It simply returns the next child value in the dictionary tree.

python 复制代码
track_data = data["flights"][next(iter(data["flights"]))]["track"]

Here we have the coordinates of the flight path on the route, the speed and altitude (I assume in the last flight) and the time of passing of each waypoint.

Let's load this data into Pandas.

python 复制代码
import pandas as pd

df = pd.DataFrame(track_data)

df

Let's separate the coordinates into latitude and longitude. First, we represent the coords column as string, because for some reason it was loaded as float datatype. Second, we strip the brackets and, finally, split the column into two. We also drop the coords column, as it is no longer needed.

python 复制代码
df["coord"] = df["coord"].map(repr)

df["coord"] = df["coord"].str.replace("[", "").str.replace("]", "")

df[["lon", "lat"]] = df["coord"].str.split(", ", expand=True)
df = df.drop("coord", axis=1)

df

Let's plot the data.

python 复制代码
import plotly.graph_objects as go
import pandas as pd

fig = go.Figure()

fig.add_trace(
    go.Scattergeo(
        locationmode="country names",
        lon=df["lon"],
        lat=df["lat"],
        hoverinfo="text",
        text=df["alt"],
        mode="lines",
        line=dict(width=3, color="red"),
    )
)

fig.update_layout(
    title_text=f"Flight path for {FLIGHT_NUMBER}",
    showlegend=False,
)

fig.show()

Let's make it better, add animation and some information!

python 复制代码
origin = data["flights"][next(iter(data["flights"]))]["activityLog"]["flights"][0][
    "origin"
]["friendlyLocation"].upper()
destination = data["flights"][next(iter(data["flights"]))]["activityLog"]["flights"][0][
    "destination"
]["friendlyLocation"].upper()

print(origin, destination)

Here, we get origin and destination cities.

python 复制代码
>>> ABU DHABI, AE SEOUL, SOUTH KOREA
python 复制代码
import plotly.graph_objects as go


def modify_t(i):
    t2 = t.copy()
    if 0 < i < df.shape[0]:
        t2[i - 1] = f"<br>FL: {df.at[i, 'alt']}<br>Speed: {df.at[i, 'gs']}"
    return t2


t = ["<br>" + origin] + ["" for j in range(df.shape[0] - 2)] + ["<br>" + destination]
c = ["red"] + ["white" for j in range(df.shape[0] - 2)] + ["red"]

fig = go.Figure(
    data=[
        go.Scattergeo(
            locationmode="country names",
            line=dict(width=3, color="rgba(255, 0, 0, 0.25)"),
            textfont=dict(size=12, color=c),
            textposition="bottom center",
            mode="lines+text",
        ),
    ],
    layout=go.Layout(
        updatemenus=[
            dict(
                type="buttons",
                buttons=[
                    dict(
                        label="▶",
                        method="animate",
                        args=[
                            None,
                            {
                                "frame": {"duration": 1},
                                "transition": {
                                    "duration": 1,
                                },
                            },
                        ],
                    )
                ],
                direction="left",
                pad={"r": 0, "t": 0},
                x=0.1,
                xanchor="right",
                y=1.05,
                yanchor="top",
            )
        ],
    ),
    frames=[
        go.Frame(
            data=[
                go.Scattergeo(
                    lon=df["lon"][:i],
                    lat=df["lat"][:i],
                    text=modify_t(i),
                ),
            ]
        )
        for i in range(df.shape[0] - 1)
    ]
    + [
        go.Frame(
            data=[
                go.Scattergeo(
                    lon=df["lon"],
                    lat=df["lat"],
                    text=t,
                ),
            ]
        )
    ],
)

fig.add_annotation(
    text="* FL is Flight Level in hundreds of Feet<br>   Speed is Ground Speed in Knots",
    xref="paper",
    yref="paper",
    x=0.94,
    y=0.00,  # 0.89, 0.1
    font=dict(color="LightGrey"),
    showarrow=False,
    align="left",
)

fig.update_layout(
    width=800,
    height=400,
    margin={"r": 0, "t": 50, "l": 0, "b": 50},
)
fig.update_layout(
    paper_bgcolor="#171717",
    title_text=f"Flight path for {FLIGHT_NUMBER}",
    title_font=dict(color="LightGrey"),
    title_x=0.12,
    title_y=0.89,
    showlegend=False,
)
center = int(df.shape[0] / 2)
fig.update_geos(
    projection_type="orthographic",
    bgcolor="#171717",
    projection_rotation={
        "lat": float(df.at[center, "lat"]),
        "lon": float(df.at[center, "lon"]),
        "roll": 0,
    },
    resolution=110,  # 50或110
    showcoastlines=False,
    showland=True,
    landcolor="#242424",
    showocean=True,
    oceancolor="#171717",
)
config = {"displayModeBar": False, "responsive": False, "staticPlot": True}
fig.show(config=config)

Plotly动画图表

Plotly动画图表TYA3422-1

python 复制代码
import plotly.graph_objects as go


def modify_t(i):
    t2 = t.copy()
    if 0 < i < df.shape[0]:
        t2[i - 1] = f"<br>FL: {df.at[i, 'alt']}<br>Speed: {df.at[i, 'gs']}"
    return t2


t = ["<br>" + origin] + ["" for j in range(df.shape[0] - 2)] + ["<br>" + destination]
c = ["red"] + ["white" for j in range(df.shape[0] - 2)] + ["red"]

fig = go.Figure(
    data=[
        go.Scattergeo(
            locationmode="country names",
            line=dict(width=3, color="rgba(255, 0, 0, 0.25)"),
            textfont=dict(size=12, color=c),
            textposition="bottom center",
            mode="lines+text",
        ),
    ],
    layout=go.Layout(
        updatemenus=[
            dict(
                type="buttons",
                buttons=[
                    dict(
                        label="▶",
                        method="animate",
                        args=[
                            None,
                            {
                                "frame": {"duration": 1},
                                "transition": {
                                    "duration": 1,
                                },
                            },
                        ],
                    )
                ],
                direction="left",
                pad={"r": 0, "t": 0},
                x=0.1,
                xanchor="right",
                y=0.91,
                yanchor="top",
            )
        ],
    ),
    frames=[
        go.Frame(
            data=[
                go.Scattergeo(
                    lon=df["lon"][:i],
                    lat=df["lat"][:i],
                    text=modify_t(i),
                ),
            ]
        )
        for i in range(df.shape[0] - 1)
    ]
    + [
        go.Frame(
            data=[
                go.Scattergeo(
                    lon=df["lon"],
                    lat=df["lat"],
                    text=t,
                ),
            ]
        )
    ],
)

fig.add_annotation(
    text="* FL is Flight Level in hundreds of Feet<br>   Speed is Ground Speed in Knots",
    xref="paper",
    yref="paper",
    x=0.94,
    y=0.1,
    font=dict(color="LightGrey"),
    showarrow=False,
    align="left",
)

fig.update_layout(width=800, height=400, margin={"r": 0, "t": 0, "l": 0, "b": 0})
fig.update_layout(
    title_text=f"Flight path for {FLIGHT_NUMBER}",
    title_font=dict(color="LightGrey"),
    title_x=0.12,
    title_y=0.89,
    showlegend=False,
)
fig.update_geos(
    resolution=110,  # 50或110
    showcoastlines=False,
    showland=True,
    landcolor="#242424",
    showocean=True,
    oceancolor="#171717",
)
config = {"displayModeBar": False, "responsive": False, "staticPlot": True}
fig.show(config=config)

Plotly动画图表

Plotly动画图表TYA3422-2

You can go a step further and store this data in a database easily. Please refer to my other tutorial on the matter: 在Python中从API数据自动创建数据库模式

谢谢阅读!

相关推荐
在世修行2 小时前
智学文枢:页面切换与跨页数据预填
python·桌面应用·signal·1024程序员节·pyside·跨页跳转·状态预填
2601_962885723 小时前
如何用 Python 把 K 线重采样成任意周期?
开发语言·python
打工仔折腾 AI3 小时前
在 Ubuntu ARM64 部署 Q1 V3.0 MuJoCo 模型:加载、单步推理与执行器响应解析
linux·运维·人工智能·python·深度学习·ubuntu·性能优化
信誓旦旦的程序猿4 小时前
【Python量化实战宝典 #21】北向资金跟随手册:聪明钱择时信号(Python 可跑)
python·1024程序员节·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
严谨的麻辣烫4 小时前
静态 ISP 代理请求超时怎么排查?Python 与 curl 实战
python·http·requests·1024程序员节·网络排障
KeepRobots6 小时前
11-01 温哥华不回拨:用 60 行 Python 查你的工单系统会不会差一小时(KeepRobots 工程笔记)
python
xiao77mwq6 小时前
你以为 OCR 慢?实测 81.3 s 里全是故障代价:如何把「定格态」消掉,让耗时从 6.5 s 到 13 倍差距显出原形
python
深圳市益普科技有限公司6 小时前
半导体良率管理YMS:良率与设备参数的相关性,先拉哪几组数据
人工智能·python·算法
“AI国潮设计-小江”6 小时前
【AIGC实战】Python+SDXL打造“潮汕英歌舞IP”万能立牌插件,批量生成“发财元宝蛋糕”视觉营销方案(附源码)
开发语言·人工智能·python·prompt·aigc
砚底藏山河7 小时前
python量化实操:冷热分层存储
java·开发语言·python·金融