pd.DataFrame读取爬虫数据报错,因HTML表格含空行、合并单元格、嵌套标签或JS渲染内容,导致read_html解析失败;需手动定位表格、清洗空行与异常字符、鲁棒转换数值、显式指定SQL字段类型,并添加全流程校验与日志。

requests + BeautifulSoup 爬下来的数据,为什么 pd.DataFrame 一读就报错?
因为爬到的 HTML 表格里常混着空行、合并单元格、嵌套 <div>、甚至 JS 渲染内容——pd.read_html() 默认只认干净的 <table> 结构,遇到 <tr> 缺 <td> 或属性不全,直接抛 ValueError: No tables found 或列对不齐。
- 先用
response.text打印原始 HTML 片段,确认目标表格是否真在源码里(防 JS 渲染) - 用
soup.find('table', {'class': 'data-table'})显式定位,别依赖pd.read_html(url)直接拉 - 提取时统一用
[row.find_all(['td', 'th']) for row in soup.find_all('tr')],再手动过滤空行和表头重复行 - 列名含换行或空格?用
[x.get_text(strip=True) for x in headers]清一遍,再df.columns = [...]
清洗阶段 df['price'].str.replace('¥', '').astype(float) 报 ValueError: could not convert string to float
价格字段里藏着“暂无报价”、“面议”、“—”、“ ”,甚至隐藏 Unicode 空格(\xa0),str.replace() 没法一锅端。
- 先跑
df['price'].apply(type).unique()看有没有float混入(NaN 会变float,干扰判断) - 用
df['price'].str.replace(r'[^\d.]', '', regex=True)只留数字和小数点,比逐个replace更鲁棒 - 转换前加
df['price'] = df['price'].replace('', np.nan),再pd.to_numeric(..., errors='coerce'),把异常值转为NaN而非中断 - 别忘了
df.dropna(subset=['price'])是最后一步,不是清洗第一步——中间步骤要保留 NaN 做标记
入库 MySQL 时 df.to_sql() 卡住或丢数据
to_sql 默认用 if_exists='fail',表存在就报错;设成 'replace' 又会清空整张表;设成 'append' 却可能因主键冲突静默失败,尤其字段类型没对齐时。
- 建表别全靠
to_sql自动推断:price字段它可能建为TEXT,后续WHERE price > 100就走不了索引 - 显式传
dtype={'price': sqlalchemy.Float(), 'title': sqlalchemy.String(200)},和数据库实际类型对齐 - 主键/唯一约束字段(如
item_id)入库前先df.drop_duplicates(subset=['item_id'], keep='last'),避免IntegrityError - 大数据量别单次
to_sql:用chunksize=500分批,配合method='multi'减少 SQL 请求次数
爬虫+清洗+入库连跑,第二天发现数据全乱了
网站前端微调一个 class 名、后端接口加个 referer 校验、甚至反爬返回 200 但 HTML 里塞满占位符——这些变化不会报错,只会让 BeautifulSoup 提取空列表、pd.to_numeric 全转成 NaN、入库时字段全 NULL。
立即学习“Python免费学习笔记(深入)”;
- 每次运行前加校验:比如
assert len(df) > 10,assert df['title'].nunique() / len(df) > 0.8(去重率太低说明标题全抓成“暂无”) - 把关键抽取逻辑封装成函数,例如
extract_price(soup),函数内加logging.warning(f'Price empty in {url}') - 不要复用 session 对象跨天跑:Cookie 过期、token 失效后请求仍返回 200,但内容已失效
- 最保险的是存一份原始 HTML 快照(按日期命名目录),出问题时能立刻对比是解析逻辑崩了,还是源站变了
链路越长,中间某个环节静默失败的概率越高。别信“跑通一次就稳了”,得让每步都留下可验证的痕迹。










