从抓包到入库:Python接口采集与MySQL幂等写入实战
简介面向Python3爬虫入门者和数据采集人员这份示例演示了如何借助请求库、正则模块和数据库连接库实现网页数据抓取并写入MySQL的完整流程。内容从抓包工具定位数据接口讲起逐步拆解正则匹配规则、代理IP设置、分页循环抓取与异常重试机制同时重点展示了创建数据表、设置唯一索引避免重复入库、批量插入记录以及事务提交等关键数据库操作。通过该示例读者可以掌握将爬虫结果持久化到数据库的工程化思路并学会处理订单监控、数据分析等场景中的自动化采集需求。资源以单个PDF文件提供体积仅214KB内容精炼且附有可运行的示例代码便于随时查阅和对照练习。截至目前该资源已有10497人学习下载是入门Python爬虫与MySQL联动开发的实用参考资料。1. 从抓包到入库跑通一个订单接口的完整路径接手这个需求的时候场景很明确一个电脑客户端有「接单大厅」大厅里会滚动展示订单简要信息单子被接走就从列表消失而客户端没有提供历史订单查询功能。于是就需要一个定时任务每 10 秒抓一次大厅接口把新出现的订单落进自己的 MySQL 库供后续统计和筛选用。这里用到的技术栈是 Python3 requests re pymysql整体链路不算复杂但有几个点很值得展开说接口的签名参数怎么处理、正则字段怎么和表结构对应、唯一索引怎么保证增量写入以及轮询间隔和异常兜底怎么设计才不会把库写乱。这篇主要就是把这条链路完整拆开每一步的代码、参数、坑点都过一遍适合正在写爬虫入库但没怎么接触过签名字段和幂等写入的人参考。2. 请求层requests 的 URL 参数、代理与重试机制2.1 URL 结构与签名参数的处理思路先看请求地址原文里是这么拼的requests.get( https://www.dianjingbaozi.com/api/dailian/soldier/hall? access_token3ef3abbea1f6cf16b2420eb962cf1c9a dan_enddan_startgame_id2kw orderprice_descpage%d % l pagesize30price_end0price_start0 server_code000200000000 signca19072ea0acb55a2ed2486d6ff6c5256c7a0773 timestamp1511235791typepublictype_id%20HTTP/1.1, proxiesproxy )注意几个细节。access_token是账号级别的凭证sign是把请求参数按某种规则拼接后再做哈希得到的签名值timestamp是时间戳这三个组合在一起基本就是这套 API 的鉴权思路。page用%d格式化进去type_id%20HTTP/1.1这个明显是抓包时把协议头一起带进来了实际服务端大概率忽略但保留也不影响。requests.adapters.DEFAULT_RETRIES 5这个赋值我一般不建议直接用。它设置的是 urllib3 的连接池重试次数但 requests 的get()默认是不会对连接失败自动重试的除非显式传入max_retries。这个代码在连接池层把重试改成 5对偶发的网络抖动有一定帮助但对 HTTP 错误码500、502没有效用真正的重试应该在异常处理里做后面会提到。2.2 代理的加载与失效场景原文里代理是写死的一个 IPproxy {HTTP: 61.135.155.82:443}。这里有个容易踩的坑requests 的proxies参数key 需要和 URL 的协议对应。如果请求的是https://开头的地址而字典里只写了HTTP部分环境会直接绕过代理发起请求或者抛MissingSchema异常。稳妥写法是把 HTTP 和 HTTPS 都配上proxy { http: http://61.135.155.82:443, https: http://61.135.155.82:443 }代理失效是家常便饭。免费代理池的 IP 存活时间通常只有几分钟到几十分钟代码里一旦代理挂了请求就会抛requests.exceptions.ProxyError或者ConnectionError。原文在GetResults()外层包了一个try/except捕获到就time.sleep(5)然后打印失败这个兜底思路对但不区分异常类型容易把代码里的语法错误一起吞掉调试时会比较痛苦。提示免费代理只适合短期调试跑生产级的定时采集最好用自建代理池或者直接走本地出口 IP避免把时间耗在代理维护上。2.3 加请求头与 Session 复用原文没有写headers这在真实场景里很容易触发服务端反爬。常见的风控策略是检查User-Agent、Referer、Origin是否齐全如果缺失直接返回 403 或空数据。我一般会加上一个基本的浏览器 UAimport requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.dianjingbaozi.com/ } s requests.Session() s.headers.update(headers) resp s.get(url, timeout10, proxiesproxy) html resp.content.decode(utf-8)用Session而不是裸requests.get()好处是连接会被复用TCP 握手次数减少对接口的访问频率控制在 10 秒一次时整体连接开销会小很多。timeout10是必须加的否则某个 IP 黑洞会让线程一直挂起。3. 解析层正则提取的字段设计与两段式抓取3.1 列表页拿 order_no详情页拿完整字段原文的思路是两层抓取先请求接单大厅接口拿到订单列表再用列表里的order_no拼出订单详情页 URL逐条请求详情接口最后用一组正则把详情页里的字段提取出来。这个设计在「列表页字段不全、详情页字段完整」的接口场景里是标准做法但代价是请求量翻倍每轮抓取需要请求 1 次列表页 N 次详情页。列表页提取order_no的正则是outcome_reg_order_no re.findall(rorder_no:(.*?),game_area, html)这行代码用了非贪婪匹配.*?意思是匹配两个引号之间的任意字符但尽量少匹配。用game_area作为结束边界比用单纯的,更精确避免跨字段误匹配。这里有个隐含前提列表接口返回的 JSON 是压缩成一行、键值顺序固定的字符串。如果服务端调整了字段顺序或者把 JSON 格式化输出这个正则立刻失效这就是正则解析 JSON 的脆弱性。3.2 22 个字段的正则表与嵌套结构展开详情页的正则定义成一个列表reg里面是 22 个独立的字段表达式。每个字段的写法都是rkey:(.*?),的结构这个模式对紧凑型 JSON 有效对带嵌套对象或数组的 JSON 就会出问题。更稳的方案是直接用json模块解析但原文的接口返回了 JSON 字符串里头带了\转义直接json.loads会报错所以用正则反而是当时最快能跑通的方式。有一个字段需要特别说明game_area的匹配模式是rgame_area:(.*?)\\/(.*?)\\/(.*?),这个字段的值是三个层级拼接的比如「国服/艾泽拉斯/部落」中间用/分隔。正则里\\/表示匹配一个反斜杠加斜杠也就是 JSON 转义后的\/。re.findall遇到多个分组时返回的是元组列表每个元组里是三个分组值所以原文用了outcome.extend(outcome[k])把元组拍平再追加到结果列表。这里有个细节值得注意reg列表里大部分字段是(.*?)个别是(.*?),区别在于数值型字段后面没有引号。比如rorder_hours:(.*?),和ris_show_pwd:(.*?),这种混合结构要求写正则是必须逐个核对原始返回不然多一个引号少一个引号就会导致匹配结果错位。3.3 结果集的结构与入库前的数据对齐组装结果列表时的关键代码是for i in range(len(reg)): outcome re.findall(reg[i], html_order) if i 4: for k in range(len(outcome)): outcome_reg.extend(outcome[k]) else: outcome_reg.extend(outcome)当下标i 4时也就是game_area那个正则返回的是元组列表这里把元组的每个元素依次取出并拍平其余下标走普通逻辑extend把列表展开加进结果。这样最终results里每一个元素都是一维列表字段顺序和reg列表的定义顺序一一对应为后面 INSERT 语句的列顺序提供了直接依据。注意正则解析的关键假设是「字段顺序固定」。只要接口方的字段顺序一变results 里的数据顺序就会整体错位入库后会出现张冠李戴。所以写这类解析逻辑时最好在第一轮抓取时把原始返回保存一份后续比对用。4. 存储层pymysql 建表与唯一索引去重4.1 建表 DDL 里的字段类型与字符集选择连库逻辑封装在mysql_create()里传参一个空字符串给mysql_host表示默认本地连接。这里要注意 UNIQUE 索引的声明用了两种方式建表语句里写了UNIQUE KEY no(order_no)又额外执行了CREATE UNIQUE INDEX id ON DUMPLINGS(id)。这两个操作的效果都是建立唯一索引但字段不一样前者给order_no建唯一约束后者给id建唯一索引。如果id是主键重复建唯一索引没有实际意义反而多一次索引开销。表结构里有几个字段类型值得提一下order_price FLOAT(10)浮点字段存价格FLOAT(10)表示显示宽度但 MySQL 8.0 已经废弃显示宽度语法用DECIMAL(10,2)存价格更稳妥避免浮点精度丢分。order_current VARCHAR(3908)这个长度偏大VARCHAR 的有效最大长度取决于行大小utf8 字符集下一个 VARCHAR(3908) 会占用约 11724 字节加上其他字段很容易逼近 65535 字节的行上限。实际order_current如果只是订单当前状态描述给到 VARCHAR(500) 足够。mobile VARCHAR(265)和contact VARCHAR(265)这两个字段分别是订单发布者的手机号和联系方式抓取入库时要考虑敏感信息合规一般建议脱敏存储或者只做统计不展示原文。is_show_pwd TINYINT布尔标识位TINYINT(1) 是合理的不需要额外加索引。建表的完整语句整理后如下CREATE TABLE DUMPLINGS ( id CHAR(10), order_no CHAR(50), order_title VARCHAR(265), publish_desc VARCHAR(265), game_name VARCHAR(265), game_area VARCHAR(265), game_area_distinct VARCHAR(265), order_current VARCHAR(3908), order_content VARCHAR(3908), order_hours CHAR(10), order_price FLOAT(10), add_price FLOAT(10), safe_money FLOAT(10), speed_money FLOAT(10), order_status_desc VARCHAR(265), order_lock_desc VARCHAR(265), cancel_type_desc VARCHAR(265), kf_status_desc VARCHAR(265), is_show_pwd TINYINT, game_pwd CHAR(50), game_account VARCHAR(265), game_actor VARCHAR(265), left_hours VARCHAR(265), created_at VARCHAR(265), account_id CHAR(50), mobile VARCHAR(265), mobile2 VARCHAR(265), contact VARCHAR(265), contact2 VARCHAR(265), qq VARCHAR(265), PRIMARY KEY (id), UNIQUE KEY no(order_no) ) ENGINEInnoDB AUTO_INCREMENT12 DEFAULT CHARSETutf8;注意id CHAR(10)被设成了主键但代码里并没有显式为每行生成 id如果接口返回的 JSON 里没有id字段插入时会因为主键为空直接报错。不过从接口返回看最外层是有id:(.*?),这个正则的说明订单本身带了数值型 idCHAR(10) 存数字没问题但排序和比较效率不如 BIGINT。如果是新写表我会把id改成BIGINT UNSIGNED AUTO_INCREMENT做主键order_no保持唯一索引做业务键。4.2 链接参数与字符集坑pymysql.connect()里charsetutf8必须强调一下。MySQL 的utf8实际是 utf8mb3只支持基本多语言平面遇到 emoji 或者特殊汉字比如「」会报Incorrect string value错误。现在的新项目建议直接用utf8mb4对应 pymysql 里传charsetutf8mb4并且建表语句也要统一DEFAULT CHARSETutf8mb4。原文里game_title、publish_desc这类字段如果碰到生僻字用 utf8 就会写入失败被except: pass静默吞掉数据不知不觉就丢了。4.3 逐条 INSERT 与唯一索引的幂等写入入库函数的核心是拼 SQL 字符串sql INSERT INTO DUMPLINGS(id,order_no,order_title,publish_desc ,game_name, \ game_area,game_area_distinct,order_current,order_content,order_hours, \ order_price,add_price,safe_money,speed_money,order_status_desc, \ order_lock_desc,cancel_type_desc,kf_status_desc,is_show_pwd,game_pwd, \ game_account,game_actor,left_hours,created_at,account_id, \ mobile,mobile2,contact,contact2,qq) VALUES ( for i in range(len(results[j])): sql sql results[j][i] , sql sql[:-1] ) sql sql.encode(utf-8) cursor.execute(sql) db.commit()这段代码有一个隐患所有字段都套了单引号遇到字段值里本身包含单引号比如订单标题里有its就会把 SQL 截断轻则插入失败重则构造出畸形 SQL。更规范的做法是使用参数化查询columns [ id, order_no, order_title, publish_desc, game_name, game_area, game_area_distinct, order_current, order_content, order_hours, order_price, add_price, safe_money, speed_money, order_status_desc, order_lock_desc, cancel_type_desc, kf_status_desc, is_show_pwd, game_pwd, game_account, game_actor, left_hours, created_at, account_id, mobile, mobile2, contact, contact2, qq ] placeholders , .join([%s] * len(columns)) insert_sql ( INSERT INTO DUMPLINGS ( , .join(columns) ) VALUES ( placeholders ) ON DUPLICATE KEY UPDATE order_no VALUES(order_no) ) for row in results: cursor.execute(insert_sql, row) db.commit()参数化之后pymysql 会负责转义字段里的特殊字符SQL 注入的问题也一并解决了。再配合ON DUPLICATE KEY UPDATE当order_no已经存在时这一行会变成更新而不是报错彻底避免重复 key 导致的插入中断。原文用的是try/except: pass静默跳过主键冲突同样能达到「只入库新数据」的效果但每次冲突都会抛一次异常触发 Python 异常机制的成本比正常执行 SQL 高不少数据量大时效率差距明显。提示cursor.execute() 执行后必须 db.commit() 才会真正落库。原文在每条插入后都 commit安全但慢。可以把 commit() 移到 for 循环外面一次性提交整个批次事务的原子性也更好中途失败可以整体回滚。5. 轮询调度、异常处理与工程化收尾主循环写在while True里每 10 秒跑一次GetResults()和IntoMysql()中间用time.sleep(10)控频率。这在日常演示里够用但放到生产环境有几个问题要补轮询抖动、实例重复启动、日志缺失。轮询抖动方面time.sleep(10)是固定间隔但GetResults()和IntoMysql()本身耗时不确定尤其详情页多时单轮可能跑到 20 秒以上实际周期就变成 30 秒而不是 10 秒。要保证严格每 10 秒一轮应该用绝对时间校准import time interval 10 next_run time.time() while True: next_run interval try: results GetResults() IntoMysql(results) except Exception as exc: print(f[{time.strftime(%Y-%m-%d %H:%M:%S)}] 采集异常: {exc}, flushTrue) else: print(f本轮入库 {len(results)} 条, flushTrue) sleep_time next_run - time.time() if sleep_time 0: time.sleep(sleep_time)这样每轮结束后的休眠时间会自动扣掉采集耗时周期稳定在 10 秒。日志里加上时间戳并用flushTrue强制输出避免 Python 的 stdout 缓冲导致日志滞后排查问题时能看到每轮的真实节奏。关于 API 里sign参数原文里的timestamp是固定的1511235791说明请求发出后签名已经过期。这个接口大概率会校验时间戳和签名的匹配关系时间一过就会返回签名错误。真实场景里要在代码里预先算好签名再请求签名算法通常是参数名按 ASCII 排序后拼接加盐做 MD5 或 SHA1具体规则需要从 JS 或客户端逆向里找。如果只是自己调试直接复制抓包时的 URL 和签名也行但注意 URL 里的timestamp过期后必须重新抓包更新否则接口会一直报错。这里不展开签名破解工具链的正道是维护一个签名计算函数每次请求前重新生成。采集频率方面10 秒一次对这个接口压力不算大但公开接口的访问要克制。即便目标源允许把间隔拉长到 15 秒或 30 秒对数据实时性影响有限对服务器和自己出口 IP 的压力都小很多。另外MySQL 连接每轮都重新connect()和close()对低频轮询无所谓如果后面把频率提到秒级建议把连接对象提升到函数外部复用一个长连接减少 TCP 和认证握手开销。最后一轮补充两个调试技巧抓包时如果用的是 HttpAnalyzerStdV7 这类工具注意看接口返回的 Content-Type如果是application/json且带转义符正则匹配之前最好先做一次unicode_escape解码把\/还原成/正则里的\\/(.*?)\\/就能改写成/(.*?)/可读性会好很多。验证入库结果时SQL 里先跑SELECT COUNT(*) FROM DUMPLINGS看总量再用SELECT order_no, created_at FROM DUMPLINGS ORDER BY created_at DESC LIMIT 5看最近入库的几个订单号是否符合预期配合UNIQUE KEY约束重复跑几轮主循环确认表里行数不涨增量逻辑就证明是通的。本文还有配套的精品资源点击获取