做电商选品分析那段时间,我需要采集某平台一批商品的价格、销量和评价关键词。上手前我看那些教程,感觉爬虫特别简单,不就是requests.get()拿到 HTML 再用 BeautifulSoup 解析一下嘛。真正跑起来才发现,从requests.get()到稳定地把数据落库,中间隔着一个巨大且不断升级的反爬系统。这篇文章不打算写成"点对点"的教案,而是把我从入门到逐渐摸清反爬思路的完整过程做个复盘,包括我踩过的坑、试过的方法、最终稳定运行的方案。如果你正打算用 Python 爬取电商商品数据,或者已经被 403、验证码、字体反爬折磨到怀疑人生,这篇应该能帮你少走很多弯路。
需要先说清楚,我做的数据采集主要用于个人选品调研和价格走势记录,采集频率不高,范围控制得很克制。文章里讲到的各种反爬对策,更多是分析原理、搭建合理的采集策略,所有的研究和测试都应当限定在授权范围、自有数据或公开接口中。合规问题不是套话,它直接决定了你能不能在阳光下长期做这件事。后面我会花一点篇幅专门说这个。
1. 项目背景与整体设计思路
1.1 正经的项目需求长什么样
很多人学爬虫都是从"爬一下淘宝评论""爬一下京东价格"开始,但真正落到实际场景里,需求的复杂度远超想象。我当时的需求是这样的:监控某个类目下大约三百个商品的价格走势和评价变化,每天抓一次,字段包括商品标题、当前价、划线价、月销量、总评价数、好评率以及评论文本里的高频词。
这个需求有几个关键点直接影响方案选择:
- 数据量不大,单次采集也就几百个商品,不需要分布式。
- 更新频率很低,一天一次,属于温和型采集。
- 字段敏感,价格和销量一旦解析错误,后续分析全跑偏。
- 需要长期稳定,平台反爬会不断更新,方案必须留出迭代空间。
所以我的技术选型思路很明确:能用公开接口直连就不去解析 HTML,能模拟浏览器就不硬怼签名算法,能慢速稳定采集就不追求秒级并发。很多新手一上来就上 Scrapy 加代理池,最后发现数据没采集多少,账号被风控了一堆,这就是需求和技术方案没有匹配的典型结果。
1.2 技术选型:Requests、Playwright 和解析库的配合
我最终采用的是requests+lxml+Playwright的组合,而不是很多人推荐的 Scrapy 或 Selenium。选型对比可以参考下面这个表。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| requests + lxml | 轻量、速度快、内存占用低 | 无法执行 JavaScript,动态页面拿不到数据 | 数据已经在 HTML 或轻量 JSON 接口中 |
| httpx | 支持 HTTP/2,接口更现代 | 比 requests 稍复杂,生态略小 | 目标接口强制 HTTP/2 的场景 |
| Selenium | 生态老、资料多 | 需要浏览器驱动,资源占用大,被识别概率高 | 老项目维护,或需要兼容旧浏览器 |
| Playwright | API 现代、速度比 Selenium 快、自动等待好用 | 需要安装浏览器内核,部署稍重 | 需要处理动态渲染、点击、滚动等场景 |
这里我特别想强调一个判断逻辑:不要指望一个工具解决所有问题。requests 直连的成本最低,只要能用就优先用;只有发现关键数据必须通过动态执行 JavaScript 才能出现时,才切换到 Playwright。如果一上来就为所有页面启用无头浏览器,采集速度会慢一个数量级,而且浏览器特征一旦被识别,风控的严重程度往往更高。
1.3 合规前提必须提前想清楚
这部分我不打算说教,但确实是被现实教育过。后来我在做一个授权范围内的爬虫时发现,如果目标站点的robots.txt明确禁止了某些路径的抓取,或者采集字段涉及个人可识别信息,那无论技术方案多完美,都必须重新评估。
我自己定的三条红线,建议你也参考:
- 只采集公开页面数据,不登录他人账号,不突破付费墙。
- 不采集手机号、地址、真实姓名等个人信息,即使技术上拿得到也不拿。
- 控制请求频率,不让自己的采集行为对目标站点造成明显压力。
这几条红线不是为了显得道德高尚,而是让项目能够在法律法规和平台规则下长期安全地运行。尤其是想用爬虫做些副业、接外包项目的话,这些边界更重要。之前就有团队因为违规采集数据被追责,这类新闻大家应该都看到过,不展开。
2. 从零开始的第一个爬虫:商品列表页
2.1 环境准备与请求头伪装
我用的是 Python 3.10,安装依赖特别简单:
pip install requests beautifulsoup4 lxml retrying第一个坑马上就出现了:直接用默认 headers 发起请求,淘宝、京东这类平台返给我 406 或 403 的概率非常高。原因很简单,服务端会检测请求头里是否包含 Accept、Accept-Language、User-Agent 等标志性字段,纯requests的默认 UA 太容易被识别。
我当时的做法是构造一个请求头函数,每次请求随机选一个 UA:
import random import requests UA_POOL = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0", ] def build_headers(referer: str = None) -> dict: headers = { "User-Agent": random.choice(UA_POOL), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", } if referer: headers["Referer"] = referer return headers这里有个很关键的细节:请求头要和页面访问路径匹配。比如你访问的是商品搜索页,Referer 指向搜索页入口,Accept-Language 保持中文,这样才像真实用户。我自己试过把 Referer 去掉,结果偶尔能过,但整体成功率明显下降。
2.2 解析页面:BeautifulSoup 还是 XPath?
HTML 解析我最初用的是 BeautifulSoup 的find_all,代码写起来很顺手,但遇到一个结构复杂、嵌套十几层的页面时,性能确实一般。后来切换到lxml的etree.HTML配合 XPath,解析速度提升明显,代码也变得更紧凑。
以京东搜索列表页为例,最常见的解析套路是这样的:
import lxml.html as lh def parse_jd_list(html: str): doc = lh.fromstring(html) items = [] for li in doc.xpath('//li[contains(@class, "gl-item")]'): title = li.xpath('.//div[contains(@class, "p-name")]/em//text()') price = li.xpath('.//div[contains(@class, "p-price")]/strong/i//text()') shop = li.xpath('.//div[contains(@class, "p-shop")]//a//text()') items.append({ "title": "".join(title).strip() if title else "", "price": "".join(price).strip() if price else "", "shop": "".join(shop).strip() if shop else "", }) return items这里有几个非常容易踩的坑,我一个个说。
第一,class 属性里的空格问题。很多新手写//li[@class="gl-item"],但真实的 HTML 里 class 可能是gl-item clearfix,多了一个空格导致 XPath 匹配失败。所以要用contains(@class, "gl-item"),而不是精确等值。
第二,价格分离。京东列表页的价格和详情页价格经常不是同一个数据源,列表页的价格节点位置和 PC 端详情页完全不同。所以解析前一定要先打开浏览器开发者工具,确认价格是在 HTML 里,还是通过异步接口渲染出来的。我最初就在这个上面浪费过一天,后来吸取教训:凡是页面上有数据但 HTML 里找不到,直接去 Network 面板看 XHR 请求,这比盲目解析高效得多。
第三,评价数和销量经常是"500+"这种格式。这种带加号的字符串不能直接转 int,必须先用正则提取数字部分。
2.3 翻页与去重:容易被轻视的关键细节
商品列表页的翻页通常是一个page参数,直接循环就能拿全。但真实场景里有两个隐藏问题:一是部分平台对翻页深度有限制,搜到第 50 页之后会强制跳回第一页;二是同一个商品会出现在多个关键词、多个分类的列表里,导致重复采集。
我的处理方案是在数据层面加一个sku_id集合去重,而不是靠 URL 去重。因为同一个商品可能有多个推广链接,URL 不同但sku_id相同。去重逻辑很简单,每次拿到商品先判断 ID 是否在集合里,不在才进后续流程。
翻页时的请求间隔也必须控制。我最初用固定 1 秒间隔,结果到第 5 页就被封了。后来改成随机间隔:
import time import random def random_sleep(base: float = 2.0, delta: float = 1.5): time.sleep(random.uniform(base, base + delta))这样的好处是让请求节奏看起来更接近真实用户的操作间隔,而不是机器人的固定频率。还有一个小技巧是每翻几页就随机暂停更长时间,比如八次请求后休息 20 秒,模拟真人看东西、思考一下的操作。
3. 进阶:登录态、Cookie 与接口直连
3.1 用开发者工具找到真正的数据接口
爬虫做到后面你会发现,HTML 解析其实是最脆弱的环节。平台的前端工程师随手改一个 class 名,你的 XPath 就彻底失效了。相比之下,直接调用后端的数据接口(XHR/JSON)要稳定得多。
操作方式很简单:打开浏览器开发者工具,切到 Network 面板,筛选XHR,然后刷新页面。你会看到一堆以https://...结尾的接口请求,点开看 Response 是否为 JSON 数据以及是否包含你需要的商品字段。
以我当时要采集的某平台为例,页面上看起来是服务端渲染出来的商品列表,但其实页面加载后会再发一个异步请求,返回完整的 JSON,字段比 HTML 里更全。我直接把那个接口的 URL 复制到 requests 里,加上合适的请求头就能拿到数据。
接口直连的好处有三点:
- 数据是结构化 JSON,解析和入库方便。
- 不受前端页面结构改版的影响。
- 请求量更小,一个接口顶多个页面请求。
当然,接口直连也有代价,就是你会直接面对反爬最核心的防线:签名参数、Cookie 校验、风控策略。这部分我在第 4 章展开。
3.2 维护会话状态:requests.Session 的用法
在很多需要登录态的接口中,Cookie 是身份凭证。直接用一个requests.Session()负责所有请求,可以在内部自动保持 Cookie,不需要每次手动拼接。
import requests session = requests.Session() # 第一次请求设置初始 Cookie session.get("https://example.com/", headers=build_headers()) # 后续请求会自动带上 resp = session.get("https://example.com/api/product/list", headers=build_headers())如果你已经从浏览器登录过,想获取当前登录的 Cookie,可以在开发者工具的 Application 面板里复制 Cookie 字符串,然后转成 requests 能接受的格式:
cookie_str = "a=xxx; b=yyy; c=zzz" cookies = {} for item in cookie_str.split(";"): key, value = item.strip().split("=", 1) cookies[key] = value session.cookies.update(cookies)这里有个容易忽略的点:Cookie 是带时效的,过期后请求会返回 302 跳转或直接 403。所以要做一个"会话有效性检查",比如请求后判断返回的状态码和页面内容是否包含登录提示,一旦发现失效就停止任务,而不是继续无意义地重试。
3.3 动态渲染页面交给 Playwright 接管
有些电商页面的数据是通过 JavaScript 异步渲染出来的,requests只能拿到一个空壳 HTML,比如那些采用"首屏先渲染框架,数据后续通过 WebSocket 推送"的页面。这时候用 Playwright 最方便。
我封装了一个极简的动态页面爬取函数:
from playwright.sync_api import sync_playwright def get_dynamic_html(url: str, wait_selector: str, proxy: dict = None) -> str: with sync_playwright() as p: browser = p.chromium.launch( headless=True, args=["--disable-blink-features=AutomationControlled"], proxy=proxy, ) context = browser.new_context( user_agent=random.choice(UA_POOL), viewport={"width": 1920, "height": 1080}, locale="zh-CN", ) page = context.new_page() page.goto(url, wait_until="domcontentloaded", timeout=30000) page.wait_for_selector(wait_selector, timeout=10000) html = page.content() browser.close() return html有几个细节值得特别提醒。
一是Playwright 默认无头浏览器存在特征缺失,navigator.webdriver这个属性可能暴露自动化状态。所以我添加了--disable-blink-features=AutomationControlled参数,同时在启动浏览器时避免使用默认的用户数据目录。
二是页面等待条件要选对。不要用固定time.sleep(3),而应该根据页面中某个关键元素是否出现来判断加载完成。比如等一个商品卡片节点出现,或等网络请求数达到某个阈值,这样既快又稳。
三是有头模式在部分场景下更稳。在我自己的 Windows 开发机上调试时,headless=False的成功率明显高于无头模式。虽然这意味着会弹出浏览器窗口,但用于个人本地采集完全可以接受。在服务器上则可以配合xvfb这类虚拟显示工具来模拟。
4. 反爬机制的类型与应对思路
4.1 最基础的入射点:请求头、频率和 IP
电商平台反爬的第一道防线是基础规则检查。请求头不完整、频率异常、同一 IP 请求量过大,都会触发拦截。
我遇到的 403 响应,原因排查优先级是这样的:
- 请求头缺失,尤其是 Referer 和 Accept-Language。
- 请求频率太快,单位时间内请求数超过阈值。
- 携带可疑 Cookie 特征,比如新会话第一次访问就直接请求敏感接口。
- IP 被标记为机房或代理段,部分平台对 IDC IP 段有更严格的风控。
对应策略也不复杂,核心就是"低调"两个字。随机 UA、随机间隔、避免固定时间点高频访问。如果数据量确实大,建议用稳定的住宅代理服务而非机房 IP,但这里我必须再提醒一次:任何代理服务都要选择合规合法的供应商,且用途不能违反法律法规。另外,不建议也不鼓励用爬虫去试探平台的安全边界,那是另一个范畴的问题。
4.2 签名参数与加密逻辑:看得懂也要守得住边界
做了一段时间接口直连后,我遇到了最核心的反爬机制:签名参数。某平台的商品详情请求里,每一次请求都带一个sign字段,没有这个字段服务器直接拒绝返回真实数据。经过逆向分析发现,它的生成逻辑是时间戳加固定 token 拼接后的哈希变换。
这一类加密逻辑的分析思路是有通用性的:
- 在浏览器开发者工具里找到生成参数的那段 JavaScript 文件。
- 用
search功能搜索变量名,比如sign、token、md5等关键词,定位到生成函数。 - 阅读代码逻辑,找出参数拼装规则。
- 用 Python 的
execjs、js2py,或直接在 Node 环境里运行同一段 JS 来生成签名。
举个例子,下面是算法逻辑的大致示意(非真实平台):
import hashlib import time def make_sign(params: dict, secret: str) -> str: raw = "".join(f"{key}={params[key]}" for key in sorted(params.keys())) raw += f"&secret={secret}" return hashlib.md5(raw.encode("utf-8")).hexdigest()如果你能拿到真实的密钥和加密逻辑,就能在 requests 中复现签名。但这里必须提个醒:对平台加密参数的完整逆向和批量调用,可能涉及平台安全机制的绕过,不应被用于未授权的商业用途或攻击性测试。我建议把这类研究限制在自己有权限的测试环境、开源项目中,或者干脆只停留在学习原理阶段。
4.3 内容级别的反爬:字体反爬与 CSS 偏移
验证码和签名挡的是机器请求,而字体反爬和 CSS 偏移则是直接在数据展示层面动手脚,让你即使拿到了 HTML 也读不出真实内容。
字体反爬的典型特征是:页面里数字的视觉显示正常,但复制到文本里全是乱码,HTML 里对应的字符可能也不是标准数字。其原理是服务端动态生成一套自定义字体,将数字或文字映射到自定义字形上,同时把字符编码用私有 Unicode 位。浏览器能正常渲染,但解析 HTML 的人拿到的是错乱字符。
我当时遇到过一次价格乱码,排查过程是这样的:
- 在 HTML 中找到
@font-face引入的.woff文件地址。 - 下载字体文件,用
fontTools.ttLib解析。 - 提取
cmap映射表,把页面里的私有字符映射回真实字符。
这种方案的实现其实不难,难的是每次请求拿到的字体文件可能都不同,所以要把"字体下载-解析-映射"做成动态的,不能一次性硬编码。
CSS 偏移则是把价格每一位数字渲染成独立的 span,然后用position: relative和left: -10px之类的方式打乱视觉顺序。真实用户看得清,但爬虫直接按 DOM 顺序拼接就会得到错乱的价格。解决思路是按left坐标排序这些 span,再拼接文本。
这两种反爬都很有意思,因为它们不是技术上的"锁",而是在数据可读性上做文章。好处是不太容易被批量拦截,坏处是你必须针对每一类结构写专属的解析逻辑。
4.4 验证码与行为风控升级时的应对策略
验证码是最后一道大坝。滑块验证、点选验证、旋转验证,还有基于行为轨迹的风险评估,单个请求可能没事,但短时间大量请求后就会触发。
我的对策很直白:不硬刚,绕远路。
- 降低采集频率,增加随机等待时间,尽量不触发风控。
- 切换采集时段,避开平台流量高峰,比如凌晨三点到六点之间。
- 优先采集非核心页面,把必须过验证码的页面放到最后手动处理。
- 如果任务量不那么大,保留一个
manual_override模式,让 Playwright 弹出浏览器窗口,由人工完成一次验证后把 Cookie 保存下来继续用。
这套方案看起来很简单,但实操中远比研究签名算法更高效。而且我要强调一个重要认知:99% 的反爬拦截是因请求频率和特征异常导致的,而不是因为对方真的启用了多高级的加密算法。把频率降下来,大部分问题都会自动消失。
5. 数据清洗、去重与持久化存储
5.1 字段抽取与清洗的常见坑
原始接口数据看起来干净,但直接入库还是会出问题,因为平台的字段设计并不面向分析场景。我遇到的典型问题有:
- 价格是区间字符串,比如
"199.00-299.00",分析时要取最低价还是平均价必须统一。 - 销量带单位,比如
"500+"、"1.2万+",需要统一换算成实际整数。 - 商品标题混乱,有些标题混入了营销词、规格、包装信息,不做清洗就统计高频词,结果全被品牌词刷屏。
- 状态字段缺失,部分商品已经下架,检索时如果不剔除,会导致价格、销量数据不准确。
我写了一个清洗函数,统一处理价格、销量和标题:
import re def clean_price(price_str: str): if not price_str: return None nums = re.findall(r"\d+\.?\d*", price_str) if not nums: return None return min(float(n) for n in nums) def clean_sales(sales_str: str): if not sales_str: return 0 sales_str = sales_str.replace(",", "").replace("+", "") if "万" in sales_str: return int(float(sales_str.replace("万", "")) * 10000) if "亿" in sales_str: return int(float(sales_str.replace("亿", "")) * 100000000) return int(float(sales_str))清洗的目标不只是让数据类型统一,更重要的是把分析口径固定下来。比如我后面做价格趋势分析,如果一次取最低价、一次取平均价,曲线就会无意义波动。所以要在清洗阶段就把口径写死并注释清楚。
5.2 存储方案:从 CSV 到 MySQL
当采集量还比较小的时候(单次几千行以内),CSV 加上 pandas 完全够用,可视化也快。但如果你在持续做增量采集,比如每天更新一次价格库存,那就必须上数据库,否则重复数据没法管理,也没法做版本对比。
我从 CSV 切换到了 MySQL,建表逻辑考虑了几件事:
- 商品主表存静态信息:
sku_id、title、shop_id、category。 - 价格历史表存动态信息:
sku_id、price、sales、crawl_time。 - 两张表通过
sku_id关联,主表用唯一索引去重。
写入时用INSERT ... ON DUPLICATE KEY UPDATE,天然解决重复问题:
import pymysql def upsert_item(conn: pymysql.connections.Connection, item: dict): sql = """ INSERT INTO product ( sku_id, title, price, sales, category, crawl_time ) VALUES ( %(sku_id)s, %(title)s, %(price)s, %(sales)s, %(category)s, %(crawl_time)s ) ON DUPLICATE KEY UPDATE title = VALUES(title), price = VALUES(price), sales = VALUES(sales), crawl_time = VALUES(crawl_time) """ with conn.cursor() as cursor: cursor.execute(sql, item) conn.commit()这是个小细节,但对于增量型爬虫来说作用非常大。以前我用SELECT判断再INSERT,数据量一上来速度就慢,改成upsert后干净利落。
5.3 定时增量采集的方案设计
我脚本里设置了两个重要参数:max_pages控制每个关键词最多采集多少页,crawl_interval_hours控制任务执行间隔。正常情况下,一天跑一次就够了,因为平台的销量和价格基本是 T+1 维度更新,没有必要更频繁。
定时执行我用系统自带的定时任务,Windows 上就是任务计划程序,Linux 上就是crontab。示例 crontab 配置:
0 3 * * * cd /opt/products && /usr/bin/python3 crawl_daily.py >> crawler.log 2>&1定时任务有个容易被忽略的坑:脚本执行环境的 PATH 和手动执行不一样。比如python3的路径可能不同,或者相对路径读取的配置文件会出错。我后来把所有读取路径统一改成了绝对路径,并在脚本开头打一条启动日志,这样每次执行失败都能快速定位。
另一个很重要的点是日志。爬虫不同于普通 Web 应用,出错时没有用户主动上报。所以我在每个关键环节都加日志:请求开始、拿到响应、解析数量、入库条数、异常详情。这样第二天查看日志就能知道昨晚是否成功、失败在哪一步。对于一个每天自动跑的任务来说,没有日志等于盲人摸象。
6. 常见问题与排查技巧实录
6.1 返回 403 或者验证码:按照优先级逐层排查
遇到 403 时不要慌,我一般按下面的顺序排查。
先看请求头是否完整,特别是 User-Agent、Referer、Accept-Language 这三件套。再看是否携带了有效的 Cookie,对一些敏感接口来说,没有 Cookie 的请求基本必死。然后看请求频率,如果刚才连续请求了很多次,可以停下来等十分钟再测一次。最后再看 IP 是否被风控,这一个很难自测,因为同一个 IP 可能被平台临时限制,但对其他人仍然正常。
为了快速定位,我会写一个最小化测试脚本,用最干净的请求头、单个 URL、单次请求来验证。如果最小化请求都返回 403,说明问题在 IP 或 Cookie 层;如果最小化请求正常,再逐步加回其他逻辑,直到触发拦截。
6.2 页面结构变了导致解析失败
有一次凌晨跑定时任务,第二天起来发现入库数为零。查日志看到 XPath 匹配结果全部为空,一打开网页才发现前端把商品卡片从<li>换成了<div>。这个教训教会我一件事:凡是写死 HTML 结构的选择器,迟早都会失效。
所以后来所有解析都优先选择 JSON 接口,因为后端接口字段比前端页面稳定得多。如果必须解析 HTML,我会把选择器提取成独立配置文件,这样前端改版时只需要改配置,不需要动代码。这也是为什么我在第 3 章花那么大的篇幅推荐接口直连的原因,在反爬环境下,稳定比速度更重要。
6.3 排查速查表
| 问题现象 | 可能原因 | 处理思路 |
|---|---|---|
| 首次请求直接 403 | 请求头缺失或异常 | 补全 UA、Referer、Accept-Language |
| 翻页到中间突然被拦截 | 请求频率过高触发风控 | 增加随机延时,降低翻页深度 |
| 所有请求返回验证码 | IP 或设备指纹被标记 | 停止任务,换时段或换网络 |
| 数据能打开但解析到空 | 页面结构变更或数据在异步接口 | 打开开发者工具检查 XHR 接口 |
| 解析出来的数字是乱码 | 字体反爬或编码问题 | 下载 woff 字体解析 cmap 映射表 |
| 登录态丢失 | Cookie 过期或会话被刷新 | 重新登录获取新 Cookie,加入有效性检查 |
| 数据重复且入库缓慢 | 表结构或去重逻辑有问题 | 使用唯一键 upsert,先查后插改为ON DUPLICATE KEY UPDATE |
这个表其实是我的个人排查日志,每次遇到新问题就往里加一条。把问题归类会比每次都从头查一遍高效得多,很多异常原因翻来覆去都是同一类。
6.4 几个实战中的心得
最后分享几个我在实际运行中特别深刻的体会。
第一个心得是:反爬破解的核心不是技术,而是节奏控制。一台机器、一个 IP、一个账号,想假冒成千上万真实用户的请求模式,本身就很难成立。所以不要追求百万级数据量,先把几十到几百条数据的稳定采集做好。大多数个人分析需要的数据量其实并不大,完全不需要把自己逼成黑产。
第二个心得是:工具的"隐身"能力是有极限的,你可以修改 UA、可以关掉自动化特征,但设备指纹、行为轨迹、鼠标移动模式这些维度很难完全模拟。认识到这个边界很重要,它让你明白该在什么时候停下来,改用人工流程或购买合法数据,而不是无限投入时间去对抗风控系统。
第三个心得比较直接:做爬虫之前的"数据需求设计"比写代码重要得多。想清楚要哪些字段、每天更新几次、数据存哪里、分析什么指标,远胜过研究一堆花哨的反爬技巧。我的经验是,需求梳理得好,技术工作量至少能砍掉一半。
采集数据这件事本身是中性的。它让我能用几百行 Python 代码,搭建起一套个人数据观察系统,持续追踪感兴趣的商品和市场变化。希望这篇经验记录能帮你少踩几个坑,也更清楚边界在哪里。祝你在自己的数据采集项目里,既能拿得到数据,也能睡得着觉。