
1. 京东商品采集入库的真实场景与链路拆解做商品库自建这件事绕不开京东。不管是比价系统、选品工具还是给内部运营做数据看板第一步都是把京东搜索列表页和详情页的字段稳定抓下来清洗成结构化数据再落到 MySQL 里。我试过直接照搬网上那种几十行的MySQLdb BeautifulSoup脚本跑一次能出结果但换台机器、换个关键词、跑上几百页就开始各种报错编码乱码、价格抓成空、字段错位、插入重复。问题不在爬虫本身而在于整条链路没有分层——抓取、解析、清洗、入库、凭证管理全糊在一个main()里。这篇要解决的就是这条完整链路从京东搜索列表页拿到商品标题和价格补上商品 ID、店铺、评论数这些真正有用的字段清洗后写进 MySQL并且把调用外部接口比如用大模型做标题归一化、类目打标的凭证统一走 TaoToken 的 Key 通道管理。适合谁看有 Python 基础、想自建商品库的数据开发者或者正在做电商数据方向、需要一套能长期跑而不是跑一次就废的采集方案的人。先说清楚京东页面的结构特点这决定了你的解析策略。搜索列表页https://search.jd.com/Search?keyword关键词encutf-8pageN返回的是服务端渲染的 HTML商品卡片在div.gl-item里价格在div.p-price的i标签标题在div.p-name的em标签。但注意列表页的价格是懒加载的部分商品价格需要请求https://p.3.cn/prices/mgets?skuIdsJ_商品ID这个价格接口才能拿到准确值。详情页https://item.jd.com/商品ID.html则包含店铺名、商品参数、评论数等更丰富的字段。所以合理的链路是列表页抓商品 ID 标题 基础价格 → 用商品 ID 请求价格接口补全价格 → 可选地进详情页补店铺和参数 → 清洗去重 → 批量插入 MySQL。每一步都要能单独调试而不是一个try/except吞掉所有异常。字段设计上我建议至少保留这些列sku_id商品唯一 ID主键、title标题、price当前价、shop_name店铺、comment_count评论数、category类目可后续用模型打标、crawl_time抓取时间。sku_id是去重的关键京东每个商品都有稳定的 SKU 编号用INSERT ... ON DUPLICATE KEY UPDATE就能实现增量更新避免每次全量重跑。凭证管理这块单独拎出来说。采集脚本本身不需要外部 Key但一旦你要做标题清洗、类目预测、评论情感分析就会调用大模型接口。如果每个脚本里硬编码一个 Key团队里几个人各写各的Key 散落各处轮换和审计都是灾难。TaoToken 的价值就在这里它提供一个统一的 API 通道Base URL 固定Key 统一管理模型 ID 按需切换。你可以在一个地方看到所有调用凭证而不是在十个.py文件里grep。下面这张表先把整条链路的模块和职责对齐后面每一节都会给出可复制的代码。模块职责关键产出抓取层请求列表页/价格接口/详情页原始 HTML / JSON解析层提取 SKU、标题、价格、店铺结构化 dict清洗层去标签、去空格、价格转 float干净记录入库层建表、批量 upsertMySQL 行凭证层统一管理模型调用 KeyTaoToken 通道链路拆清楚之后你会发现原来那个脚本最大的问题是「解析和入库耦合」——printGoodsList里既打印又插库一旦插入失败你连原始数据都拿不到。分层之后每层都能单独跑、单独测出问题定位快得多。2. TaoToken 统一 Key 通道的前置准备与凭证管理在写爬虫之前先把凭证这件事理清楚否则后面每加一个模型调用就要改一次代码。TaoToken 的定位是一个统一的 API 通道你只需要记住一个 Base URL一个 Key模型 ID 通过参数切换。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 注意 API 地址不带 UTM 参数直接用于代码里的base_url。为什么采集项目需要它举个具体场景你抓了 5000 条京东商品标题想用模型把「【限时秒杀】XX品牌2024新款…」这种带营销词的标题清洗成规范商品名同时预测类目。如果直接对接各家模型你要维护多套 Key、多套 SDK、多套计费。走 TaoToken 的话代码里只改model字段Key 和 Base URL 不变。对于需要长期跑的采集管道这种稳定性比省几毛钱重要得多。前置准备分三步。第一步拿到 Key。登录后在控制台的 API Keys 页面创建地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建后立刻复制保存页面不会再次完整显示。第二步确认你要用的模型 ID可以在模型对话页面先手动试一次地址 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 输入一段商品标题看返回效果确认模型可用再写进代码。第三步如果你要做的是长期编码或 Agent 类任务比如让模型自动生成解析规则可以看 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。凭证不要写死在代码里。用环境变量或者.env文件配合python-dotenv读取。下面是一个.env示例# .env 文件不要提交到 git TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api MYSQL_HOSTlocalhost MYSQL_USERroot MYSQL_PASSWORD你的密码 MYSQL_DBjd_goods然后在代码里统一读取。这样换机器、换 Key 都不用动业务逻辑。注意.env一定要加进.gitignore我见过太多人把 Key 提交到公开仓库然后被刷爆的案例。对于需要多环境开发/测试/生产的团队建议在 TaoToken 控制台按环境创建不同的 Key命名上带前缀比如dev-、prod-。这样某个环境的 Key 泄露直接吊销那一个不影响其他环境。控制台的 Key 列表支持随时删除重建轮换成本很低。还有一点容易被忽略Base URL 的写法。OpenAI 兼容的 SDK 通常要求base_url以/v1结尾或者由 SDK 自动拼接。TaoToken 的 API 入口是https://taotoken.net/api在 OpenAI SDK 里配置时base_url填这个地址即可SDK 会处理路径拼接。如果你用的是requests直接请求完整路径是https://taotoken.net/api/v1/chat/completions。两种方式都行用 SDK 更省事。凭证层准备好之后采集脚本里调用模型就变成了一件很轻的事一个封装好的函数传入标题列表返回清洗后的结果。Key 的复杂度被隔离在配置层业务代码只关心数据。3. 可复制的爬虫配置与 MySQL 建表语句这一节给出可以直接复制运行的配置。先建库建表再写抓取和解析最后组装成完整脚本。所有路径和参数都按前面说的分层来。先看 MySQL 建表语句。用utf8mb4字符集避免商品标题里的 emoji 或特殊符号插入失败。sku_id设为主键配合ON DUPLICATE KEY UPDATE实现增量更新。CREATE DATABASE IF NOT EXISTS jd_goods DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE jd_goods; CREATE TABLE IF NOT EXISTS goods ( sku_id BIGINT NOT NULL COMMENT 京东商品SKU编号, title VARCHAR(512) NOT NULL DEFAULT COMMENT 商品标题, price DECIMAL(10,2) DEFAULT NULL COMMENT 当前价格, shop_name VARCHAR(255) DEFAULT COMMENT 店铺名称, comment_count INT DEFAULT 0 COMMENT 评论数, category VARCHAR(128) DEFAULT COMMENT 类目, crawl_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 抓取时间, PRIMARY KEY (sku_id), KEY idx_price (price), KEY idx_crawl_time (crawl_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT京东商品采集表;字段映射关系要提前想清楚京东列表页的data-sku属性对应sku_idp-name里的em文本对应title价格接口返回的p字段对应price。店铺和评论数在详情页如果只跑列表页可以先留空后续补抓。接下来是抓取层的配置。用requests.Session复用连接设置合理的超时和重试。请求头里User-Agent必须带否则京东会返回验证页。Referer也建议带上搜索页地址。import os import re import time import json import pymysql import requests from bs4 import BeautifulSoup from dotenv import load_dotenv load_dotenv() HEADERS { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 ), Referer: https://search.jd.com/, Accept-Language: zh-CN,zh;q0.9, } session requests.Session() session.headers.update(HEADERS) def fetch(url, retries3, timeout15): for i in range(retries): try: resp session.get(url, timeouttimeout) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f[fetch] 第{i1}次失败: {e}) time.sleep(1.5 * (i 1)) return None解析层单独写函数输入 HTML输出记录列表。列表页解析用BeautifulSoup价格接口解析用json。def parse_list_page(html): 解析京东搜索列表页返回 [{sku_id, title, price}, ...] soup BeautifulSoup(html, html.parser) items [] for li in soup.select(li.gl-item): sku li.get(data-sku) if not sku: continue name_em li.select_one(div.p-name em) title name_em.get_text(stripTrue) if name_em else price_i li.select_one(div.p-price i) price_text price_i.get_text(stripTrue) if price_i else price None if re.match(r^\d(\.\d)?$, price_text): price float(price_text) items.append({sku_id: int(sku), title: title, price: price}) return items def fetch_prices(sku_ids): 批量请求价格接口返回 {sku_id: price} if not sku_ids: return {} ids_param ,.join(fJ_{s} for s in sku_ids) url fhttps://p.3.cn/prices/mgets?skuIds{ids_param} text fetch(url) if not text: return {} result {} try: data json.loads(text) for row in data: sku int(row[id].replace(J_, )) result[sku] float(row[p]) if row.get(p) else None except (json.JSONDecodeError, KeyError, ValueError) as e: print(f[price] 解析失败: {e}) return result清洗层做两件事标题去多余空白和营销符号价格统一成Decimal或float。入库层用executemany批量 upsert。def clean_title(title): title re.sub(r\s, , title).strip() title re.sub(r^【[^】]*】, , title).strip() return title[:500] def save_to_mysql(records): conn pymysql.connect( hostos.getenv(MYSQL_HOST, localhost), useros.getenv(MYSQL_USER, root), passwordos.getenv(MYSQL_PASSWORD, ), databaseos.getenv(MYSQL_DB, jd_goods), charsetutf8mb4, autocommitFalse, ) sql INSERT INTO goods (sku_id, title, price, crawl_time) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE title VALUES(title), price VALUES(price), crawl_time NOW() rows [(r[sku_id], clean_title(r[title]), r[price]) for r in records] try: with conn.cursor() as cur: cur.executemany(sql, rows) conn.commit() print(f[mysql] 写入/更新 {len(rows)} 条) except Exception as e: conn.rollback() print(f[mysql] 失败回滚: {e}) raise finally: conn.close()主流程把上面几层串起来控制翻页深度和请求间隔。def crawl(keyword, pages3): all_records [] for page in range(1, pages 1): url ( fhttps://search.jd.com/Search?keyword{keyword} fencutf-8page{page} ) html fetch(url) if not html: print(f[crawl] 第{page}页抓取失败跳过) continue items parse_list_page(html) print(f[crawl] 第{page}页解析到 {len(items)} 条) all_records.extend(items) time.sleep(1.2) sku_ids [r[sku_id] for r in all_records if r[price] is None] price_map fetch_prices(sku_ids) for r in all_records: if r[price] is None and r[sku_id] in price_map: r[price] price_map[r[sku_id]] return all_records if __name__ __main__: records crawl(机械键盘, pages3) if records: save_to_mysql(records) print(f完成共 {len(records)} 条)这套配置的关键点是抓取、解析、清洗、入库各自独立价格缺失时用价格接口补全入库用 upsert 保证幂等。跑一次之后数据库里就有数据了重复跑不会产生重复行。4. 验证请求与成功结果确认代码写完不算完得验证数据真的进去了而且字段是对的。这一节给出从请求到入库的完整验证动作包括怎么确认价格接口返回正常、怎么查库、怎么处理编码问题。先单独验证价格接口。这是最容易出问题的一环因为它的返回格式和列表页完全不同。手动请求一次curl -s https://p.3.cn/prices/mgets?skuIdsJ_100012043978 \ -H User-Agent: Mozilla/5.0 | python -m json.tool正常返回类似[ { id: J_100012043978, p: 399.00, m: 599.00, op: 399.00 } ]p是当前价m是市场价op是原价。如果返回空数组或者p为空字符串说明该 SKU 没有价格数据代码里要处理成None而不是0否则会污染价格统计。再验证列表页解析。把抓到的 HTML 存一份到本地用解析函数单独跑看输出条数和字段html fetch(https://search.jd.com/Search?keyword机械键盘encutf-8page1) items parse_list_page(html) print(f解析条数: {len(items)}) for it in items[:3]: print(it)正常输出应该是每条都有sku_id、title、price标题里没有 HTML 标签残留。如果标题里出现lt;emgt;这种转义字符说明你用了str(soup)再正则的方式应该直接用get_text()。这是原脚本里re.findall(rem.*/em, str(tlt))的典型坑——先转字符串再正则转义和嵌套标签都会出问题。入库验证用 SQL 直接查SELECT sku_id, title, price, crawl_time FROM goods ORDER BY crawl_time DESC LIMIT 10; SELECT COUNT(*) AS total, COUNT(price) AS with_price, AVG(price) AS avg_price FROM goods;total是总条数with_price是有价格的条数两者差距大说明价格补全没生效。avg_price可以用来快速判断价格是否合理如果出现0.00或者异常大的值回去检查价格解析。编码问题单独说。京东页面有 UTF-8 和 GBK 混用的情况resp.apparent_encoding有时会猜错。稳妥的做法是优先用resp.encoding utf-8如果出现乱码再回退到apparent_encoding。数据库连接必须指定charsetutf8mb4建表也用utf8mb4否则 emoji 和生僻字会插入失败。验证通过的标准是跑一次crawl(机械键盘, pages3)控制台输出每页解析条数最后输出写入条数查库能看到对应条数的记录标题干净、价格是数字、crawl_time是当前时间。再跑一次同样的命令总条数不变因为 upsertcrawl_time更新说明幂等生效。如果要做增量采集可以加一个last_crawl表记录每个关键词的最后抓取时间或者直接用crawl_time过滤。对于商品库这种场景建议每天定时跑一次全量 upsert而不是只抓增量因为价格和标题会变。5. 本篇常见报错与排查对照采集入库这条链路上报错集中在几个地方数据库连接、编码、解析空值、请求被拦。下面按真实报错逐条对照。报错一pymysql.err.OperationalError: (1045, Access denied for user rootlocalhost)这是 MySQL 认证失败。检查.env里的MYSQL_PASSWORD是否正确注意密码里如果有特殊字符.env文件里不要加引号或者用单引号包裹。另外确认 MySQL 用户允许从localhost连接有些环境 root 只允许 socket 登录需要单独建一个应用用户CREATE USER jd_crawlerlocalhost IDENTIFIED BY 你的密码; GRANT INSERT, UPDATE, SELECT ON jd_goods.* TO jd_crawlerlocalhost; FLUSH PRIVILEGES;报错二UnicodeEncodeError: latin-1 codec cant encode characters这是连接字符集没设对。pymysql.connect里必须显式传charsetutf8mb4建库建表也要utf8mb4。如果还报错检查 MySQL 服务端的character_set_server用SHOW VARIABLES LIKE character%;查看必要时在my.cnf里改。报错三AttributeError: NoneType object has no attribute get_text解析时select_one返回None。京东页面结构会变某些商品卡片可能没有p-price或p-name。代码里每个select_one后面都要判空像前面parse_list_page里那样用条件表达式兜底。不要用try/except吞掉整个解析那样你连哪条数据有问题都不知道。报错四requests.exceptions.HTTPError: 403 Client Error请求被拦。检查User-Agent是否带了Referer是否设置。如果还是 403降低请求频率把time.sleep调到 2 秒以上。京东对高频请求敏感采集要克制不要并发几十个线程猛打。另外注意列表页翻页参数page是奇数递增的1、3、5…但用page1,2,3也能拿到数据只是可能有重复靠sku_id去重即可。报错五json.decoder.JSONDecodeError在价格接口价格接口返回的不是 JSON可能是空字符串或者 HTML。先打印原始返回看看text fetch(url) print(repr(text[:200]))如果是空说明skuIds参数格式不对必须是J_前缀加 SKU多个用逗号分隔不能有空格。如果是 HTML说明被拦了加Referer: https://item.jd.com/再试。报错六pymysql.err.DataError: (1406, Data too long for column title)标题超长。建表时title给了VARCHAR(512)清洗时title[:500]截断。如果还是报错检查是不是VARCHAR长度不够或者字符集导致字节数超限。utf8mb4下VARCHAR(512)最多存 512 个字符不是字节一般够用。报错七模型调用返回401 Unauthorized或local proxy failed如果你在清洗环节调用了 TaoToken 的模型接口出现 401 说明 Key 不对或没带。检查.env里的TAOTOKEN_API_KEY是否复制完整请求头里是不是Authorization: Bearer sk-xxx。local proxy failed通常是本地网络配置问题检查base_url是否写成了https://taotoken.net/api不要多加/v1或斜杠。如果用的是 OpenAI SDKbase_url填https://taotoken.net/api即可。报错八reading choices相关错误模型返回结构里没有choices字段通常是请求体格式不对或者模型 ID 写错。检查model字段是否是你确认可用的模型 ID请求体里messages是否是标准格式。用requests直接请求时完整路径是https://taotoken.net/api/v1/chat/completions请求头Content-Type: application/json。排查的通用思路是先隔离单层。抓取报错就单独跑fetch解析报错就喂本地 HTML入库报错就单独跑save_to_mysql传假数据。分层之后问题定位从「整个脚本挂了」变成「某一层挂了」效率差很多。6. 长期采集的凭证与通道管理建议采集脚本能跑通只是起点真正难的是让它稳定跑上几个月。这中间最大的变量不是京东页面改版而是凭证管理和调用通道的稳定性。这一节说几个实操建议。第一把模型调用封装成独立模块Key 从环境变量读Base URL 固定为 TaoToken 的 API 入口。这样无论你后面换模型、加模型业务代码都不动。封装示例import os import requests TAOTOKEN_BASE os.getenv(TAOTOKEN_BASE_URL, https://taotoken.net/api) TAOTOKEN_KEY os.getenv(TAOTOKEN_API_KEY, ) def chat(prompt, model你的模型ID, timeout30): url f{TAOTOKEN_BASE}/v1/chat/completions headers { Authorization: fBearer {TAOTOKEN_KEY}, Content-Type: application/json, } payload { model: model, messages: [{role: user, content: prompt}], temperature: 0.2, } resp requests.post(url, headersheaders, jsonpayload, timeouttimeout) resp.raise_for_status() data resp.json() return data[choices][0][message][content]这个函数可以拿来做标题归一化、类目打标、评论摘要。Key 和 Base URL 都在环境变量里换环境只改.env。第二给采集任务加日志和告警。不要只用print用logging写到文件按天切割。关键节点记录每页抓取条数、价格补全条数、入库条数、异常条数。如果某天入库条数突然掉到 0或者异常条数暴涨说明页面结构变了或者被拦了需要及时处理。第三控制采集频率和并发。单机串行、每页间隔 1-2 秒一天跑几千页没问题。不要为了快上几十个线程那样既容易被拦也给对方服务器造成压力。采集这件事稳定比快重要。第四凭证轮换要有预案。TaoToken 控制台可以随时创建和删除 Key建议每季度轮换一次或者团队成员离职时立即轮换。轮换时先在.env里更新重启任务确认新 Key 生效后再删除旧 Key。控制台地址 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 操作很快。第五数据备份。MySQL 里的商品库是你的核心资产定期mysqldump备份至少保留最近 7 天。如果数据量大考虑按crawl_time分区老数据归档。最后说一个实际经验采集项目的代码要尽量简单依赖要少。requests BeautifulSoup pymysql这三个库足够覆盖大部分场景不要引入重型框架。依赖越少环境问题越少长期维护成本越低。模型调用走 TaoToken 统一通道凭证集中管理业务代码只关心数据本身。这样一套下来你的京东商品库就能稳定地跑下去而不是跑一次就废。