2025, Dec 02 15:02

Как парсить формы CoCoRaHS: правильный POST, __VIEWSTATE и рабочий пример

Разбираем, почему парсинг CoCoRaHS ломается без точного POST‑payload, и показываем рабочий код на Python (requests, BeautifulSoup, pandas) с __VIEWSTATE.

Сбор данных со страниц, управляемых формами, часто срывается не из‑за парсинга, а из‑за того, что POST‑запрос несет не тот набор данных, который реальная страница отправляет. Достаточно малейшего расхождения в значении чекбокса или опции выпадающего списка — и сервер вернет другой HTML, а значит, парсить будет нечего. Ниже — краткий разбор реального кейса на cocorahs.org, где поиск таблицы заканчивался RuntimeError, и как корректное выравнивание данных формы решает проблему.

Постановка задачи

Цель — список ежедневных отчетов по осадкам по адресу https://www.cocorahs.org/ViewData/ListDailyPrecipReports.aspx. Скрипт отправляет запрос с идентификатором станции FL-BV-163 и диапазоном дат, затем пытается вытащить таблицу результатов по ее id. В ответ — ошибка «table#ucReportList_ReportGrid not found».

Ниже — минимальный пример, который воспроизводит проблему. Логика программы сохранена, для ясности изменены только имена.

import requests
from bs4 import BeautifulSoup
from requests_html import HTMLSession

import pandas as pd
from io import StringIO

from datetime import datetime

client = requests.Session()

first_resp = client.get('https://www.cocorahs.org/ViewData/ListDailyPrecipReports.aspx')

dom = BeautifulSoup(first_resp.content, "html.parser")
vs_token = dom.find("input", {"name": "__VIEWSTATE", "value": True})["value"]
vsg_token = dom.find("input", {"name": "__VIEWSTATEGENERATOR", "value": True})["value"]
ev_token = dom.find("input", {"name": "__EVENTVALIDATION", "value": True})["value"]

search_resp = client.post('https://www.cocorahs.org/ViewData/ListDailyPrecipReports.aspx', data={
    "__EVENTTARGET": "",
    "__EVENTARGUMENT": "",
    "__LASTFOCUS": "",
    "VAM_Group": "",
    "__VIEWSTATE": vs_token,
    "VAM_JSE": "1",
    "__VIEWSTATEGENERATOR": vsg_token,
    "__EVENTVALIDATION": ev_token,
    "obsSwitcher:ddlObsUnits": "usunits",
    "frmPrecipReportSearch:ucStationTextFieldsFilter:tbTextFieldValue": "FL-BV-163",
    "frmPrecipReportSearch:ucStationTextFieldsFilter:cblTextFieldsToSearch:0": "checked",
    "frmPrecipReportSearch:ucStationTextFieldsFilter:cblTextFieldsToSearch:1": "",
    "frmPrecipReportSearch:ucStateCountyFilter:ddlCountry": "allcountries",
    "frmPrecipReportSearch:ucDateRangeFilter:dcStartDate:di": "6/13/2025",
    "frmPrecipReportSearch:ucDateRangeFilter:dcStartDate:hfDate": "2025-06-13",
    "frmPrecipReportSearch:ucDateRangeFilter:dcEndDate:di": "6/16/2025",
    "frmPrecipReportSearch:ucDateRangeFilter:dcEndDate:hfDate": "2025-06-16",
    "frmPrecipReportSearch:ddlPrecipField": "GaugeCatch",
    "frmPrecipReportSearch:ucPrecipValueFilter:ddlOperator": "LessEqual",
    "frmPrecipReportSearch:ucPrecipValueFilter:tbPrecipValue:tbPrecip": "0.15",
    "frmPrecipReportSearch:btnSearch": "Search",
})

grid = BeautifulSoup(search_resp.content, "html.parser").find("table", id="ucReportList_ReportGrid")

if grid is None:
    raise RuntimeError("table#ucReportList_ReportGrid not found")

frame = pd.read_html(StringIO(str(grid)))[0]

print(frame)

Почему это не работает

Запрос не совпадает с тем, что ожидает сайт. Поля формы для чекбокса текстового поиска по станции, выпадающего списка страны и поля выбора типа осадков имеют конкретные значения, которые сайт отправляет при сабмите. Если передать другие значения, сервер вернет иной ответ, и нужной таблицы в HTML не будет — отсюда RuntimeError.

Конкретно нужно привести к фактическому payload сайта следующие параметры. Чекбокс должен отправлять «on», а не «checked». Выпадающий список страны — «0» вместо «allcountries». Поле выбора типа осадков — «TotalPrecipAmt», а не «GaugeCatch». Кроме того, строку для второго чекбокса текстового поля нужно убрать.

