避开这些坑!用Python爬东方财富股票数据时,90%新手都会遇到的3个问题及解决方案
避开这些坑用Python爬东方财富股票数据时90%新手都会遇到的3个问题及解决方案最近在帮朋友处理一个股票数据分析项目时发现东方财富网的API设计真是让人又爱又恨。爱的是数据全面且更新及时恨的是接口返回的数据格式和字段命名简直像在玩解谜游戏。今天我就来分享三个最常见的坑以及如何优雅地避开它们。1. 如何处理API返回的jQuery包装数据第一次看到东方财富API返回的数据时我差点以为自己的爬虫代码出错了。返回的内容看起来像这样jQuery1124007675389012158473_1703949729655({data:{...}});这种格式叫做JSONPJSON with Padding是为了解决跨域问题而设计的。但对我们爬虫开发者来说它就是个需要额外处理的麻烦。正则表达式处理的正确姿势最直接的方法是用正则表达式去掉前后的包装部分。但这里有个常见的陷阱# 错误示范直接匹配括号会导致转义问题 left_data re.search(r^.*?\(, data).group() data re.sub(left_data, , data)正确的做法应该是# 正确方法分步处理 data response.text # 匹配到第一个左括号前的内容 left_data re.search(r^.*?(?\(), data).group() # 替换掉包装部分 data re.sub(left_data \(, , data) data re.sub(\);, , data)提示这里的(?\()是正向预查确保我们匹配到的是括号前的内容但不包含括号本身。更安全的替代方案如果你对正则表达式不太放心这里有个更稳妥的方法import json def parse_jsonp(jsonp_str): try: # 找到第一个左括号和最后一个右括号的位置 left jsonp_str.find(() 1 right jsonp_str.rfind()) return json.loads(jsonp_str[left:right]) except (ValueError, AttributeError) as e: print(f解析JSONP失败: {e}) return None这种方法不依赖复杂的正则表达式通过简单的字符串操作就能提取出JSON数据再用标准库的json.loads解析既安全又可靠。2. 如何智能判断数据爬取完毕翻页是爬虫开发中的另一个痛点。东方财富的API在翻到最后一页后会返回null但处理这个null值也有讲究。null值处理的正确方式直接使用API返回的null会导致Python抛出NameError因为null在Python中不是关键字。解决方法很简单# 在代码开头定义null变量 null None # 或者 null null while True: data get_data(page) if data[data] null: break # 处理数据...更智能的终止条件除了检查null我们还可以结合其他条件来判断是否应该停止爬取检查数据长度如果返回的数据列表为空检查页码有些API会在响应中包含总页数信息超时机制设置最大尝试次数避免无限循环max_retry 3 retry_count 0 while retry_count max_retry: data get_data(page) if not data or data[data] null or len(data[data][diff]) 0: retry_count 1 continue retry_count 0 # 处理有效数据...分页参数优化东方财富的API使用pn(page number)和pz(page size)参数控制分页。合理设置pz可以减少请求次数# 每页获取100条数据减少总请求数 url fhttps://api.example.com?pn{page}pz100但要注意有些API对单次请求的数据量有限制过大的pz可能导致请求失败。3. 如何破译神秘的字段代码东方财富API最让人头疼的莫过于那些f1,f2,f3...的字段名了。第一次看到这些时我完全不知道哪个字段对应什么数据。字段映射表经过多次尝试和验证我整理出了这些字段的对应关系字段代码含义字段代码含义f1未知f12股票代码f2最新价f13市场类型f3涨跌幅(%)f14股票名称f4涨跌额f15最高价f5成交量(手)f16最低价f6成交额(元)f17今开价f7振幅(%)f18昨收价f8换手率(%)f20总市值f9市盈率(动态)f21流通市值f10量比f23市净率动态字段映射为了代码更易维护我们可以创建一个字段映射字典FIELD_MAPPING { f12: code, f14: name, f2: price, f3: change_percent, f4: change_amount, # 其他字段映射... } def parse_stock_data(raw_data): return {FIELD_MAPPING.get(k, k): v for k, v in raw_data.items()}这样处理后的数据可读性大大提高后续分析也方便很多。自动发现字段含义如果你不确定某些字段的含义可以通过以下方法推测对比网页显示的数据和API返回的数据观察字段值的变化规律如f3总是百分比格式查看不同股票间该字段的差异# 打印所有字段及其示例值帮助识别未知字段 sample_stock data[data][diff][0] for field, value in sample_stock.items(): print(f{field}: {value} ({type(value)}))4. 实战完整的股票数据爬取方案结合上面三个问题的解决方案我们来看一个完整的实现import requests import re import pandas as pd from typing import Dict, List # 字段映射表 FIELD_MAPPING { f12: 代码, f14: 名称, f2: 最新价, f3: 涨跌幅, f4: 涨跌额, f5: 成交量, f6: 成交额, f7: 振幅, f8: 换手率, f9: 市盈率, f10: 量比, f15: 最高, f16: 最低, f17: 今开, f18: 昨收, f23: 市净率, } def parse_jsonp(jsonp_str: str) - Dict: 解析JSONP格式的响应 try: left jsonp_str.find(() 1 right jsonp_str.rfind()) return eval(jsonp_str[left:right]) # 使用eval更灵活但要注意安全性 except Exception as e: print(f解析JSONP失败: {e}) return None def get_stock_data(cmd: str, page: int) - List[Dict]: 获取单页股票数据 url fhttps://7.push2.eastmoney.com/api/qt/clist/get?cbjQuery1124...pn{page}pz100fid{cmd} headers {User-Agent: Mozilla/5.0...} try: resp requests.get(url, headersheaders, timeout10) data parse_jsonp(resp.text) if not data or not data.get(data): return None stocks [] for item in data[data][diff]: stock {FIELD_MAPPING.get(k, k): v for k, v in item.items() if k in FIELD_MAPPING} stocks.append(stock) return stocks except Exception as e: print(f获取数据失败: {e}) return None def crawl_all_stocks(cmd: str, max_pages: int 100) - pd.DataFrame: 爬取所有页面的股票数据 all_stocks [] for page in range(1, max_pages 1): stocks get_stock_data(cmd, page) if not stocks: break all_stocks.extend(stocks) print(f已获取第{page}页数据共{len(stocks)}条) return pd.DataFrame(all_stocks) # 示例爬取沪深京A股数据 df crawl_all_stocks(f3fsm:0t:6,m:0t:80,m:1t:2,m:1t:23,m:0t:81s:2048) df.to_excel(stock_data.xlsx, indexFalse)这个实现包含了我们讨论的所有最佳实践安全的JSONP解析智能的分页终止判断清晰的字段映射完善的错误处理5. 进阶技巧与优化建议请求头优化东方财富对爬虫有一定限制合理的请求头可以减少被封禁的风险headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://quote.eastmoney.com/, Accept: application/json, Accept-Language: zh-CN,zh;q0.9, }代理和重试机制对于大规模爬取建议添加代理和自动重试from time import sleep def get_with_retry(url, max_retry3, delay1): for i in range(max_retry): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: return resp except Exception: pass sleep(delay * (i 1)) return None数据存储优化除了保存为Excel还可以考虑更高效的存储方式# 保存为Parquet格式适合大数据量 df.to_parquet(stock_data.parquet) # 或者直接存入数据库 import sqlite3 conn sqlite3.connect(stocks.db) df.to_sql(stock_data, conn, if_existsreplace, indexFalse)异步爬取提升效率对于大量数据可以使用异步请求加速爬取import aiohttp import asyncio async def fetch_page(session, url): async with session.get(url) as response: return await response.text() async def crawl_all(): async with aiohttp.ClientSession() as session: tasks [fetch_page(session, build_url(page)) for page in range(1, 101)] return await asyncio.gather(*tasks)6. 常见问题排查指南即使按照最佳实践实现在实际操作中仍可能遇到各种问题。以下是几个常见问题及解决方法问题1突然获取不到数据了可能原因IP被封禁API接口变更请求频率过高解决方案检查是否能正常访问网页版尝试更换IP或添加延迟检查API URL是否有变化问题2返回的数据格式异常可能原因网站更新了JSONP包装格式请求参数不正确解决方案打印原始响应检查格式变化验证请求参数是否完整更新正则表达式或解析逻辑问题3某些字段值为空或异常可能原因该股票确实没有这个数据字段映射错误解决方案检查网页显示是否有该数据验证字段映射是否正确添加默认值处理逻辑# 在字段映射处理中添加默认值 stock {FIELD_MAPPING.get(k, k): v if v is not None else for k, v in item.items()}7. 法律与合规注意事项在爬取任何网站数据时务必注意以下几点遵守robots.txt检查目标网站是否允许爬取控制请求频率避免对服务器造成过大压力尊重数据版权确认数据使用是否符合网站条款用户代理标识明确标识你的爬虫身份重要本文提供的技术方案仅用于学习交流实际应用中请确保遵守相关法律法规和网站的使用条款。