news 2026/10/15 8:47:27

小程序模仿滴滴打车:订单状态机与地图选点最小闭环实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
小程序模仿滴滴打车:订单状态机与地图选点最小闭环实战

简介:这份资源是一套微信小程序仿滴滴打车的完整项目源码,面向具备小程序基础、希望进阶实战出行类应用的开发者。项目围绕叫车核心流程展开,涵盖首页、叫车页、订单页、个人中心等页面模块,并整合地图定位、路线规划、实时通信、微信支付与订单状态管理等关键技术点,接口部分需自行替换填写,适合作为学习演示与二次开发底稿。压缩包为rar格式,共151个文件,约652KB,其中80个png图片资源、20个wxss样式文件、20个js逻辑文件、16个json配置文件与15个wxml结构文件,另含工具函数库、可复用模板与第三方地图SDK封装,目录按页面、样式、资源、工具分层组织,便于按模块检索。目前已有3810人学习下载,读者可借此梳理小程序项目结构,理解位置服务、订单流转与支付集成的实现思路,并参考工具函数与模板组件的写法,积累出行类应用的排错与调试经验。

1. 小程序模仿滴滴打车:从订单状态机到地图选点的最小闭环

很多人第一次听到「小程序模仿滴滴打车」这个需求,脑子里蹦出来的是地图、定位、实时轨迹、司机接单推送这一整套东西,然后打开编辑器就卡住了——因为不知道从哪一行开始写。我做过几个类似的小程序出行 Demo,血泪经验是:真正难的不是地图,而是订单状态怎么流转。你把状态机想清楚了,地图只是渲染层;状态机没想清楚,地图画得再漂亮,乘客点了「确认上车」司机那边还是「待接单」,整个流程就是玄学。

这篇笔记面向的是想在小程序里跑通一套「乘客下单 → 司机接单 → 行程中 → 完成支付」最小闭环的开发者。不追求做成商业级打车平台,而是把订单状态、地图选点、司机乘客双端数据同步这三件事拆开讲透,让你能在本地开发环境里复现一个能点、能走、能结束的 Demo。适合两类人:一类是刚接触小程序、想找个有真实业务感的项目练手;另一类是已经会写页面、但没处理过「两端同时操作同一订单」这种并发场景的熟手。下面所有代码都是可抄的骨架,参数我会标清楚为什么这么设。

2. 订单状态机怎么设计才不翻车

2.1 为什么先画状态图再写页面

出行类小程序的核心不是 UI,是订单。乘客和司机看到的是同一个订单的两个视角,任何一方的操作都要让另一方的界面跟着变。如果你把状态散落在各个页面的 data 里,比如乘客页存一个isAccepted,司机页存一个orderStatus,两边一旦对不上,就会出现「司机已接单但乘客还在转圈」的经典翻车现场。

常见做法是:订单只存一个status字段,所有界面根据这个字段派生显示。状态枚举我一般会定成下面这几个,够一个 Demo 用,也留了扩展位:

状态值含义谁能触发进入
0待接单乘客下单成功
1已接单司机点击接单
2司机已到达司机点击到达上车点
3行程中乘客确认上车
4待支付司机点击结束行程
5已完成乘客支付成功
9已取消乘客或司机取消

这张表看着简单,但每个状态之间的跃迁条件必须写死。比如状态 2 只能由状态 1 进入,状态 3 只能由状态 2 进入,不能从 1 直接跳到 3。为什么?因为「司机已到达」这个动作是乘客确认上车的前提,少了这一步,乘客可能在司机还没到的时候就点了上车,后面行程时长和费用全乱。

2.2 用云函数做状态跃迁的唯一入口

小程序端直接改数据库是大忌,因为两端都能改,谁后写谁覆盖。我一般把状态变更收口到一个云函数里,所有跃迁都走它,顺便做权限校验。下面是一个最小可用的云函数骨架:

