You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Multiprocessing爬取HKEX数据CPU占用过高,寻求替代方案

问题:HKEX股票爬取性能优化需求

我尝试爬取HKEX网站的多只股票数据,采用multiprocessing方案后仍耗时数分钟,还常因CPU占用过高导致设备崩溃,寻求可行替代方案。
附注:股票代码存储于S:/Model/JT/Tickers.xlsx,原实现代码如下:

import multiprocessing
import csv
from bs4 import BeautifulSoup
import pandas as pd
import os, time, random
import numpy as np
import datetime
import locale
import openpyxl
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
lastBusDay = datetime.datetime.today()
shift = datetime.timedelta(max(1, (lastBusDay.weekday() + 6) % 7 - 3))
lastBusDay = lastBusDay - shift
lastBusDay.strftime('%Y-%m-%d')

LBD = str(lastBusDay)
LBD = LBD[0:10]

shift = datetime.timedelta(max(1, (lastBusDay.weekday() + 6) % 7 - 3))
lastBusDay = lastBusDay - shift
LBD1 = str(lastBusDay)
LBD1 = LBD1[0:10]

t = LBD1
tm1 = LBD
t_list = list(t)
t_list[4] = "/"
t_list[7] = "/"
t = ''.join(t_list)
tm1_list = list(tm1)
tm1_list[4] = "/"
tm1_list[7] = "/"
tm1 = ''.join(tm1_list)

t = t.replace('/', '-')
tm1 = tm1.replace('/', '-')

filename = "S:/Model/JT/Tickers.xlsx"
df = pd.read_excel(filename, engine='openpyxl')

# Extract data from the first column and convert it to a list
tickers = df.iloc[:, 0].tolist()


def get_ccass(ticker, date):
    service = Service(executable_path=r"S:/Model/JT/chromedriver.exe")
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')
    # Initialize Chrome driver
    driver = webdriver.Chrome(service=service, options=options)
    #driver = webdriver.Chrome(r"S:/Lily/chromedriver.exe", options=options)
    # Navigate to website URL
    driver.get('https://www3.hkexnews.hk/sdw/search/searchsdw.aspx')

    # Wait for page to load fully
    driver.implicitly_wait(5)
    # Find textbox and input number
    input_box = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "txtStockCode")))
    input_box.send_keys(ticker)

    driver.implicitly_wait(5)

    button_xpath = '//*[@id="onetrust-reject-all-handler"]'
    button = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.XPATH, button_xpath))
    )

    # Scroll the button into view
    driver.execute_script("arguments[0].scrollIntoView();", button)
    # Use JavaScript to click the button
    driver.execute_script("arguments[0].click();", button)

    driver.implicitly_wait(5)
    element = driver.find_element("xpath", '//*[@id="txtShareholdingDate"]')

    # Change the value of the element to 2023/05/20
    driver.execute_script(f"arguments[0].setAttribute('value', '{date}')", element)

    # Locate the button using its XPath
    button_xpath = '//*[@id="btnSearch"]'
    button = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.XPATH, button_xpath))
    )

    # Scroll the button into view
    driver.execute_script("arguments[0].scrollIntoView();", button)

    # Use JavaScript to click the button
    driver.execute_script("arguments[0].click();", button)

    # Wait for new page to load
    driver.implicitly_wait(3)
    value_element = driver.find_element(By.CSS_SELECTOR,
                                        '#pnlResultSummary > div > div.ccass-search-datarow.ccass-search-total > div.shareholding > div.value')
    value = value_element.text
    driver.quit()
    return value


def mp_worker(data):
    data_t = get_ccass(data, t)
    data_t = float(data_t.replace(',', ''))
    data_tm1 = get_ccass(data, tm1)
    data_tm1 = float(data_tm1.replace(',', ''))
    change = (data_tm1 - data_t)
    # format the values using the locale module
    locale.setlocale(locale.LC_ALL, '')  # use the default locale settings
    data_t = locale.format_string('%d', data_t, grouping=True)
    data_tm1 = locale.format_string('%d', data_tm1, grouping=True)
    change = locale.format_string('%d', change, grouping=True)
    return ([data, data_t, data_tm1, change])


