最近在做一个空气质量分析的小项目,需要获取历史数据。调研发现aqistudy.cn这个网站的数据比较全面,但直接抓取接口会遇到加密参数的问题。经过一番折腾,最终选择了用Selenium模拟浏览器操作的方式来获取数据,效果很不错。
这种方法的核心思路是:让浏览器自己执行JS渲染页面,我们只需要等页面加载完成后"抄"数据就行。相比直接破解加密接口,这种方式更稳定可靠,特别适合像我这样不擅长逆向工程的同学。
首先需要安装Python的Selenium库:
bash复制pip install selenium
建议使用虚拟环境来管理依赖,避免污染全局环境。我习惯用venv:
bash复制python -m venv env
source env/bin/activate # Linux/Mac
env\Scripts\activate # Windows
Selenium需要浏览器驱动才能工作。以Chrome为例:
chrome://version/注意:Chrome和ChromeDriver的版本必须严格匹配,否则会报错。建议使用稳定版而非测试版。
python复制from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def init_browser(headless=True):
options = Options()
if headless:
options.add_argument('--headless') # 无头模式
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')
driver = webdriver.Chrome(options=options)
return driver
这里有几个实用技巧:
--disable-gpu可以避免一些渲染问题--no-sandbox在Linux服务器上可能需要完整的抓取流程如下:
python复制def fetch_aqi_data(city, month, driver):
# 1. 打开目标页面
driver.get('https://www.aqistudy.cn/historydata/daydata.php')
# 2. 选择城市
city_select = driver.find_element(By.ID, 'city')
Select(city_select).select_by_visible_text(city)
# 3. 选择月份
month_select = driver.find_element(By.ID, 'month')
Select(month_select).select_by_visible_text(month)
# 4. 点击查询
query_btn = driver.find_element(By.ID, 'query')
query_btn.click()
# 5. 等待数据加载
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, 'table'))
)
# 6. 解析表格数据
table = driver.find_element(By.CLASS_NAME, 'table')
rows = table.find_elements(By.TAG_NAME, 'tr')
data = []
for row in rows[1:]: # 跳过表头
cols = row.find_elements(By.TAG_NAME, 'td')
data.append({
'date': cols[0].text,
'aqi': cols[1].text,
'pm25': cols[3].text,
# 其他字段...
})
return data
获取到数据后,建议保存为CSV格式:
python复制import csv
def save_to_csv(data, filename):
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
Selenium提供了多种元素定位方式,各有优劣:
| 定位方式 | 示例 | 适用场景 | 稳定性 |
|---|---|---|---|
| ID定位 | find_element(By.ID, 'query') |
唯一元素 | ★★★★★ |
| CSS选择器 | find_element(By.CSS_SELECTOR, '.table tr') |
复杂选择 | ★★★★ |
| XPath | find_element(By.XPATH, '//table[@class="table"]') |
灵活定位 | ★★★ |
| 类名 | find_element(By.CLASS_NAME, 'table') |
简单选择 | ★★★★ |
经验:优先使用ID定位,其次是CSS选择器。XPath虽然强大但容易受页面结构调整影响。
页面加载需要时间,必须使用显式等待:
python复制from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 等待最多10秒,直到表格出现
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, 'table'))
)
常见的等待条件:
presence_of_element_located:元素出现在DOM中visibility_of_element_located:元素可见element_to_be_clickable:元素可点击网站可能有反爬机制,可以尝试以下方法:
time.sleep(random.uniform(1, 3))python复制options.add_argument('user-agent=Mozilla/5.0...')
python复制options.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2})
python复制from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
import csv
import time
def get_aqi_data(cities, months, output_file):
driver = init_browser()
all_data = []
try:
for city in cities:
for month in months:
print(f"正在获取 {city} {month} 的数据...")
data = fetch_aqi_data(city, month, driver)
all_data.extend(data)
time.sleep(2) # 礼貌性延迟
save_to_csv(all_data, output_file)
finally:
driver.quit()
if __name__ == '__main__':
cities = ['北京', '上海', '广州']
months = ['2023-01', '2023-02', '2023-03']
get_aqi_data(cities, months, 'aqi_data.csv')
错误信息:
code复制SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version...
解决方案:
webdriver-manager自动管理驱动版本:python复制from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(ChromeDriverManager().install())
可能原因:
python复制driver.switch_to.frame('iframe_id')
python复制driver.execute_script("arguments[0].click();", element)
解决方法:
try-finally确保浏览器退出python复制options.add_argument('--disable-dev-shm-usage')
我在实际项目中发现,这种方法的稳定性比直接调用API要高很多。虽然速度稍慢,但对于学术研究和小规模数据分析完全够用。