// cloudfunctions/updateOrder/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 合法跃迁表:key 是当前状态,value 是允许到达的下一个状态数组 const TRANSITIONS = { 0: [1, 9], // 待接单 -> 已接单 / 已取消 1: [2, 9], // 已接单 -> 司机到达 / 已取消 2: [3, 9], // 司机到达 -> 行程中 / 已取消 3: [4], // 行程中 -> 待支付 4: [5], // 待支付 -> 已完成 5: [], 9: [] } exports.main = async (event) => { const { orderId, nextStatus, role } = event const wxContext = cloud.getWXContext() const openid = wxContext.OPENID // 1. 取出当前订单 const orderRes = await db.collection('orders').doc(orderId).get() const order = orderRes.data if (!order) return { code: 404, msg: '订单不存在' } // 2. 校验角色:乘客只能取消,司机才能推进行程 if (role === 'passenger' && nextStatus !== 9) { return { code: 403, msg: '乘客无权推进该状态' } } // 3. 校验跃迁合法性 const allowed = TRANSITIONS[order.status] || [] if (!allowed.includes(nextStatus)) { return { code: 400, msg: `不允许从 ${order.status} 到 ${nextStatus}` } } // 4. 写入新状态,并记录操作人 await db.collection('orders').doc(orderId).update({ data: { status: nextStatus, updateTime: db.serverDate(), lastOperator: openid } }) return { code: 0, msg: 'ok' } }

逻辑说明:TRANSITIONS是整段代码的灵魂,它把「什么状态能到哪」变成数据而不是散落的 if-else,以后加状态只改这张表。角色校验放在跃迁校验之前,是因为乘客和司机的权限边界不同——乘客能取消,但不能替司机点「已到达」。db.serverDate()用服务端时间而不是客户端时间,避免用户改手机时间导致订单时间线错乱。

参数说明:orderId是订单文档 ID,nextStatus是目标状态数字,role是调用方身份。这三个参数必须由前端传,但服务端要重新校验,不能信前端。lastOperator存 openid 是为了后面排查「谁把订单改乱了」时有据可查。

2.3 前端怎么根据状态渲染不同按钮

状态机定好之后,前端就轻松了。乘客页和司机页各自维护一个「状态 → 按钮文案 → 点击调用」的映射,不要写一堆if (status === 1) ... else if (status === 2)。我一般写成配置对象:

// pages/passenger/order.js const PASSENGER_ACTIONS = { 0: { text: '等待司机接单', disabled: true }, 1: { text: '司机正在赶来', disabled: true }, 2: { text: '确认上车', next: 3, role: 'passenger' }, 3: { text: '行程中', disabled: true }, 4: { text: '去支付', next: 5, role: 'passenger' }, 5: { text: '已完成', disabled: true }, 9: { text: '已取消', disabled: true } } Page({ data: { order: null, action: null }, onLoad(options) { this.orderId = options.orderId this.watchOrder() }, watchOrder() { // 用 watch 监听订单变化,司机端一改,乘客端自动刷新 const db = wx.cloud.database() this.watcher = db.collection('orders').doc(this.orderId).watch({ onChange: snapshot => { const order = snapshot.docs[0] this.setData({ order, action: PASSENGER_ACTIONS[order.status] }) }, onError: err => console.error('订单监听失败', err) }) }, async onActionTap() { const { action, order } = this.data if (!action || action.disabled) return await wx.cloud.callFunction({ name: 'updateOrder', data: { orderId: order._id, nextStatus: action.next, role: action.role } }) }, onUnload() { // 页面卸载必须关掉监听,否则会内存泄漏 if (this.watcher) this.watcher.close() } })

逻辑说明:PASSENGER_ACTIONS把状态和 UI 绑在一起,新增状态只加一行。watch是云开发数据库的实时监听能力,司机端改了状态,乘客端不用轮询就能收到推送,这是小程序做双端同步最省事的方案。onUnload里关监听是必须的,我见过有人忘了关,来回切页面几次之后小程序直接卡死。

参数说明:snapshot.docs[0]是监听结果里的订单文档,action.next是目标状态,action.role传给云函数做权限判断。注意disabled: true的按钮不要绑点击事件,或者绑了也要在函数里再拦一次,防止用户通过开发者工具强行触发。

3. 地图选点和距离计算:别让用户手动输地址

3.1 用 chooseLocation 拿到起终点坐标

出行小程序最影响体验的一步是地址输入。让用户手打「XX 路 88 号」既慢又容易错,常见做法是调wx.chooseLocation让用户在地图上点选,拿到经纬度和名称。这个 API 会打开一个地图选点界面,用户拖动地图或搜索后确认,返回{ name, address, latitude, longitude }。