def mp_handler(data_list):
    # write the header row to the output csv file
    file_name = 'results_' + tm1 + '.csv'
    exfile_name = 'results_' + tm1 + '.xlsx'
    file_path = os.path.abspath("S:\\Model\\JT\\CCASS results\\"+file_name)
    with open(file_path, 'w') as f:
        f.write(','.join(['ticker', t, tm1, 'change']) + '\n')
    with multiprocessing.Pool(1) as p:
        with open(file_path, 'a') as f:
            writer = csv.writer(f, lineterminator='\n', delimiter=',')
            for result in p.imap_unordered(mp_worker, data_list):
                writer.writerow([result[0], result[1], result[2], result[3]])
    read_file = pd.read_csv(file_path)
    read_file.to_excel("S:\\Model\\JT\\CCASS results\\"+exfile_name, index=None, header=True)
    workbook = openpyxl.load_workbook("S:\\Model\\JT\\CCASS results\\"+exfile_name)
    worksheet = workbook.active
    column_width = 25
    worksheet.column_dimensions['B'].width = column_width
    worksheet.column_dimensions['C'].width = column_width
    worksheet.column_dimensions['D'].width = column_width
    # Save the modified workbook
    workbook.save("S:\\Model\\JT\\CCASS results\\"+exfile_name)
    os.remove(file_path)
if __name__ == '__main__':
    # code to prepare the data list
    data_list = tickers
    mp_handler(data_list)

原代码核心问题

  • 多进程配置无效:multiprocessing.Pool(1)仅启用单进程,完全浪费了多进程的并行能力,还额外增加进程调度开销
  • 资源浪费极端:每个get_ccass调用都新建/销毁Chrome实例,浏览器启动是高负载操作,直接拉高CPU和内存占用
  • 无请求节流机制:连续高频请求不仅容易触发网站反爬,还会加剧设备瞬时负载

替代方案一:修复多进程+复用浏览器实例

优化要点

  1. 合理设置进程数:根据CPU核心数限制进程数量(比如min(os.cpu_count(),4)),避免进程过多抢占资源
  2. 进程内复用浏览器:每个进程启动一次Chrome,处理多个任务,彻底消除重复启动浏览器的开销
  3. 添加随机延迟:降低请求频率,避免触发反爬,同时平摊设备负载

优化后核心代码片段

import multiprocessing
from selenium import webdriver
# 其他导入省略...

# 每个进程维护一个全局driver实例
driver = None

def init_driver():
    """进程初始化时启动浏览器"""
    global driver
    service = Service(executable_path=r"S:/Model/JT/chromedriver.exe")
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')
    options.add_argument('--disable-gpu')
    options.add_argument('--no-sandbox')  # 减少内存占用
    driver = webdriver.Chrome(service=service, options=options)
    driver.get('https://www3.hkexnews.hk/sdw/search/searchsdw.aspx')
    # 提前处理cookie弹窗
    try:
        button = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.XPATH, '//*[@id="onetrust-reject-all-handler"]'))
        )
        driver.execute_script("arguments[0].click();", button)
    except:
        pass

def get_ccass(ticker, date):
    """复用已有浏览器实例获取数据"""
    global driver
    # 重置输入框
    driver.execute_script("document.getElementById('txtStockCode').value = '';")
    input_box = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "txtStockCode")))
    input_box.send_keys(ticker)
    
    # 设置日期
    element = driver.find_element("xpath", '//*[@id="txtShareholdingDate"]')
    driver.execute_script(f"arguments[0].setAttribute('value', '{date}')", element)
    
    # 触发搜索
    button = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.XPATH, '//*[@id="btnSearch"]'))
    )
    driver.execute_script("arguments[0].click();", button)
    
    # 等待结果加载
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, '#pnlResultSummary > div > div.ccass-search-datarow.ccass-search-total > div.shareholding > div.value'))
    )
    value_element = driver.find_element(By.CSS_SELECTOR,
                                        '#pnlResultSummary > div > div.ccass-search-datarow.ccass-search-total > div.shareholding > div.value')
    value = value_element.text
    # 添加随机延迟
    time.sleep(random.uniform(0.5, 1.5))
    return value

