1688商品信息爬虫:Selenium自动化采集实战指南
简介基于Selenium的1688商品信息爬虫项目资料包主要面向Python学习者、高校毕业设计及电商数据采集开发者用于解决1688商品信息的自动化抓取、Cookie管理和数据整理等问题。资源共19个文件包含7个Python脚本、3个Markdown说明文档、项目报告Word版以及用于展示结果的图片、环境配置JSON等辅助文件压缩包仅985KB整体轻量且结构清晰便于按需查阅和快速启动。目前已有80人学习下载。包内除核心爬虫代码外还配有详细设计文档、项目报告、运行依赖清单和辅助脚本涵盖Selenium采集流程、登录Cookie获取、多平台爬虫扩展思路等关键内容经多种环境测试稳定适合作为毕设、课设或项目初期的快速原型也可在具备一定基础后修改扩展以实现更多功能。1. 1688商品信息爬虫是什么Selenium自动化与电商数据抓取的一次正面交手做选品和市场调研的人大概都有过这种经历想批量核对1688上某个品类的价格区间、起订量、供应商实力手动开几十个标签页复制粘贴两个小时过去表格里才躺了一百来条数据还带着各种格式不统一的脏字段。1688商品信息爬虫Selenium自动化采集就是把这段手动流程切成脚本能执行的步骤驱动一个真实浏览器打开搜索页等待商品卡片渲染完成抽取标题、价格、链接再翻页、进详情最终把数据落成可查询的文件或数据库并附上一份采集报告。它和常见电商数据抓取里用requests直连的方案最大的区别是不需要逆向前端接口签名而是让完整浏览器把异步渲染的DOM呈现在眼前。这篇笔记适合两类人想尽快跑通首版脚本的Python爬虫新手以及需要长期维护采集任务、关注异常恢复和增量更新的后端工程师。前者能跟着全文跑通首版脚本后者可以直接跳到第4章避坑记录和第6章断点续采方案。2. requests直取1688为什么经常白屏Selenium选型与采集环境准备2.1 1688页面渲染方式与requests方案的三个短板很多人上手电商数据抓取第一反应都是requests带上Header直接请求搜索页地址。这个思路在旧网页时代成立放到1688这类平台上却频繁吃瘪。1688搜索页和详情页大量采用前端异步渲染页面加载时先返回一个骨架HTML再通过XHR接口把商品价格、库存、起订量这些字段慢慢填进DOM。用requests拿回来的页面很多只有标题和链接价格那一栏是空字符串整个页面的有效信息量打了对折。除去HTML不完整这个表象还有两层更麻烦的东西。第一层是异步接口的签名校验。1688的商品接口并不是拿着一个URL敲一下就有返回URL里带着动态加密参数过期时间短、生成逻辑不定期调整。想靠requests去拆接口需要跟随平台更新节奏反复做逆向维护成本非常高。第二层是访问行为特征。平台对请求频率、点击路径、请求上下文都有自己的判断逻辑一个不带浏览器环境的裸请求被打回来的概率远高于真实浏览。这几个短板叠加起来让Selenium自动化采集成了1688场景里最常见的选择——Selenium走的是一次完整浏览器加载页面JavaScript算完DOM里就是最终内容完全不需要关心中间那些接口签名。Selenium也不是银弹它的短板是速度和资源占用。一个页面完整渲染往往要几秒到十几秒在服务器上多开浏览器实例也吃内存。我一般在项目初期先用Selenium把字段结构和页面路径跑通以后如果接口规则一直没有变化再把高频请求迁到更轻量的方案上。但前期的字段验证环节Selenium的价值是替代不了的。2.2 WebDriver初始化的options参数和最小驱动配置Selenium 4的WebDriver初始化逻辑比3.x清爽很多。chromedriver只要在系统PATH里就可以直接webdriver.Chrome(optionsoptions)不需要再手动指定executable_path。真正值得花时间的是options参数——它们决定脚本在什么样的环境里跑也影响浏览器行为看起来像不像普通用户。from selenium import webdriver from selenium.webdriver.chrome.options import Options def build_driver() - webdriver.Chrome: options Options() options.add_argument(--headlessnew) # 无头模式新版Chrome推荐写法 options.add_argument(--no-sandbox) # 容器/root用户下必须加 options.add_argument(--disable-gpu) # 减少图形资源占用 options.add_argument(--window-size1920,1080) # 固定窗口尺寸影响响应式布局 options.add_argument( user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) driver.set_page_load_timeout(30) # 页面加载超时防止白屏拖死任务 driver.set_script_timeout(15) # 异步脚本执行超时 return driver这段代码里有几个参数值得逐个说清楚。headlessnew走的是新版本无头模式适合跑在服务器上不开窗口本地调试时我通常先不开headless因为能从窗口里直接看到页面卡在哪一步是验证码还是网络问题一眼就知道。no-sandbox是在Linux容器或root用户下跑Chrome的必需品不加的话启动就报Chrome failed to start。window-size看着不起眼实际上有些页面在窄窗口下会切成移动端样式class结构完全不同固定成1920×1080能避开一半定位问题。excludeSwitches和useAutomationExtension是关闭自动化提示的常规做法。它只能去掉部分可见特征不能指望浏览器从此隐形。最后两个set_page_load_timeout和set_script_timeout是给浏览器上紧发条1688被限流时页面可能一直转圈这两个超时能让脚本及时抛错而不是无限期卡死。关于chromedriver和浏览器版本的匹配Selenium启动时会检查大版本号。如果报session not created先看chromedriver版本再决定更新哪一边。我没有在代码里做自动下载driver的逻辑原因是把那一步编进代码会让首次运行变慢手动装一次其实最省事。2.3 第一条跑通的代码关键词搜索页加载与页面自检环境准备好之后第一条脚本只做一件事用关键词打开1688搜索结果页并确认页面真的加载出来了而不是被重定向。1688的搜索入口地址会有调整常见路径是s.1688.com下的offer_search.htmkeywords接关键词的URL编码。如果某天这个路径被重定向先在浏览器里手动搜一次从地址栏复制真实URL替换掉就好。from urllib.parse import quote from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def open_search(driver, keyword: str): # 关键词必须编码中文关键词在URL里会变成%XX格式 url fhttps://s.1688.com/selloffer/offer_search.htm?keywords{quote(keyword)} driver.get(url) # 显式等待15秒等商品卡片容器出现再继续而不是无脑sleep WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.XPATH, //div[contains(class,space-offer-card-box)])) ) # 常见风控页面会把地址改成login或captcha开头在这里提前暴露异常 if login in driver.current_url or captcha in driver.current_url: raise RuntimeError(页面被重定向到登录或验证码页) print(页面标题:, driver.title)wait这块我多说两句。EC.presence_of_element_located的意思是只要元素出现在DOM里就算通过不需要可见也不需要可点击。这对列表页卡片够用了因为卡片填充完基本就可以解析。有人把这一步写成time.sleep(10)临时跑可以长期跑就会出问题网络快的时候浪费时间网络慢的时候10秒还没加载完脚本接着就报找不到元素。显式等待把等待条件和时间绑定在一起是Selenium爬虫里少走弯路的起点。页面自检那一行的价值在于提前止损。如果没有这个检查后续find_elements可能拿到空列表脚本继续跑完整个循环之后你才发现数据文件是空的浪费一整轮。加三行判断失败时直接抛异常并记录日志排查范围能一下子缩小。3. 从搜索结果页到详情页1688商品字段抽取与翻页控制3.1 商品卡片的XPath定位策略与字段抽取拿到搜索页之后下一步是从一屏卡片里提取商品数据。我推荐的定位顺序是先定位所有卡片容器再在容器内做相对定位而不是在一个大列表里用全局XPath找标题、找价格。原因很简单——1688搜索页里除了商品卡片还有推荐位、广告位、活动位全局XPath很容易命中错误区块先拿到卡片容器再逐字段取至少能保证同一个卡片里的标题、价格、链接是配对的。def parse_card_list(driver, max_items30): # 先定位商品卡片容器再在容器内做相对定位 cards driver.find_elements(By.XPATH, //div[contains(class,space-offer-card-box)]) results [] for card in cards[:max_items]: item {} try: title card.find_element( By.XPATH, .//div[contains(class,title)] ).text price card.find_element( By.XPATH, .//span[contains(class,price)] ).text link card.find_element( By.XPATH, .//a[contains(class,title-content) or contains(class,offer)] ).get_attribute(href) item[title] title.strip() item[price_text] price.strip() item[link] link except Exception as exc: # 单张卡片失败不影响整页解析记录后继续 print(卡片字段抽取失败:, type(exc).__name__, str(exc)[:60]) if item: results.append(item) return results这里有两个细节值得留意。第一相对XPath写成.//div[contains(class,title)]前面的点表示从当前卡片内部找不要写成全局的//div否则可能把页面其他区域的标题抓到当前卡片里。第二class属性通常是多值比如title-text ellipsis-line用contains能避开完整class名差异但前提是你要选中的那个子串足够唯一。如果今天用某个class跑通明天页面改版失败优先打开开发者工具重新看DOM不要盲调XPath。字段抽取失败我先打日志跳过这张卡片而不是让整个循环崩掉。采集任务量一大个别卡片结构异常是常态程序要能扛住局部失败继续往下走。还有一个关于XPath文本提取的注意点.text拿到的是可见文本页面上价格如果被拆成多个span.text会把它们拼接在一起清洗时反而好处理。如果遇到.text返回空字符串可以先看innerHTML里是不是有隐藏节点。3.2 翻页与无限滚动的触发方式与防漏搜索页翻页一般会遇到两种情况一种是传统的“下一页”按钮点一下URL变化另一种是滚动到底部后自动加载。1688搜索页两种都可能出现所以脚本里我一般两个逻辑都带上先尝试滚动加载再尝试点击下一页最后比较URL是否真的变了。def scroll_to_load(driver, times3): # 懒加载场景分多次滚到底给网络留出响应时间 for _ in range(times): driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(1.5) def click_next_page(driver): # 用文本匹配“下一页”而不是依赖脆弱的class next_nodes driver.find_elements( By.XPATH, //button[contains(text(),下一页)] | //a[contains(text(),下一页)] ) if not next_nodes: return False, None next_nodes[0].click() return True, driver.current_urlscroll_to_load里的time.sleep(1.5)不能省滚动太快要给浏览器留出加载时间不然数据还没渲染出来。click_next_page用contains(text(),下一页)的好处是不依赖class页面再怎么改样式也能靠文案找到。点击之后别急着立刻解析先用WebDriverWait等URL里的页码参数变化或者等页面出现其他可见变化再开始下一步采集。翻页有一个常见误区拿到第2页数据后第3页因为加载慢还没出现脚本就直接判空退出任务本来该跑20页结果第3页就提前结束。我一般会在循环里记录当前页码和连续空页次数连续两页解析为空才算真正结束单页为空不触发退出。3.3 详情页字段清洗价格区间、起订量、标题规范化搜索页抽到的大多是摘要信息价格可能是一组区间真正做采购调研要看的是区间上下限和起订量。详情页字段更全但解析之前要先清洗字符串。import re def clean_price(raw: str): 把 ¥12.50 - ¥15.00 或 12.5元/件 转成最小/最大价格 if not raw: return None, None # 去掉千分位逗号后提取所有整数或小数 nums re.findall(r\d\.?\d*, raw.replace(,, )) if not nums: return None, None vals [float(n) for n in nums] return min(vals), max(vals)这段正则先把“12,500.00”这种带千分位的数字去逗号再提取避免把逗号当成小数分隔。价格区间单位可能是元/件、元/个、元/千克clean_price只取数字单位不回传——需要单位时再加一个字段存单位。另一个常见问题是标题里带换行或多个空格直接strip后把空白字符压缩成单个空格不然存到CSV里会多出看不见的换行。详情页解析比列表页麻烦我通常先构造一个通用函数def parse_detail(driver, url: str): driver.get(url) WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, //h1 | //div[contains(class,title)])) ) title driver.find_element(By.XPATH, //h1).text.strip() price driver.find_element(By.XPATH, //span[contains(class,price)]).text.strip() min_p, max_p clean_price(price) return {title: title, min_price: min_p, max_price: max_p, url: url}详情页标题、价格选择器很可能会变好在这种变化只影响单个详情页的字段不会把已经完成的搜索结果全废掉。跑任务时如果发现大量详情页超时优先怀疑页面改版其次再怀疑网络环境。4. 抓1688时常见的Selenium踩坑与排查记录4.1 元素定位失败懒加载、动态class与等待时机现象列表页明明有20个商品find_elements一张卡片都没找出来或者找到10个之后就断了。有时候页面刚加载完卡片里价格还是“--”这种占位符。原因1688列表页有懒加载机制滚动到视口附近才把详情渲染进去另外class属性是多个类名拼接contains匹配时前缀或后缀稍有变化就会命中失败。第三层原因是脚本跑得比数据快页面异步请求还没返回就急着解析。解决先用WebDriverWait等一个确定性比较高的元素比如翻页按钮或加载完成标记滚动后再抽取一次定位尽量绕开不稳定的class改用文本、链接地址或图片的alt属性。如果还是失败把driver.page_source写成HTML文件直接在文本里搜关键词确认某个class真实存在再写进代码。4.2 滑块验证码弹出现象、原因与应对现象跑到第20个关键词时driver.get之后页面停在一个滑块界面左右拖动也过不去或者拖完直接白屏。原因短时间请求频率超过了阈值平台对浏览器环境产生了怀疑。滑块验证码本身是风控的一部分Selenium拖拽成功率并不高指望程序正面拖过去不现实频繁尝试反而会加重风控。解决脚本每次解析前先探测页面里有没有滑块容器一旦识别到就停止采集并把当前URL写入本地日志等10到15分钟、人工介入处理后再继续。更稳妥的方案是控制整体频率把每个关键词之间的间隔拉长不要在同一个时间窗口里密集点击。不要用硬编码坐标去猜滑块位置平台每次生成的轨迹缺口位置都不同。4.3 Selenium被识别页面空白、立即跳转、接口401现象打开搜索页页面标题显示正常商品区域却是空的或者一打开就跳到非1688的空白页有的场景是页面能浏览但点击详情时接口返回401。原因程序化浏览存在浏览器指纹层面的特征比如navigator.webdriver标记、自动化提示、固定窗口尺寸、缺少历史Cookie等。有些特征是options能抹掉的有些需要运行时改但这类办法边界很敏感平台一更新判断逻辑就可能再次失效。解决先保证基础配置不过度暴露——关掉自动化开关用真实user-agent窗口尺寸设成常见分辨率。对长期任务更可靠的做法是让浏览器环境接近日常使用痕迹用一个长期登录状态的浏览器用户目录启动让平台看到一个有历史的账号而不是每次新建的陌生会话。这类办法只降低误伤概率解决不了所有识别问题所以日志记录还要跟上。4.4 高频采集后列表页翻车频控与请求节奏现象采集进行到第40页时列表突然空了卡片数量从20慢慢变成0继续往后翻页面开始要求登录。原因当前访问特征已经被纳入限流名单。平台对页面浏览节奏有自己的期望太快、太规律都会被标记固定间隔的sleep其实也容易被识别因为规律的等待时间在统计学上过于完美。解决把固定sleep改成随机区间例如1.5到4秒之间随机取值每翻5到10页停一次做点页面外的动作比如滚动到页面底部再回来模拟真实浏览再不行就分时段跑把任务拆成早、中、晚三个批次。核心思路是让请求分布接近人的自然浏览行为。5. 数据落库与项目报告把抓到的1688商品信息变成可交付的资产5.1 存储选型CSV、JSON还是SQLite存储方式适合场景主要痛点CSV一次性调研交给运营用Excel打开并发写入容易冲突价格字段容易变成文本JSON后续程序处理字段灵活人直接看困难大文件加载慢SQLite增量采集、去重、断点续跑需要会写几条SQL如果只是抓几百个商品做临时调研CSV就够用了编码要存成utf-8-sig不然Excel打开直接乱码。如果要长期维护一个采集任务SQLite是更好的选择。它的表结构就是天然去重依据重复的商品ID可以用唯一约束挡在数据库层避免数据文件里出现几百条同一个商品。建表我一般这么写CREATE TABLE IF NOT EXISTS items ( item_id TEXT PRIMARY KEY, title TEXT, min_price REAL, max_price REAL, url TEXT, fetched_at TEXT DEFAULT (datetime(now, localtime)) );字段说明item_id用商品链接里的ID保证业务上唯一min_price和max_price用REAL方便后续做价格区间统计fetched_at记录采集时间增量任务里能看出数据时效性。5.2 项目报告应有的结构与生成思路项目报告不是用来摆样子的核心作用是让接手的人看懂这次采集的可信度。一份能落地的报告至少要包含任务配置关键词、页码范围、启动时间、采集结果总量、唯一商品数、价格字段缺失率、耗时、异常请求数量、失败记录样例。这些数字拼在一起别人才能判断你的数据能不能支撑后续结论。def generate_report(db_path, report_pathreport.md): import sqlite3 conn sqlite3.connect(db_path) total conn.execute(SELECT COUNT(*) FROM items).fetchone()[0] with_price conn.execute(SELECT COUNT(*) FROM items WHERE min_price IS NOT NULL).fetchone()[0] unique conn.execute(SELECT COUNT(DISTINCT item_id) FROM items).fetchone()[0] with open(report_path, w, encodingutf-8) as f: f.write(# 1688采集报告\n\n) f.write(f- 采集结果{total} 条\n) f.write(f- 唯一商品{unique} 条\n) f.write(f- 价格字段覆盖率{with_price / total * 100:.1f}%\n) conn.close()这段代码只展示了覆盖率计算真实任务里我还会把时间跨度、失败样本数量一起写进去。报告不用做成PDF那种重格式Markdown就够团队里任何人打开都能改。项目报告重点在“数据的可信度说明”不在排版精美。5.3 监控采集运行状态日志、命中率与失败样本采集脚本不是跑完就结束任务崩掉之后的排查效率取决于日志记录得全不全。我一般加两层第一层是运行日志每个关键词记录开始时间、页码、解析条数、耗时第二层是页面快照一旦页面结构异常或触发风控把page_source存下来等任务结束后慢慢比对。import logging import os logging.basicConfig( filenamecrawler.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) def log_page_snapshot(driver, tag): os.makedirs(snapshots, exist_okTrue) with open(fsnapshots/{tag}.html, w, encodingutf-8) as f: f.write(driver.page_source)用法失败时调用log_page_snapshot(driver, ferror_{keyword}_{page})后面排查时直接打开对应HTML文件看问题点不需要重新跑整个任务。日志里还要关注“解析条数明显偏少”的情况一旦连续两页条数为0可以认为采集结束而不是还在空转浪费资源。6. 增量采集与断点续采给1688采集任务加一道后悔药爬虫任务维护久了最让人头疼的其实不是反爬而是跑到一半崩了、数据只抓了一半、重启之后又从头开始。1688商品信息爬虫这类任务加一个简单的断点续采功能就能省掉大量重复劳动。核心做法有两步。第一步用state.json记录每个关键词当前跑到第几页以及任务已经处理到哪个关键词。第二步把已抓取的商品ID写入SQLite的唯一约束重启后跳过已经存在的数据。这两步加起来任务崩溃后可以从断点继续而且不会产生重复记录。import json import os def save_state(state: dict): with open(state.json, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2) def load_state(): if not os.path.exists(state.json): return {} with open(state.json, r, encodingutf-8) as f: return json.load(f)每次翻页成功后调用一次save_state把keyword、page、下一个关键词下标写进文件load_state在任务启动时读取从断点处继续。写入数据库时用唯一约束挡重复INSERT OR IGNORE INTO items(item_id, title, min_price, max_price, url) VALUES(?, ?, ?, ?, ?);不需要先查一遍再插入唯一约束会帮你去重插入影响行数为0说明这条商品之前已经抓过了。验证方法很简单同一个关键词跑两遍第二遍结束时检查唯一商品总数没有变化再手动改state.json里的页码重启脚本看它是否从改动后的页码继续翻页而不是回到第1页。这两条通过断点续采就成立。我自己最开始没有做断点记录网络抖动导致脚本崩过一次从头再跑发现已经抓过的数据夹在中间又花了一轮时间去重。后来把state.json和SQLite唯一索引配上崩了就直接续跑省下的时间都用来调字段清洗逻辑。希望这篇笔记能让你少踩一遍我之前踩过的坑把精力更多放在数据质量上希望帮到你。本文还有配套的精品资源点击获取