Исправление и рабочий пример

Фрагмент ниже исправляет значения в payload и добавляет заголовок User‑Agent. Остальной сценарий тот же: загрузить стартовую страницу, извлечь служебные токены состояния, отправить поиск, разобрать полученную таблицу и преобразовать её в DataFrame.

import requests
from bs4 import BeautifulSoup
import pandas as pd
from io import StringIO
from datetime import datetime

http_agent = requests.Session()

landing = http_agent.get('https://www.cocorahs.org/ViewData/ListDailyPrecipReports.aspx')

tree = BeautifulSoup(landing.content, "html.parser")
vs_val = tree.find("input", {"name": "__VIEWSTATE", "value": True})["value"]
vsg_val = tree.find("input", {"name": "__VIEWSTATEGENERATOR", "value": True})["value"]
ev_val = tree.find("input", {"name": "__EVENTVALIDATION", "value": True})["value"]

req_headers = {
    'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
}

form_payload = {
    "__EVENTTARGET": "",
    "__EVENTARGUMENT": "",
    "__LASTFOCUS": "",
    "VAM_Group": "",
    "__VIEWSTATE": vs_val,
    "VAM_JSE": "1",
    "__VIEWSTATEGENERATOR": vsg_val,
    "__EVENTVALIDATION": ev_val,
    "obsSwitcher:ddlObsUnits": "usunits",
    "frmPrecipReportSearch:ucStationTextFieldsFilter:tbTextFieldValue": "FL-BV-163",
    "frmPrecipReportSearch:ucStationTextFieldsFilter:cblTextFieldsToSearch:0": "on",
    "frmPrecipReportSearch:ucStateCountyFilter:ddlCountry": "0",
    "frmPrecipReportSearch:ucDateRangeFilter:dcStartDate:di": "6/13/2025",
    "frmPrecipReportSearch:ucDateRangeFilter:dcStartDate:hfDate": "2025-06-13",
    "frmPrecipReportSearch:ucDateRangeFilter:dcEndDate:di": "6/16/2025",
    "frmPrecipReportSearch:ucDateRangeFilter:dcEndDate:hfDate": "2025-06-16",
    "frmPrecipReportSearch:ddlPrecipField": "TotalPrecipAmt",
    "frmPrecipReportSearch:ucPrecipValueFilter:ddlOperator": "LessEqual",
    "frmPrecipReportSearch:ucPrecipValueFilter:tbPrecipValue:tbPrecip": "0.15",
    "frmPrecipReportSearch:btnSearch": "Search"
}

results = http_agent.post('https://www.cocorahs.org/ViewData/ListDailyPrecipReports.aspx', headers=req_headers, data=form_payload)

grid_node = BeautifulSoup(results.content, "html.parser").find("table", id="ucReportList_ReportGrid")

if grid_node is None:
    raise RuntimeError("table#ucReportList_ReportGrid not found")

out_frame = pd.read_html(StringIO(str(grid_node)))[0]

print(out_frame.to_string())

Если хотите пропустить второй проход парсинга HTML, можно читать таблицу напрямую, указав атрибут id в read_html:

out_frame = pd.read_html(StringIO(results.content.decode('utf-8')), flavor='bs4', attrs={'id': 'ucReportList_ReportGrid'})[0]

Почему это важно при работе с динамическими формами

На страницах с формами, особенно там, где используются скрытые поля состояния вроде __VIEWSTATE, важно передавать ровно то, что отправляет браузер. Значения чекбоксов и select‑элементов не всегда очевидны из исходного кода страницы. Практичный путь — открыть инструменты разработчика в браузере, перейти на вкладку Network, поработать с формой, отправить её и посмотреть запрос, чтобы увидеть фактический payload. Воспроизведите эти имена и значения в своём POST без изменений. Так обнаруживаются нюансы вроде того, что чекбокс отправляет «on», а выпадающий список — «0» для конкретного выбора.

Выводы

Если после отправки формы селектор ничего не находит и последующий парсинг падает, сначала проверьте payload. Повторно используйте значения состояния сервера из первоначального GET. Сверяйте значения чекбоксов и списков с тем, что отправляет браузер, а не с тем, что «логично» по статическому HTML. Удаляйте поля, которые сайт не передаёт при выбранных опциях. Когда ваш POST полностью отражает реальную отправку, нужная таблица появляется, и преобразование в DataFrame проходит без проблем.