简介:这是一套面向数据采集工程师、电商分析从业者及Python爬虫初学者的实战型电商平台数据抓取工具,解决多平台商品信息难以批量获取、反爬适配成本高、配置灵活性不足等痛点。资源共20个文件,含12个核心Python脚本(如spiders/、pipelines_*.py、settings.py)、2个Shell启动脚本(init.sh/run.sh)、1个JSON配置文件(setting.json)、1个CFG模板、1个README.md和1个说明文档(.docx),总大小仅49KB,轻量易部署。已有90人学习下载,适合快速上手京东、淘宝、天猫三大平台的商品标题、价格、描述、评价等全量字段采集。用户可直接通过修改关键词与Cookie配置实现定向抓取,无需重写逻辑;项目结构遵循Scrapy标准规范,包含中间件、管道、Item定义等完整模块,附带cookie格式化工具与CSV/MongoDB双存储支持,兼顾教学演示与生产调试需求。
1. 这不是“一键采集神器”,而是一套需手动注入登录态、适配三端反爬策略的 Scrapy 1.4 实战工程
你打开run.sh,执行scrapy crawl jd_search,结果返回403 Forbidden或redirect to login—— 这才是这套代码的真实起点。它不封装 Selenium,不内置 Cookie 自动刷新,也不提供 Web UI 控制台;它是一份基于 Python 2.7 + Scrapy 1.4 的可调试、可断点、可逐层绕过平台反爬的工程化爬虫骨架,专为需要长期稳定抓取京东、淘宝、天猫商品列表页与详情页的中等规模数据任务设计。它解决的不是“能不能跑起来”,而是“如何在京东搜索页返回 302 跳转、淘宝商品页嵌套多层 iframe、天猫详情页依赖动态 JS 渲染 DOM 的混合场景下,用纯 Scrapy 同步机制完成字段级可控采集”。适合已有 Python 基础、能读middlewares.py里process_request逻辑、愿为每个平台单独维护cookie_format.py中 domain 映射规则的工程师或数据分析师,而非零基础想“填个关键词就出 Excel”的用户。
该工程将反爬对抗拆解为三个可验证层次:会话层(Cookie 注入与有效期管理)→ 请求层(User-Agent/Referer/Headers 动态构造)→ 解析层(XPath 与正则双轨提取,规避 JS 渲染陷阱)。例如,京东商品价格藏在#J_price > .p-price > .price,但页面源码中实际是<span class="price"># cookie_format.py import cookielib def load_jd_cookie(cookie_file): """从 Netscape 格式 cookie.txt 加载京东 Cookie,返回 dict""" cookie_jar = cookielib.LWPCookieJar() try: cookie_jar.load(cookie_file, ignore_discard=True, ignore_expires=True) except Exception as e: raise ValueError("Failed to load JD cookie: %s" % str(e)) # 构建 domain -> {name: value} 映射 cookies_dict = {} for cookie in cookie_jar: if cookie.domain == '.jd.com' and cookie.name in ['pt_key', 'pt_pin', 'trackid']: if cookie.domain not in cookies_dict: cookies_dict[cookie.domain] = {} cookies_dict[cookie.domain][cookie.name] = cookie.value return cookies_dict.get('.jd.com', {})
该函数输出结构为:
{ 'pt_key': 'xxx', 'pt_pin': 'yyy', 'trackid': 'zzz' }此结构被spiders/jd_spider.py的start_requests()方法直接注入到scrapy.Request的cookies参数中:
# spiders/jd_spider.py def start_requests(self): jd_cookies = load_jd_cookie('cookies/jd_cookie.txt') search_url = "https://search.jd.com/Search?keyword=%s&enc=utf-8" % self.keyword yield scrapy.Request( url=search_url, cookies=jd_cookies, callback=self.parse_search, headers={'User-Agent': self.ua_pool.random} )注意:淘宝 Cookie 必须包含
t和_tb_token_字段,天猫则需cna和cookie2。cookie_format.py中load_taobao_cookie()与load_tmall_cookie()分别校验这些字段是否存在,缺失则抛出ValueError,避免静默失败。
2.3 配置文件setting.json的字段约束与平台路由逻辑
setting.json是工程的策略中枢,其结构强制约束三端行为:
{ "platform": "jd", "keyword": "iPhone 15", "max_pages": 5, "output_format": "csv", "cookie_file": "cookies/jd_cookie.txt", "proxy_enabled": false, "delay_range": [1.5, 3.0] }spiders/__init__.py根据platform字段动态导入对应 spider:
# spiders/__init__.py import json from scrapy.crawler import Crawler def get_spider_by_platform(): with open('setting.json') as f: config = json.load(f) platform = config.get('platform', 'jd').lower() if platform == 'jd': from .jd_spider import JDSpider return JDSpider elif platform == 'taobao': from .taobao_spider import TaobaoSpider return TaobaoSpider elif platform == 'tmall': from .tmall_spider import TmallSpider return TmallSpider else: raise ValueError("Unsupported platform: %s" % platform)run.sh调用时传入参数,触发配置加载:
#!/bin/bash # run.sh PLATFORM=${1:-jd} KEYWORD=${2:-"手机"} sed -i "s/\"platform\": \"[^\"]*\"/\"platform\": \"$PLATFORM\"/" setting.json sed -i "s/\"keyword\": \"[^\"]*\"/\"keyword\": \"$KEYWORD\"/" setting.json scrapy crawl $(python -c "from spiders import get_spider_by_platform; print(get_spider_by_platform().__name__.lower())")3. 三端解析逻辑拆解:XPath 定位、正则提取与 JS 渲染规避策略
3.1 京东商品列表页:DOM 结构与价格字段的双重提取路径
京东搜索结果页(search.jd.com)的 HTML 源码中,商品价格存在两种形态:
- 静态 DOM:
<div class="p-price"> <i>¥</i><em>699.00</em> </div>→ 可用 XPath//div[@class='p-price']/em/text()提取; - 动态 data 属性:
<div class="p-price">def parse_search(self, response): for sel in response.xpath('//li[@class="gl-item"]'): item = JdItem() # 轨道一:从 em 标签提取 price_em = sel.xpath('.//div[@class="p-price"]/em/text()').extract_first() # 轨道二:从>def parse_item(self, response): # 提取页面内嵌的 g_config 变量 script_text = response.xpath('//script[contains(text(), "g_config")]/text()').extract_first() if not script_text: self.logger.warning("No g_config found in Taobao item page") return # 正则匹配 JSON 对象 config_match = re.search(r'g_config\s*=\s*({.*?});', script_text, re.DOTALL) if not config_match: self.logger.warning("Failed to extract g_config JSON") return try: g_config = json.loads(config_match.group(1)) item = TaobaoItem() item['title'] = g_config.get('itemTitle', '') item['price'] = float(g_config.get('price', {}).get('price', '0')) item['sales'] = int(g_config.get('sellCount', '0')) # 商品描述在 g_config['desc'] 中,但为 HTML 字符串,需进一步清洗 desc_html = g_config.get('desc', '') item['description'] = re.sub(r'<[^>]+>', '', desc_html)[:500] # 去除 HTML 标签,截断 yield item except (ValueError, TypeError) as e: self.logger.error("JSON parse error in g_config: %s", str(e))此方法绕过了 Selenium 启动浏览器的开销,利用 Scrapy 的
response.text直接解析页面源码中的 JS 变量。re.DOTALL标志确保跨行匹配,re.sub(r'<[^>]+>', '', ...)则是轻量级 HTML 清洗,避免pipelines_csv.py写入 CSV 时因逗号或换行符导致格式错乱。3.3 天猫商品评论:AJAX 接口逆向与分页参数构造
天猫评论数据不直接渲染在商品页,而是通过 AJAX 请求
https://rate.tmall.com/list_detail_rate.htm?itemId=...&sellerId=...¤tPage=1获取。tmall_spider.py在parse_item()中构造该请求:def parse_item(self, response): # 从页面提取 itemId 和 sellerId item_id_match = re.search(r'"itemId"\s*:\s*(\d+)', response.text) seller_id_match = re.search(r'"sellerId"\s*:\s*(\d+)', response.text) if not (item_id_match and seller_id_match): return item_id = item_id_match.group(1) seller_id = seller_id_match.group(1) # 构造评论接口 URL for page in range(1, self.settings.getint('TMALL_MAX_COMMENT_PAGES', 3) + 1): comment_url = "https://rate.tmall.com/list_detail_rate.htm?itemId={}&sellerId={}¤tPage={}&append=0&content=1&picture=0".format( item_id, seller_id, page ) yield scrapy.Request( url=comment_url, meta={'item_id': item_id, 'page': page}, callback=self.parse_comments, headers={'Referer': response.url} ) def parse_comments(self, response): # 天猫评论接口返回的是 JSONP,需去除 callback 包裹 jsonp_text = response.text.strip() if jsonp_text.startswith('_RateListCallback(') and jsonp_text.endswith(');'): json_text = jsonp_text[18:-2] else: json_text = jsonp_text try: data = json.loads(json_text) rate_list = data.get('rateList', []) for rate in rate_list: comment_item = TmallCommentItem() comment_item['item_id'] = response.meta['item_id'] comment_item['user_nick'] = rate.get('nick', '') comment_item['rating'] = rate.get('rated', 0) comment_item['comment'] = rate.get('rateContent', '').strip() comment_item['date'] = rate.get('date', '') yield comment_item except ValueError as e: self.logger.error("Failed to parse Tmall comment JSON: %s", str(e))Referer头部必须设置为原始商品页 URL,否则接口返回空数据。currentPage参数控制分页,append=0表示不追加历史数据,content=1表示包含评论正文。4. 数据管道与存储:CSV 与 MongoDB 的字段映射与异常容错
4.1
pipelines_csv.py的字段标准化与编码处理pipelines_csv.py不是简单调用csv.writer,而是对每个字段执行强制类型转换与编码归一化:# pipelines_csv.py import csv import codecs import cStringIO from scrapy.exporters import CsvItemExporter class UnicodeWriter: def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): self.queue = cStringIO.StringIO() self.writer = csv.writer(self.queue, dialect=dialect, **kwds) self.stream = f self.encoder = codecs.getincrementalencoder(encoding)() def writerow(self, row): self.writer.writerow([s.encode("utf-8") if isinstance(s, unicode) else str(s) for s in row]) data = self.queue.getvalue() data = data.decode("utf-8") self.stream.write(data) self.queue.truncate(0) class CsvPipeline(object): def __init__(self): self.files = {} def open_spider(self, spider): filename = "output_%s.csv" % spider.name self.files[spider] = open(filename, 'w+b') self.exporter = CsvItemExporter( self.files[spider], include_headers_line=True, join_multivalued=',', encoding='utf-8' ) self.exporter.start_exporting() def close_spider(self, spider): self.exporter.finish_exporting() self.files[spider].close() def process_item(self, item, spider): # 强制字段类型标准化 for field in ['price', 'sales', 'rating']: if field in item and item[field] is not None: try: item[field] = float(item[field]) if field == 'price' else int(item[field]) except (ValueError, TypeError): item[field] = 0 # 默认值容错 # 清洗 title 字段:去除首尾空格、换行、多余空格 if 'title' in item: item['title'] = re.sub(r'\s+', ' ', item['title'].strip()) # 确保 description 不为空字符串 if 'description' in item and not item['description'].strip(): item['description'] = 'N/A' return itemUnicodeWriter类解决 Python 2.7 下csv模块对 Unicode 的兼容问题,process_item()中的re.sub(r'\s+', ' ', ...)将\n\t iPhone 15 Pro \n归一化为"iPhone 15 Pro",避免 CSV 解析器将换行符误判为新行。4.2
pipelines_mongodb.py的唯一索引与更新策略MongoDB 存储采用
upsert模式,以platform + item_id为联合唯一键,避免重复插入:# pipelines_mongodb.py from pymongo import MongoClient from scrapy.conf import settings class MongoPipeline(object): def __init__(self): connection = MongoClient( settings['MONGODB_SERVER'], settings['MONGODB_PORT'] ) db = connection[settings['MONGODB_DB']] self.collection = db[settings['MONGODB_COLLECTION']] def open_spider(self, spider): # 创建唯一索引:platform 和 item_id 组合唯一 self.collection.create_index( [("platform", 1), ("item_id", 1)], unique=True ) def close_spider(self, spider): pass def process_item(self, item, spider): # 构建查询条件 query = { "platform": spider.name, "item_id": item.get('item_id') or item.get('url', '').split('/')[-1] } # 更新或插入 self.collection.update_one( query, {"$set": dict(item)}, upsert=True ) return itemsettings.py中需配置 MongoDB 连接参数:# settings.py MONGODB_SERVER = "localhost" MONGODB_PORT = 27017 MONGODB_DB = "ecommerce" MONGODB_COLLECTION = "products"item_id的提取逻辑兼顾三端:京东 URL 如https://item.jd.com/100012345678.html,取100012345678;淘宝 URL 如https://item.taobao.com/item.htm?id=1234567890,取1234567890;天猫 URL 如https://detail.tmall.com/item.htm?id=9876543210,同样取数字 ID。or item.get('url', '').split('/')[-1]作为兜底,确保即使item_id字段缺失也能生成唯一键。5. 反爬对抗实战:Referer 动态构造、User-Agent 轮询与请求延迟控制
5.1
middlewares.py中的 Referer 中间件实现京东搜索页对
Referer头部敏感,若缺失或错误,会重定向至登录页。middlewares.py中的JDRefererMiddleware动态构造 Referer:# middlewares.py import random from scrapy.downloadermiddlewares.retry import RetryMiddleware class JDRefererMiddleware(object): def process_request(self, request, spider): if spider.name == 'jd_spider': # 构造合法 Referer:必须是 jd.com 域下的搜索页或首页 referers = [ "https://www.jd.com/", "https://search.jd.com/Search?keyword=%s" % spider.keyword, "https://search.jd.com/Search?keyword=%s&page=2" % spider.keyword ] request.headers['Referer'] = random.choice(referers) class TaobaoRefererMiddleware(object): def process_request(self, request, spider): if spider.name == 'taobao_spider': # 淘宝要求 Referer 为 item.taobao.com 或 www.taobao.com referers = [ "https://www.taobao.com/", "https://item.taobao.com/item.htm?id=%s" % (request.url.split('id=')[-1].split('&')[0] if 'id=' in request.url else '1234567890') ] request.headers['Referer'] = random.choice(referers)该中间件在
settings.py中启用:# settings.py DOWNLOADER_MIDDLEWARES = { 'middlewares.JDRefererMiddleware': 543, 'middlewares.TaobaoRefererMiddleware': 544, 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550, }543和544的优先级高于默认的RetryMiddleware(550),确保 Referer 在重试前已设置。5.2
settings.py中的 UA 池与延迟策略UA 池定义在
settings.py中,避免单一 UA 被封禁:# settings.py USER_AGENT_LIST = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1" ] # 随机 UA 中间件 class RandomUserAgentMiddleware(object): def process_request(self, request, spider): ua = random.choice(spider.settings.get('USER_AGENT_LIST', [])) request.headers.setdefault('User-Agent', ua)请求延迟由
DOWNLOAD_DELAY和RANDOMIZE_DOWNLOAD_DELAY控制:# settings.py DOWNLOAD_DELAY = 2.0 RANDOMIZE_DOWNLOAD_DELAY = TrueDOWNLOAD_DELAY = 2.0设置基础延迟为 2 秒,RANDOMIZE_DOWNLOAD_DELAY = True使实际延迟在[2.0 * 0.5, 2.0 * 1.5] = [1.0, 3.0]秒间随机,模拟人工浏览节奏。此参数与setting.json中的"delay_range": [1.5, 3.0]互补——前者控制全局,后者可被spiders/中的custom_delay属性覆盖。5.3 自定义重试逻辑:针对 403/503 的专项处理
默认
RetryMiddleware对403仅重试一次,但京东常返回403并附带X-TraceID头部用于追踪。middlewares.py中增强重试:class EnhancedRetryMiddleware(RetryMiddleware): def process_response(self, request, response, spider): if response.status in [403, 503]: # 记录 TraceID 辅助排查 trace_id = response.headers.get('X-TraceID', 'N/A') spider.logger.warning("Received %s from %s, TraceID: %s", response.status, request.url, trace_id) # 增加重试次数:403 最多重试 3 次,503 重试 2 次 retry_times = request.meta.get('retry_times', 0) if response.status == 403 and retry_times < 2: reason = "403 Forbidden" return self._retry(request, reason, spider) or response elif response.status == 503 and retry_times < 1: reason = "503 Service Unavailable" return self._retry(request, reason, spider) or response return response此中间件替换默认
RetryMiddleware,在settings.py中注册:DOWNLOADER_MIDDLEWARES = { 'middlewares.EnhancedRetryMiddleware': 550, }当
scrapy日志出现Retrying <GET https://search.jd.com/...> (failed 1 times): 403 Forbidden时,表明该机制已生效,且X-TraceID可提供给平台客服定位风控原因。本文还有配套的精品资源,点击获取