// pages/passenger/create.js Page({ data: { start: null, end: null }, async chooseStart() { const res = await wx.chooseLocation({ latitude: 39.908, // 默认中心点纬度,一般给城市中心 longitude: 116.397, // 默认中心点经度 scale: 16 // 缩放级别,16 能看到街道 }).catch(() => null) if (!res) return // 用户取消 this.setData({ start: res }) }, async chooseEnd() { const res = await wx.chooseLocation({ latitude: this.data.start ? this.data.start.latitude : 39.908, longitude: this.data.start ? this.data.start.longitude : 116.397, scale: 16 }).catch(() => null) if (!res) return this.setData({ end: res }) } })

逻辑说明:chooseLocation返回的latitude/longitude是 GCJ-02 坐标系,小程序地图组件用的也是这套,所以直接存直接渲染,不需要转换。默认中心点给城市中心是为了让用户打开时不是一片空白。选终点时如果已经选了起点,就把地图中心挪到起点附近,减少用户拖动距离。

参数说明:scale控制缩放,16 左右适合看街道,太小看不到路名,太大视野又不够。latitude/longitude是必填,不传会报错。用户取消时chooseLocation会 reject,所以要用.catch(() => null)兜住,不然控制台一片红。

3.2 直线距离和预估价格怎么算

Demo 阶段不需要接真实路径规划,用两点间球面距离乘一个系数就够了。下面这个函数算的是 Haversine 距离,单位公里:

// utils/distance.js function toRad(d) { return d * Math.PI / 180 } // 计算两个经纬度点之间的球面距离,返回公里 function getDistanceKm(lat1, lng1, lat2, lng2) { const R = 6371 // 地球平均半径,公里 const dLat = toRad(lat2 - lat1) const dLng = toRad(lng2 - lng1) const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) return R * c } // 预估价格:起步价 + 里程费,最低不低于起步价 function estimatePrice(distanceKm) { const BASE = 8 // 起步价,元 const PER_KM = 2.5 // 每公里单价,元 const BASE_KM = 3 // 起步价包含的公里数 if (distanceKm <= BASE_KM) return BASE return BASE + (distanceKm - BASE_KM) * PER_KM } module.exports = { getDistanceKm, estimatePrice }

逻辑说明:Haversine 公式把地球当球体算两点大圆距离,对城市内几公里的订单来说误差可以忽略。estimatePrice用「起步价含 N 公里,超出部分按公里加价」的常见出租车计费模型,比单纯距离 × 单价更接近真实。注意这里算的是直线距离,实际道路距离通常是直线的 1.3 到 1.5 倍,Demo 里可以在结果上乘 1.4 做粗略修正。

参数说明:R = 6371是地球平均半径,单位公里,换成米就乘 1000。BASE_KM是起步价覆盖的公里数,设 3 公里比较符合多数城市。PER_KM是超出部分单价,设 2.5 元。这些数字都应该是配置项,不要硬编码在业务逻辑里,后面调价方便。

3.3 把选点结果写进订单并展示路线

选完起终点后,下单时把坐标和名称一起写进订单文档,司机端就能直接拿到。展示路线用小程序map组件的polyline属性,把起终点连一条直线:

// pages/passenger/create.js 下单部分 async submitOrder() { const { start, end } = this.data if (!start || !end) { wx.showToast({ title: '请选择起终点', icon: 'none' }) return } const { getDistanceKm, estimatePrice } = require('../../utils/distance') const km = getDistanceKm(start.latitude, start.longitude, end.latitude, end.longitude) const price = estimatePrice(km) const db = wx.cloud.database() const res = await db.collection('orders').add({ data: { status: 0, start, end, distanceKm: Number(km.toFixed(2)), price, createTime: db.serverDate(), passengerOpenid: '{openid}' // 云开发会自动替换成当前用户 } }) wx.navigateTo({ url: `/pages/passenger/order?orderId=${res._id}` }) }