def mp_handler(data_list):
    # 其他代码不变...
    # 初始化进程池时启动浏览器
    with multiprocessing.Pool(min(os.cpu_count(),4), initializer=init_driver) as p:
        with open(file_path, 'a') as f:
            writer = csv.writer(f, lineterminator='\n', delimiter=',')
            for result in p.imap_unordered(mp_worker, data_list):
                writer.writerow([result[0], result[1], result[2], result[3]])
    # 其他代码不变...

替代方案二:改用多线程+requests(无浏览器)

核心思路

直接调用HKEX的搜索接口(通过抓包获取POST参数),完全抛弃浏览器,资源占用骤降,速度提升数倍

实现代码

import requests
import concurrent.futures
import pandas as pd
import os
import datetime
import locale
from bs4 import BeautifulSoup

# 日期处理逻辑与原代码一致,省略...

filename = "S:/Model/JT/Tickers.xlsx"
df = pd.read_excel(filename, engine='openpyxl')
tickers = df.iloc[:, 0].tolist()

def get_ccass_via_requests(ticker, date):
    """通过requests直接调用接口获取数据"""
    session = requests.Session()
    # 先访问首页获取必要的ViewState和Cookie
    url = 'https://www3.hkexnews.hk/sdw/search/searchsdw.aspx'
    response = session.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')
    
    # 提取表单验证参数
    viewstate = soup.find('input', {'id': '__VIEWSTATE'}).get('value')
    event_validation = soup.find('input', {'id': '__EVENTVALIDATION'}).get('value')
    
    # 构造POST请求参数
    data = {
        '__VIEWSTATE': viewstate,
        '__EVENTVALIDATION': event_validation,
        'txtStockCode': ticker,
        'txtShareholdingDate': date,
        'btnSearch': 'Search'
    }
    
    # 发送请求并解析结果
    response = session.post(url, data=data)
    soup = BeautifulSoup(response.text, 'html.parser')
    value_element = soup.select_one('#pnlResultSummary > div > div.ccass-search-datarow.ccass-search-total > div.shareholding > div.value')
    return value_element.text if value_element else '0'

def worker(ticker):
    """单股票数据处理逻辑"""
    data_t = get_ccass_via_requests(ticker, t)
    data_t = float(data_t.replace(',', ''))
    data_tm1 = get_ccass_via_requests(ticker, tm1)
    data_tm1 = float(data_tm1.replace(',', ''))
    change = (data_tm1 - data_t)
    
    locale.setlocale(locale.LC_ALL, '')
    data_t = locale.format_string('%d', data_t, grouping=True)
    data_tm1 = locale.format_string('%d', data_tm1, grouping=True)
    change = locale.format_string('%d', change, grouping=True)
    return [ticker, data_t, data_tm1, change]

def main():
    file_name = 'results_' + tm1 + '.xlsx'
    file_path = os.path.abspath(f"S:\\Model\\JT\\CCASS results\\{file_name}")
    
    # 多线程并行处理
    with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
        results = list(executor.map(worker, tickers))
    
    # 写入Excel并设置列宽
    df_result = pd.DataFrame(results, columns=['ticker', t, tm1, 'change'])
    df_result.to_excel(file_path, index=None, header=True)
    
    from openpyxl import load_workbook
    workbook = load_workbook(file_path)
    worksheet = workbook.active
    column_width = 25
    for col in ['B', 'C', 'D']:
        worksheet.column_dimensions[col].width = column_width
    workbook.save(file_path)

if __name__ == '__main__':
    main()

替代方案三:异步请求(最快方案)

使用aiohttp实现异步HTTP请求,单线程即可处理数百个并发请求,CPU和内存占用极低,适合大量股票的爬取场景

核心优势

  • 资源占用仅为多进程/多线程方案的1/5~1/10
  • 爬取速度比多线程方案快2~3倍

内容的提问来源于stack exchange,提问作者testing testing

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 04:23:14