使用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和内存占用 - 无请求节流机制:连续高频请求不仅容易触发网站反爬,还会加剧设备瞬时负载
替代方案一:修复多进程+复用浏览器实例
优化要点
- 合理设置进程数:根据CPU核心数限制进程数量(比如
min(os.cpu_count(),4)),避免进程过多抢占资源 - 进程内复用浏览器:每个进程启动一次Chrome,处理多个任务,彻底消除重复启动浏览器的开销
- 添加随机延迟:降低请求频率,避免触发反爬,同时平摊设备负载
优化后核心代码片段
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
相关产品推荐
相关产品推荐