逻辑说明:distanceKm保留两位小数,避免存一长串浮点数。price在下单时就算好存进去,后面支付直接用,不要在支付时重算,否则用户看到的价格和下单时不一致会投诉。passengerOpenid用'{openid}'占位符,云开发写入时会自动替换成当前用户 openid,这是云数据库的便捷写法。

参数说明:start和end是chooseLocation返回的完整对象,包含 name、address、latitude、longitude。status: 0对应待接单。createTime用服务端时间。订单 ID 由云数据库自动生成,通过res._id拿到后跳转到订单详情页。

4. 双端同步与司机接单:轮询还是实时监听

4.1 watch 实时监听和定时轮询的取舍

双端同步有两种做法:一是前端定时调接口拉最新订单,二是用云数据库的watch实时监听。轮询实现简单,但间隔设短了费流量、设长了司机接单后乘客要等好几秒才看到。watch是长连接推送,状态一变立刻到,体验好很多。

但watch有它的边界:单个小程序同时监听的文档数量有限制,而且页面卸载必须关。我一般只在订单详情页用watch,列表页用轮询或者下拉刷新。下面是一个司机端监听「待接单列表」的简化写法,实际项目里待接单列表更适合用轮询,因为它是集合查询不是单文档:

// pages/driver/hall.js 司机接单大厅 Page({ data: { orders: [] }, onShow() { this.loadPending() // 每 5 秒刷新一次待接单列表 this.timer = setInterval(() => this.loadPending(), 5000) }, onHide() { clearInterval(this.timer) }, async loadPending() { const db = wx.cloud.database() const res = await db.collection('orders') .where({ status: 0 }) .orderBy('createTime', 'desc') .limit(20) .get() this.setData({ orders: res.data }) }, async grabOrder(e) { const orderId = e.currentTarget.dataset.id const { result } = await wx.cloud.callFunction({ name: 'updateOrder', data: { orderId, nextStatus: 1, role: 'driver' } }) if (result.code !== 0) { wx.showToast({ title: result.msg, icon: 'none' }) return } wx.navigateTo({ url: `/pages/driver/order?orderId=${orderId}` }) } })

逻辑说明:待接单列表是「多个订单」的集合,watch对集合查询的支持不如单文档稳定,所以这里用 5 秒轮询。onHide里清定时器是必须的,否则用户切到后台还在跑,耗电。grabOrder调云函数而不是直接改数据库,是为了让状态跃迁走统一入口,防止两个司机同时接单时都写成功。

参数说明:where({ status: 0 })只查待接单。limit(20)限制一次拉 20 条,避免数据量大时卡顿。orderBy('createTime', 'desc')让最新的订单排前面。e.currentTarget.dataset.id是列表项上绑的订单 ID。

4.2 防止两个司机同时接单的并发处理

上面grabOrder有个隐患:两个司机几乎同时点接单,两个云函数实例都读到status: 0,都判断可以跃迁到 1,然后都写成功,结果一个订单被两个司机接了。这是出行类 Demo 最典型的并发坑。

解决办法是在云函数里用数据库的原子更新,把「检查状态」和「更新状态」合成一步:

// cloudfunctions/updateOrder/index.js 接单部分改造 if (nextStatus === 1) { // 只有当前 status 还是 0 时才更新,返回更新条数 const updateRes = await db.collection('orders') .where({ _id: orderId, status: 0 }) .update({ data: { status: 1, driverOpenid: openid, acceptTime: db.serverDate() } }) if (updateRes.stats.updated === 0) { return { code: 409, msg: '手慢了,订单已被接走' } } return { code: 0, msg: 'ok' } }

逻辑说明:where({ _id: orderId, status: 0 })把状态作为更新条件,数据库保证这条更新是原子的——如果另一个司机已经把它改成 1,这次where就匹配不到,updated为 0,直接返回失败。这样就不需要加锁,也不用担心竞态。driverOpenid记录接单司机,后面司机端查自己的订单靠它。

参数说明:updateRes.stats.updated是实际更新的文档数,0 表示没更新到。acceptTime记录接单时间,用于后面算司机等待时长。返回码 409 表示冲突,前端拿到后提示用户刷新列表。

4.3 司机端和乘客端的状态展示差异

同一个订单,乘客和司机看到的信息不一样。乘客关心「司机到哪了、还有多久」,司机关心「乘客在哪、路线怎么走」。我一般用同一个订单文档,但两端渲染时取不同字段:

字段乘客端展示司机端展示
start起点名称导航目标
end终点名称终点名称
status状态文案操作按钮
driverOpenid不展示判断是否是自己接的单
price预估价格行程结束后展示

乘客端在状态 1 时可以展示一个「司机正在赶来」的动画,司机端在状态 1 时展示「已接单,前往上车点」和导航按钮。两端都监听同一个订单文档,任何一方操作,另一方通过watch立刻收到。这里有个细节:司机端在状态 2 点「已到达」后,乘客端按钮才变成「确认上车」,所以状态 2 是两端交互的同步点,不能省。

5. 避坑与常见问题排查

5.1 地图选点返回 null 导致下单失败

现象:用户点了选点,也确认了,但chooseLocation返回null,后面读res.latitude直接报错。

原因:chooseLocation在用户取消时会 reject,如果没 catch,Promise 链断掉,res就是 undefined。另外部分机型在权限没给的情况下也会返回失败。

解决:所有chooseLocation调用都加.catch(() => null),拿到结果先判空再往下走。权限问题在app.json里声明requiredPrivateInfos,并在首次进入时引导用户授权。

5.2 watch 监听不触发或触发多次

现象:司机改了状态,乘客端界面没反应;或者乘客端一次操作触发了两次回调。

原因:watch不触发通常是没在onLoad里建立监听,或者订单 ID 传错。触发多次一般是页面被重复创建,比如navigateTo叠了好几层,每层都建了监听。

解决:监听只在onLoad建一次,onUnload关掉。跳转订单详情用redirectTo而不是navigateTo,避免页面栈里堆多个相同页面。如果确实需要navigateTo,在onShow里判断监听是否已存在,存在就不重复建。

5.3 云函数里拿不到 openid

现象:云函数里cloud.getWXContext().OPENID是 undefined,导致权限校验全失败。

原因:云函数必须由小程序端通过wx.cloud.callFunction调用才会带上用户上下文。如果你在云函数控制台直接测试,或者用 HTTP 触发,是拿不到 openid 的。

解决:本地调试时在云函数控制台测试要手动传一个模拟 openid,或者干脆在小程序开发者工具里点按钮触发。正式逻辑里不要依赖控制台测试的结果,以真机调用为准。

5.4 订单状态跳跃导致流程错乱

现象:乘客还没确认上车,订单直接到了「待支付」。

原因:某个云函数调用没走TRANSITIONS校验,或者前端传了错误的nextStatus。也可能是数据库被手动改过。

解决:所有状态变更必须走updateOrder云函数,云函数里TRANSITIONS校验不能省。数据库权限设成「仅创建者可读写」不够,因为司机也要读,正确做法是设成「所有用户可读,仅管理端可写」,写操作全部收口到云函数。

5.5 真机上地图不显示或定位偏移

现象:开发者工具里地图正常,真机上白屏或者定位点偏到几百米外。

原因:真机需要用户授权定位权限,没授权时地图可能不显示。定位偏移通常是坐标系问题,小程序地图用 GCJ-02,如果你从其他来源拿了 WGS-84 坐标直接塞进去就会偏。

解决:在app.json里声明permission的scope.userLocation,首次进入地图页时调wx.authorize引导授权。坐标统一用chooseLocation返回的,不要自己从别处拼。如果必须用外部坐标,先做坐标系转换再渲染。

6. 把 Demo 跑起来之后还能往哪走

一个能跑通的最小闭环做完之后,你会发现真正有意思的部分才刚开始。我一般会先加一个「模拟司机移动」的功能来验证实时轨迹:在云函数里起一个定时任务,每隔几秒把司机的坐标往乘客方向挪一点,写进订单的driverLocation字段,乘客端地图上就能看到小车在动。这个功能不需要真实司机,但能把「实时位置同步」这条链路完整跑一遍。

