А ведь интересная задачка 😀
В том смысле, что проверять количество товаров в наличии через насилие корзины — довольно остроумно. Не знаю зачем это вам, но поехали.
Получаем cookies
Тут вы, в общем то, делаете всё правильно, создаёте сессию, в сессии они будут храниться. В большинстве случаев cookies появятся там сами после первого GET запроса, но… тут у нас немного посложнее будет.
Печенюх у нас не так много, можно посмотреть в браузере, какие есть.
А это:
- ___wbs
- ___wbu
- __wba_s
- _wbauid
- BasketUID
Как их получать?
Если cookies не подтягиваются автоматически, первое что стоит попробовать, это поискать содержание запросов на предмет set-cookie
, т.к. именно там в headers они могут быть присвоены. Поищем.
Для этого идём в developer console, вкладка Network, с которой вы уже знакомы, и ищем где ставятся cookies.

Я использую браузер Firefox, но в Chrome всё выглядит примерно так же. Стоит также отметить, что поиск надо вести во вкладке Search, а не filter URLs, это два разных поиска. В Chrome-based браузерах для этого необходимо нажать на лупу, она находится сверху-слева в панели инструментов.
Чтож, супер, нашли!
BasketUID можно уже забирать.
import requests
s = requests.Session()
headers = {'x-requested-with': 'XMLHttpRequest'}
s.post('https://www.wildberries.ru/webapi/basket/info', headers=headers)
print(s.cookies)
Вывод:
<RequestsCookieJar[<Cookie BasketUID=d874176e-4c6d-4b29-b582-5f14a07f6506 for .wildberries.ru/>]>
Sanitize делать не буду, с каждой новой сессией UID будет новый, так что не страшно его здесь показать.
Чудно, также видим __wbs и __wbu, которые можно получить, но… Смотрим запрос, в request headers уже указаны __wba_s, _wbauid и BasketUID. Копируем в Postman, или же используем Edit and Resend в Firefox, пробуем убрать cookies в request и смотрим дадут ли их нам в response. Хмм, забавно, выдают.
По итогу можем получить всё кроме _wbauid, который нам нужен, а его нет. Какой вывод можно сделать? Он берётся откуда-то из js скриптов, придётся помучаться.
Забираем cookie из javascript
⚠ Должен отметить, что я не гуру JS, поэтому, возможно, есть более простые и эффективные подходы.
Итак, метод дедукции приводит нас к тому, что _wbauid получается откуда то из js. Проверяем теорию, запрещаем скрипты в dev console и стираем cookies в браузере.
Методом тыка (а есть более простой метод?) запрещаем все скрипты и по одному разрешаем. Оказывается, что _wbauid присваивается в /sdk/sdk.js
.
Придётся заглянуть в js код. В ряде случаев, кода очень много. Учитывая, что искомый id мы не нашли в запросах, он может как-то банально рассчитываться, либо же на основе нескольких нетривиальных запросов. Anyway, смотрим код. Используем pretty print, иначе минифицированное полотно смотреть невозможно. Пробуем банальный поиск по _wbauid.
Опа, а вот и код где он создаётся:
o.prototype.genNewUserID = function() {
var t = Math.floor((new Date).getTime() / 1e3)
, e = Math.floor(Math.random() * Math.pow(2, 30)).toString() + t.toString()
, n = new Date;
return n.setTime(n.getTime() + 31536e6),
document.cookie = "_wbauid=" + e + "; expires=" + n.toUTCString() + "; path=/; domain=" + this._cookieDomain,
e
}
Можно поставить breakpoint и посмотреть значения в момент создания, чтобы убедиться, что всё правильно нашли. Да-да, это тот самый ID!
Как видно, это довольно простые расчёты без запросов.
Осталось повторить этот код на python и получить долгожданный cookie.
Получаем t
:
Аналогом .getTime()
является .timestamp()
.
Интересно, что JS выдаёт целочисленное число из 13 цифр, Python выдаёт 16 цифр, но 6 из них в дроби. Соответственно / 1e3
нам не нужно, достаточно написать int()
и будет округление до целочисленного числа аналогичное Math.floor()
. Кода получается даже меньше, результат тот же, целочисленное число из 10 цифр.
Получаем e
:
- аналогом
Math.random()
будет random.random()
.
В качестве разницы, возможно они выдают разное максимальное количество после запятой, но учитывая дальнейшее округление это не так важно.
- аналогом
Math.pow(2, 30)
будет pow(2,30)
.
Но, не совсем понятно зачем это делать, результат будет всегда одним и тем же, так что можно просто взять результат — 1073741824.
- Результат округляется вниз, уже знаем, что для этого есть
int()
- Ну и вместо
toString()
будет str()
.
получаем n
:
Так, а что здесь происходит? Если переводить на Python, то получим:
n = datetime.now()
n = datetime.fromtimestamp((int(n.timestamp()*1000) + 31536e6) / 1000)
Помним ведь про разницу в 13 и 16 цифр после запятой? Поэтому получается так страшно, но по сути нам надо просто добавить год и отнять один день, поэтому код будет выглядеть гораздо проще.
Добавляем cookie.
Как это правильно сделать?
Вариантов разобраться с этим много. Можно подсмотреть готовый ответ, но лично меня всегда распирает от любопытства, откуда например взялась опция 1?
В целом, создавать свои cookies, конечно, не рекомендуется, но в нашем случае необходимо. Смотрим тип через type(s.cookies)
. Вторую опцию можно подсмотреть в документации здесь и здесь, первую же через dir(s.cookies)
и s.cookies.set.__code__.co_varnames
.
В итоге
import requests
from datetime import datetime
from dateutil.relativedelta import relativedelta
import random
s = requests.Session()
n = datetime.now()
t = int(n.timestamp())
e = str(int(random.random() * pow(2,30))) + str(int(t))
n = n + relativedelta(years=1) - relativedelta(days=1)
s.cookies.set('_wbauid', e, domain='.wildberries.ru', expires=int(n.timestamp()))
Получаем количество доступного товара
Есть задача спарсить остатки товара с помощью только requests.
Как получить куки? Или может сам подход неправильный? Занимаюсь этим впервые.
C cookie разобрались. Но, они нам не пригодятся. Зачем я про них расписывал? Если, вдруг, будет необходимо автоматизировать заказ товаров, то, конечно же, для корзины они пригодятся.
Но наша задача довольно тривиальна.
Добавление товара в корзину - это post вот на такой url (в декодированном виде):
Даже если занимаетесь "впервые" ход мыслей правильный. 👍🏻
Но есть нюанс.
Попробуйте заблокировать https://a.wb.ru/e/ec
(block URL) и посмотреть что будет. Корзина продолжает работать! Как же так? Не берусь судить, что делает указанный вами url, возможно статистику отправляет, возможно на стороне backend в корзину добавляет, но если его повторить "Edit and Resend" в корзине ничего не меняется.
Так, а что ещё происходит после нажатия кнопки? Если заблокировать basketAdderHelper.min.js и / или basketAdderAdapter.min.js тогда корзина не сработает. Но они на сервер ничего не отправляют.
Также если заблокировать GET запрос card.wb.ru/cards/detail
то… Сайт напишет, что товара нет в наличии.
Стоп, что?
Именно это мы и ищем, наличие. Как это работает, JS подготавливает данный GET запрос и‥ пишет в local storage. Так что, если кому-то придёт в голову автоматизировать покупку товаров в корзине знаете куда смотреть и где она формируется. Локально.
Нам же достаточно посмотреть в GET запрос, как раз таки там и содержится информация которую ищем, корзину для этого насиловать не надо.
import requests
import urllib.parse
s = requests.Session()
address = 'Ивановская площадь, Москва'
url = 'https://nominatim.openstreetmap.org/search/' + urllib.parse.quote(address) +'?format=json'
coords = requests.get(url).json()
params = {
'latitude': coords[0]["lat"],
'longitude': coords[0]["lon"],
'address': 'Город чудес'
}
r = s.get('https://user-geo-data.wildberries.ru/get-geo-info', params = params).json()
params = {p.split('=')[0]:p.split('=')[1] for p in r['xinfo'].split('&')}
search_params = {
'query': 'iPhone',
'resultset': 'catalog',
'page': 1
}
search_params.update(params)
url = 'https://search.wb.ru/exactmatch/ru/common/v4/search'
r = s.get(url, params=search_params).json()
card_params = params
url = 'https://card.wb.ru/cards/detail'
for ix, product in enumerate(r['data']['products'][:10]):
card_params['nm'] = product['id']
r = s.get(url, params = card_params).json()
print('{}: {}'.format(ix+1, product['name']), end=' ')
q = []
for i in r['data']['products']:
for j in i['sizes']:
for k in j['stocks']:
q.append(k['qty'])
print(' '.join(str(x) for x in q))
Вывод:
1: iPhone 13 128GB (Global) 522
2: iPhone 13 128GB (Global) 36
3: iPhone 11 64GB (Global) 347
4: iPhone 13 128GB (Global) 689
5: iPhone 13 128GB (Global) 594
6: iPhone 14 128GB (США) 42
7: iPhone 11 128GB (Global) 389 1 3
8: iPhone 14 128GB (Гонконг) 17
9: iPhone 13 256GB (Global) 34
10: iPhone 13 128GB (Гонконг) 24
Первую часть кода про каталог / поиск и параметры очень подробно рассказал здесь.
Для каждого product
идём в карточку и забираем количество. Три цикла — это dirty hack. Обратите внимание на 7 позицию, там три числа. На самом деле их ещё больше, по хорошему надо ещё забирать расцветки из basket-10.wb.ru…card.json, метчить id размеров с понятными названиями и разобраться что такое wh, скорее всего это id складов и названия у них тоже, наверняка, есть.
По расцветкам, у каждой расцветки свой nm_id.
Но это оставим на домашнее задание. Принцип, я думаю, теперь понятен, а на описание циклов, возможно, уйдёт не один день.