ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django实战:构建多平台电商数据爬虫与清洗入库系统

Django实战:构建多平台电商数据爬虫与清洗入库系统 简介一份聚焦电商数据采集的Python爬虫分析系统源码面向高校学生、课程设计与毕业设计人群适合用来学习爬虫开发与Django项目搭建。系统实现了对京东、淘宝、苏宁、亚马逊中国四个主流电商平台的商品信息抓取字段涵盖商品名称、商家名称、商品ID、价格、评分、评论数量、图片、库存地址等抓取结果可自动写入数据库并支持按关键词、页数和搜索索引灵活控制采集范围。源码包共两千个文件其中以一千七百三十七个Python源文件为核心涵盖爬虫脚本、Django视图与模型另有一百一十七个HTML模板用于后台页面展示三十个JavaScript与九个CSS文件完善前端交互四十九个TXT文档提供说明与配置参考整体压缩后约一百三十六MB结构清晰便于按模块研读。目前已有五十七人学习。对需要快速搭建电商数据采集与分析原型的读者这套源码提供了完整的抓取、入库、展示闭环是一份值得动手拆解的课程设计或毕业设计参考材料。1. 为什么多平台电商爬虫必须做成 Django 工程一次课程设计让我意识到用单个脚本去抓京东、淘宝、苏宁和亚马逊中国并不是最难的难的是每个平台翻页参数不同同一商品在不同平台价格标签不一样抓到 10 万条数据后如果还是躺在 CSV 里根本没法做竞品数据分析。于是我把这个爬虫做成了 Django 项目抓取、清洗、入库、查询、分析都放在同一个工程里。对这个项目而言Django 不只是后台框架它把 requests 爬虫、ORM 存储和数据分析串成一条完整链路。适合正在做 python 作业、课程设计或毕业设计的人也适合想知道电商竞品数据怎么落到数据库里做进一步分析的工程师。2. 商品解析与并发抓取requests 封装与平台差异适配2.1 四个电商平台的请求差异京东、淘宝、苏宁、亚马逊中国这四个平台的搜索 URL 和翻页字段几乎没有完全一致的。源码里把每个平台抽成了独立的 request builder核心思想是“统一入口各自实现”。以京东为例搜索请求在抓包里通常是search.jd.com/Search带上keyword、page和encutf-8淘宝更依赖 Cookie 和签名参数q是关键词入口苏宁经常把关键词拼在搜索路径里亚马逊中国则习惯用k参数和b-page这类翻页字段。这些字段一旦改版就会失效所以源码里把每个平台的请求构造集中在一个函数里方便单独修改。如果只写一个requests.get然后到处 copy后面两个维护成本会很高。常见做法是每个平台一个build_params(platform, keyword, page, index)方法返回平台对应的参数字典这样上层调度逻辑完全不用感知每个平台的差异。2.2 关键词、页数、搜索索引各负责什么关键词就是搜索词直接对应业务侧想监控的商品类目例如“机械键盘”“洗面奶”。页数控制抓取深度一般从第 1 页开始抓完一页再翻下一页直到达到预定的pages数量。搜索索引在源码里承担两个作用有些平台用它定位排序方式例如综合排序、销量排序有些平台用它作为分页偏移量比如先抓index0的那批再抓index1的那批。抓这类数据时务必把关键词、页数、索引同时记录到数据库否则后面做竞品数据分析时根本不知道这条记录来自哪个搜索条件。这里的核心思路是让抓取任务可复现。常见错误是为了省事只存商品信息不存keyword和platform字段结果同一个商品被不同关键词抓到多次去重逻辑无法判断该保留哪一条。2.3 requests Session 封装与重试代码直接使用requests.get会在每个请求新建连接抓几十页后 TCP 连接开销很大。项目里一般会用requests.Session复用连接再挂上 urllib3 的重试策略。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def make_session(retries3): session requests.Session() retry_cfg Retry( totalretries, backoff_factor1, status_forcelist[500, 502, 503, 504], allowed_methods[GET], ) adapter HTTPAdapter(max_retriesretry_cfg) session.mount(https://, adapter) session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, }) return session session make_session() def fetch_search_page(platform, keyword, page, index0): # 平台差异集中在 build_params 中 params build_params(platform, keyword, page, index) resp session.get(entry_url(platform), paramsparams, timeout8) resp.raise_for_status() return resp.text这段代码的关键点是Retry 只对 GET 请求生效因为电商搜索场景不允许请求被自动重复提交造成副作用timeout8是保守值移动网络或平台慢时可以调到 15但不要不给 timeout否则一个卡死请求会让整个任务挂住。Session 保持 Cookie 对淘宝这类依赖 Cookie 的站点尤其重要。需要说明的是build_params和entry_url需要按源码里的平台配置实现京东、淘宝、苏宁、亚马逊各有不同的参数映射表。参数含义常见取值注意事项platform平台标识jd / taobao / suning / amazon影响 entry_url 和 build_params 分支keyword搜索关键词任意字符串requests 会自动做 URL 编码page页面序号1, 2, 3...部分平台 page 要乘以 2index搜索索引/排序偏移0, 1, 2...不是所有平台都有没有则留 0注意实际页面字段经常改版以抓包结果为准不要长期写死某一年份的参数名。2.4 用线程池抓多页时别忽略限速抓 100 页商品列表逐页串行可能要几分钟用concurrent.futures.ThreadPoolExecutor可以把时间缩短到几十秒。但并发数需要根据目标站点忍耐度决定。源码里我一般把线程数控制在 5 以下再加上后面第 5 章的限速器避免被平台识别成攻击流量。并发抓取时每个线程必须使用同一个 Session 吗不requests.Session不是严格线程安全的常见做法是每个线程建一个独立 Session或者用 thread-local 存放 Session。每一个抓取结果都通过同一个入库函数写入数据库避免多个线程各自维护缓存。这里再补充一个判断标准如果你用 20 个线程抓 100 页但几乎没有报错说明该平台给列表页的阈值很高如果出现大量 418、429 或验证码页面优先把线程数降下来而不是去绕验证码。后面第 5 章会展开讲这类限速逻辑。3. 数据入库与清洗从 HTML 片段到可分析的 Product 表3.1 为什么用 Django ORM 而不是 pandas抓下来的数据经过 BeautifulSoup 或 lxml 解析后是内存里的一组字典。如果只用 pandas 做分析每次重跑脚本都要重新抓一遍数据没有增量更新能力。Django ORM 的优势在项目里体现为三点一是表结构由models.py统一管理字段变化时用 migration 平滑升级二是写入时可以直接做唯一键去重避免重复记录三是后面做按平台、关键词、时间聚合分析时Django 的 query set 可以直接翻译成 SQL效率比逐行遍历列表高。所以这个项目的存储层选择了 SQLite/MySQL 加 Django ORM。开始抓取前先定义好模型。3.2 Product 模型与字段设计商品基本信息包括名称、商家、商品 ID、价格、评分、评论数量、图片和库存地址这些在模型里都要有对应字段并且把platform、keyword、product_id组合成唯一键。这样同一个商品在不同平台能独立存在同一个平台抓同一个关键词也不会重复插入。from django.db import models class Product(models.Model): platform models.CharField(max_length16, db_indexTrue) keyword models.CharField(max_length128, db_indexTrue) product_id models.CharField(max_length64) name models.CharField(max_length255) shop models.CharField(max_length128, blankTrue) price models.FloatField(nullTrue, blankTrue) rating models.FloatField(nullTrue, blankTrue) comment_count models.IntegerField(nullTrue, blankTrue) image_url models.URLField(blankTrue) stock_address models.CharField(max_length128, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together [(platform, product_id, keyword)]字段设置上的关键点是price 用 FloatField 而不是 DecimalField原因是异步抓取过程中价格经常出现缺值FloatField 的 null 处理更轻如果做财务级精度再升级成 DecimalField。rating 同样允许为空因为苏宁部分商品不一定有评分。唯一键的第三个字段选keyword是个细节同一个商品在不同关键词下可能出现在不同搜索结果中如果unique_together里没有 keyword第一次解析到这条商品后第二次用另一个关键词抓到它时会被当作重复而跳过导致搜索条件和商品关系的关联丢失。3.3 清洗逻辑价格、评论数和评分平台的原始文本几乎不适合直接入库。价格可能出现“¥2,399”“2399元”评论数可能出现“2.3万”“--”评分会有“4.8分”。这里要做两层清洗第一层提取数字第二层处理单位。import re def clean_price(value): if not value: return None text str(value).replace(¥, ).replace(元, ).replace(,, ).strip() try: return float(text) except ValueError: return None def clean_count(value): if not value or 暂无 in str(value): return 0 text str(value).replace(, ).strip() m re.search(r([\d.])(万)?, text) if not m: return 0 num float(m.group(1)) if m.group(2) 万: num * 10000 return int(num) def clean_rating(value): if not value: return None m re.search(r([\d.]), str(value)) return float(m.group(1)) if m else Noneclean_price的目标是兼容人民币符号、中文字符和千分位逗号float 转换失败时返回 None而不是抛异常中断新一轮抓取。clean_count把“2.3万”先拆出 2.3 再乘 10000最后转成整数。clean_rating只取字符串中的第一个浮点数所以“4.8分”和“4.8/5”都能得到 4.8。这三个函数全部设计成“异常输入返回空值”这样调度循环不会因为一条脏数据崩掉。常见错误是清洗函数里抛 ValueError导致一万条里一条数据出问题整个循环停掉。这是处理脏数据的常见坑。原始字段清洗函数输入示例输出priceclean_price¥2,3992399.0comment_countclean_count2.3万23000comment_countclean_count暂无评价0ratingclean_rating4.8分4.8ratingclean_rating-None3.4 入库与幂等去重保存数据时使用update_or_create它的语义是“存在就更新不存在就插入”前提是 objects 能通过唯一键定位到记录。def save_product(data): filtered { name: data[name], shop: data.get(shop, ), price: clean_price(data.get(price)), rating: clean_rating(data.get(rating)), comment_count: clean_count(data.get(comment_count)), image_url: data.get(image_url, ), stock_address: data.get(stock_address, ), } obj, created Product.objects.update_or_create( platformdata[platform], product_iddata[product_id], keyworddata[keyword], defaultsfiltered, ) return obj, createdupdate_or_create的三个定位字段来自唯一键defaults里放动态变化的信息。第一次抓某关键词时created为 True第二次再抓同一关键词同商品时created为 False不会产生第二条记录。这种方式也让后续抓取任务具备幂等性重跑一次任务不会把数据库撑爆。数据入库后要验证一下字段分布比如价格是否为负数、评论数是否超过合理范围。常见做法是抓完后直接执行一条聚合查询看 price 为 NULL 的比例是否超过 20%超过就说明解析器的价格选择器可能失效了。4. Django 管理命令与查询 API把爬虫变成可复用任务4.1 用 Management Command 替代手动运行脚本如果爬虫逻辑写在 views.py 里每次抓取都要开 Web 服务很不合理。Django 的 management command 能让抓取任务在命令行触发同时复用项目的 settings、数据库配置和 ORM。源码里将抓取入口封装成python manage.py crawl这在课程设计和毕业设计里也是比较容易讲清楚的结构。命令文件的目录结构app/ management/ __init__.py commands/ __init__.py crawl.py两个__init__.py缺一不可否则 Django 找不到命令。4.2 支持 --platform、--keyword、--pages、--index 的命令实现工程中需要把抓取流程参数化。下面是一个标准实现from django.core.management.base import BaseCommand from app.crawler import run_crawl class Command(BaseCommand): help 抓取京东、淘宝、苏宁、亚马逊商品信息 def add_arguments(self, parser): parser.add_argument(--platform, requiredTrue, choices[jd, taobao, suning, amazon]) parser.add_argument(--keyword, requiredTrue) parser.add_argument(--pages, typeint, default1) parser.add_argument(--index, typeint, default0) def handle(self, *args, **options): total run_crawl( platformoptions[platform], keywordoptions[keyword], pagesoptions[pages], indexoptions[index], ) self.stdout.write(self.style.SUCCESS(f完成共写入/更新 {total} 条))执行示例python manage.py crawl --platform jd --keyword 机械键盘 --pages 3 --index 0这里把--platform限定为四个固定值避免手误。--pages控制抓多少页--index传入搜索索引帮助脚本在结果不完整时重新定位排序偏移。run_crawl的返回值用于命令成功后的反馈。需要注意在写命令文件时不能直接在 handle 里放一堆反复出现的解析逻辑应把抓取循环放到app/crawler.py方便单元测试。4.3 基于 Django ORM 的竞品数据分析接口数据抓到库里之后最实用的功能是同一关键词在不同平台的均价对比。常见做法是提供一个 JSON 接口返回聚合结果。from django.http import JsonResponse from django.db.models import Avg, Max, Min, Count def price_compare(request): keyword request.GET.get(keyword, ).strip() if not keyword: return JsonResponse({error: keyword is required}, status400) rows (Product.objects.filter(keywordkeyword) .values(platform) .annotate( avg_priceAvg(price), max_priceMax(price), min_priceMin(price), product_countCount(id), ) .order_by(platform)) return JsonResponse(list(rows), safeFalse).values(platform)告诉 ORM 按 platform 分组.annotate为每个组计算 avg_price、max_price、min_price 和 product_count。这几句会翻译成带 GROUP BY 的 SQL比把全表拉进 Python 再循环统计快得多。返回的 JSON 可直接被前端表格或 pandas 继续处理这也是“电商数据分析”在管理后台落地最快的方式。参数类型说明keyword必填字符串搜索关键词为空时返回 400分组字段platform京东、淘宝、苏宁、亚马逊四平台聚合字段avg_price/max_price/min_price价格区间快速对比product_count聚合计数可用于判断平台数据量是否足够4.4 任务日志与断点续抓命令式的爬虫跑起来之后日志比 print 重要。否则后台执行命令时看不到输出出了问题也不知道卡在哪一页。通常在 run_crawl 里记录每个平台每个页面的开始和结束。可以配合 Python logging 来做。import logging logger logging.getLogger(crawler) def run_crawl(platform, keyword, pages, index): from app.models import Product from app.parsers import parse_platform logger.info(start platform%s keyword%s pages%d, platform, keyword, pages) total 0 for page in range(1, pages 1): html fetch_search_page(platform, keyword, page, index) items parse_platform(platform, html, keyword, page) for item in items: save_product(item) total 1 logger.info(page%d total%d, page, total) return total这里fetch_search_page是第 2 章的会话函数parse_platform按平台解析列表页。logger 输出会包含时间戳方便后续用 grep 分析每个页面的耗时。断点续抓的逻辑也可以挂在这里如果某页网络异常记录到日志后继续下一页而不是让整个命令中途退出。5. 反爬应对与频率控制requests 爬虫不只要会请求5.1 平台常见的反爬信号电商列表页最常见的反爬不是验证码而是对请求身份和节奏的校验。缺 User-Agent 的请求很可能直接 403短时间高频请求会触发 429 或回到验证码页不带 Referer 的搜索请求在某些平台会被拒绝。这些都是 requests 爬虫层面的基础问题。源码里要求每个平台请求都走统一 Session并在 headers 中带上 UA、Accept、Accept-Language、Referer。Referer 一般指向该平台首页让请求看起来是从浏览器入口流入的。有些平台还会检查 Cookie 中是否包含特定字段例如淘宝需要先访问首页拿到几个必要 Cookie再带 Cookie 去请求搜索页。这套流程在爬虫里通常体现为“预热请求”。但有一个边界要清楚如果平台要求登录、验证码、滑块不要尝试绕过应该停止抓取并记录异常。课程设计级项目把公开可见数据抓下来分析没什么问题但不能把“对抗验证码”作为卖点。5.2 限速器控制 QPS 而不是无限加并发很多新手在遇到封禁时第一反应是加大线程数这恰好和平台预期相反。requests 爬虫应该控制不同线程之间发请求的时间间隔。下面是一个简单的限速器按函数级别生效。import threading import time import random class RateLimiter: def __init__(self, min_interval1.0, jitter0.5): self.min_interval min_interval self.jitter jitter self.lock threading.Lock() self.last_ts 0.0 def wait(self): with self.lock: now time.time() wait self.min_interval random.uniform(0, self.jitter) - (now - self.last_ts) if wait 0: time.sleep(wait) self.last_ts time.time() limiter RateLimiter(min_interval1.0, jitter0.5)调用方式是在fetch_search_page发起请求之前先执行limiter.wait()。min_interval表示理论上的最小请求间隔jitter添加 0~0.5 秒随机抖动避免请求间隔过于规律而被识别。把锁放在限速器内部是为了多线程场景下多个线程不会同时通过等待点。极端情况下如果线程数很大这个限速器会排队导致并发收益下降。所以实际配置里线程数一般不超过 5限速间隔取 0.8 到 1.5 秒。场景线程数请求间隔说明单平台快速演示10.5s只抓 1~2 页风险低课程设计完整抓取31.0s平衡速度与稳定性多关键词批量监控51.5s长时间运行时降低触发频率分布式爬虫多机每机限速需要额外队列和调度中心5.3 失败重试但不是无限重试requests 库的 Retry 在第 2 章已经出现但这里需要明确重试边界。超时和 5xx 可以重试403、429、验证码页面不能盲目重试。403 重试大概率还是 403429 需要等待一段时间如果 Retry 的 backoff 策略不合适反而会加重被限速的印象。因此代码里对状态码做一个分级。from requests.exceptions import HTTPError def fetch_with_limited_retry(url, paramsNone, sessionNone, max_attempts3): for attempt in range(max_attempts): limiter.wait() try: resp session.get(url, paramsparams, timeout8) if resp.status_code 403: logger.warning(403 forbidden, url%s, resp.url) return None if resp.status_code in (429, 503): logger.warning(rate limited, wait longer, attempt%d, attempt 1) time.sleep(5 * (attempt 1)) continue resp.raise_for_status() return resp.text except requests.Timeout: if attempt max_attempts - 1: raise return None注意到 429 和 503 是加重退避等待再进入下一轮循环403 直接返回 None不再浪费请求。timeout 异常保留在最后一轮向上抛让外层抓到异常后记录是哪一页、哪个关键词出的问题。这样可以保证不重试封禁响应也避免异常被静默吞掉。5.4 合规提醒robots、公开数据与使用范围这一节不需要展开太大篇幅。抓取公开商品信息用于学习、课程设计、竞品价格统计是比较常见的场景但要注意目标站点的 robots.txt 如果明确禁止爬取尽量只抓少量样例不要抓取用户私人数据不要批量下载图片后二次分发。源码中对图片 URL 只做存储不主动下载就是这个原因。频率控制也不是为了对抗平台而是为了不干扰正常用户访问。合规边界清晰之后再上分布式爬虫才有意义。如果只是课程设计单机加限速已经足够如果想把数据量做到几十万上百万才需要考虑把抓取任务拆分到多个机器用一个共享队列做任务分发这就是另一个话题了。6. 抓取结果验证与运维技巧6.1 用计数 SQL 验证抓取任务是否完成抓完 3 页京东商品最怕的不是代码 bug而是解析器对页面结构判断错误实际只入库了半页数据。验证方法很简单直接查数据库python manage.py shell -c from app.models import Product from django.db.models import Count for row in Product.objects.values(platform, keyword).annotate(totalCount(id)): print(row[platform], row[keyword], row[total]) 如果 total 接近 0说明解析器没有匹配到商品列表如果 total 明显小于 3 页应有的条数检查是不是第二页和第三页 URL 构造错误或者页面提示“无搜索结果”。这个技巧比看日志更直接因为入库条数是最终产物。6.2 抓取完成后核对价格异常值入库后执行一次聚合from app.models import Product from django.db.models import Min, Max print(Product.objects.aggregate(loMin(price), hiMax(price)))如果发现价格最小值是 0或者最大值达到千万级多半是清洗函数把促销文案中的数字误判成了价格。这类异常要在全部数据进入分析前修掉否则后面均价会被几个异常点拉偏。另外可以写一个临时脚本把所有价格为 0 或为空的商品打印出来定位是哪个平台的问题。京东通常没有价格的商品很少淘宝二手商品价格可能为 1苏宁和亚马逊偶尔会有“暂无报价”。过滤规则要和业务对齐。6.3 增量抓取的简单实现重复执行同一关键词和页数不会重复入库但会覆盖价格、评论数等动态字段。要想控制数据膨胀可以在分析时加上created_at过滤。用一个查询仅取最近一次抓取的结果from django.utils import timezone from datetime import timedelta recent Product.objects.filter( created_at__gtetimezone.now() - timedelta(hours24) )如果后续需要精确处理同关键词的多次抓取可以考虑增加task_id字段把每次命令执行作为一个批次。这样即使同一个商品重复入库也能通过task_id区分批次。注意在目前的唯一键设计下同一个 task 的重复更新语义是覆盖而不是追加所以做时间序列趋势分析时需要在模型上再加created_at并取消 keyword 唯一约束。6.4 排查页面结构变化的一个习惯最后说一个省时间的动作当抓取结果为空时不要急着改代码先打印原始 HTML 里是否存在目标关键字段。在fetch_search_page返回后随手执行python manage.py shell -c from app.crawler import session html session.get(https://search.jd.com/Search, params{keyword: 机械键盘, enc: utf-8}, timeout8).text print(len(html)) print(机械键盘 in html) 如果 len(html) 只有几千字节说明很可能被重定向到验证码或安全页如果关键词在 html 里但解析器没提取到数据问题就出在正则或 BeautifulSoup 选择器上。这个二分排查法能快速把问题定位到网络层还是解析层是维护爬虫项目时最实用的习惯。本文还有配套的精品资源点击获取
返回列表