// cloudfunctions/mockDriverMove/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 把司机位置向终点推进一步,step 是每次移动的经纬度增量 exports.main = async (event) => { const { orderId, step = 0.001 } = event const orderRes = await db.collection('orders').doc(orderId).get() const order = orderRes.data if (!order || order.status !== 1) return { code: 0, msg: 'skip' } const cur = order.driverLocation || order.start const target = order.start // 简单线性插值,朝起点移动 const nextLat = cur.latitude + (target.latitude - cur.latitude) * 0.2 const nextLng = cur.longitude + (target.longitude - cur.longitude) * 0.2 await db.collection('orders').doc(orderId).update({ data: { driverLocation: { latitude: nextLat, longitude: nextLng }, updateTime: db.serverDate() } }) return { code: 0, msg: 'moved' } }

逻辑说明:这里用 0.2 的插值系数,每次调用把司机位置往起点挪剩余距离的 20%,越接近越慢,看起来比较自然。step参数留着是为了以后换成按固定距离移动。判断order.status !== 1就跳过,避免订单已经进入行程中还在挪位置。乘客端地图上把driverLocation渲染成一个 marker,配合watch就能看到小车动起来。

参数说明:orderId是订单 ID,step是预留的步长参数,当前用插值系数代替。driverLocation初始值取order.start,也就是司机从起点开始。这个云函数需要配合定时触发器,在云开发控制台配一个每分钟触发一次的定时器,或者在小程序端用setInterval调,但后者切后台会停,只适合演示。

再往后走,可以加的东西包括:行程结束后的费用结算(把distanceKm和实际时长结合算最终价)、订单评价、历史订单列表。但我的习惯是每加一个功能之前先问自己:这个功能会不会引入新的状态?如果会,先改TRANSITIONS表,再写代码。状态机是这类小程序的骨架,骨架歪了,后面加什么都是补丁摞补丁。我踩过最深的坑就是一开始图省事,把「司机到达」和「行程中」合成一个状态,结果乘客端和司机端的按钮逻辑纠缠不清,最后花了两倍时间拆回来。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 8:47:25

光伏逆变器阻抗建模与扫频分析技术详解

1. 项目背景与核心价值光伏并网逆变器的阻抗特性分析是新能源电力系统稳定性研究的关键环节。这个项目复现了博士论文中关于逆变器阻抗建模与扫频验证的核心方法&#xff0c;特别关注了锁相环(PLL)和电流控制环对系统稳定性的影响。在实际工程中&#xff0c;类似的分析方法被广…

作者头像 李华
网站建设 2026/10/15 8:47:21

PS5折腾笔记:数据迁移、M.2扩容、远程串流与保养验机

先聊一个我隔三差五就会收到的提问&#xff1a;“PS5到手之后&#xff0c;除了开机打游戏&#xff0c;到底还能怎么折腾&#xff1f;”如果你问的是改装、刷机、魔改系统那类方向&#xff0c;我劝你趁早收手&#xff0c;这事儿既违规又容易把机器变成砖。但如果你问的是数据迁移…

作者头像 李华
网站建设 2026/10/15 8:47:20

R语言群落生态学分析:从数据处理到可视化实战

1. 项目概述群落生态学是研究物种在特定时空范围内分布、相互作用及其与环境关系的重要领域。作为一名长期使用R语言进行生态学数据分析的研究者&#xff0c;我深知这个领域的数据处理、统计建模和可视化存在几个典型痛点&#xff1a;数据格式复杂&#xff08;如物种矩阵、环境…

作者头像 李华
网站建设 2026/10/15 8:47:05

APK重新签名全流程:签名方案、工具选型与踩坑指南

拿到一个已经签好名的APK&#xff0c;想重新换签名&#xff0c;这事在开发、测试分发、渠道定制、安全分析这些环节里非常常见。你要是直接右键替换签名&#xff0c;往往会在安装或运行时踩到“签名不一致”“无法覆盖安装”“启动闪退”之类的坑。这篇内容就是梳理一套我实际验…

作者头像 李华
网站建设 2026/10/15 8:46:51

爬虫必看:图形验证码与滑块验证码识别原理与实战

写爬虫的人&#xff0c;早晚都会撞上验证码这东西。我最早遇到它的时候&#xff0c;正爬一个站点爬得顺风顺水&#xff0c;突然返回一张歪歪扭扭的字母图片&#xff0c;程序当场就断了。当时的第一反应是“绕过去”&#xff0c;换代理、降频率、抢接口&#xff0c;折腾一圈发现…

作者头像 李华