Парсинг количества товара на Wildberries
Есть задача спарсить остатки товара с помощью только requests. Selenium, bs4 и пр. библиотеки использовать нельзя.
В ручном режиме остатки можно увидеть на странице корзины, если добавлять туда товар. То есть, добавляем товар в корзину, идём в неё и там играемся с количеством.
Начинаю с самого первого пункта плана. Добавление товара в корзину - это post вот на такой url (в декодированном виде):
https://a.wb.ru/e/ec?t=iPhone 14 Pro Max 1TB (США) Apple 139760729 купить за 153 992 ₽ в интернет-магазине Wildberries&u=https://www.wildberries.ru/catalog/139760729/detail.aspx&cid=4&s=800x980x24&w=400x490&user_id=6292790831681475795&vbn=324&r=https://www.wildberries.ru/catalog/139760729/detail.aspx
Часть параметров из карточки товара, s, w - это размеры экрана устройства, насколько мне удалось разобраться. Что есть vbn и откуда брать user_id? Последний сохраняется в куках, но достать его с помощью стандартного подхода не получается - куки сессии в итоге пустые:
import requests
s = requests.Session()
res = s.get("https://www.wildberries.ru/",
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.79 Safari/537.36',
'accept': '*/*'
},
)
print(s.cookies)
Как получить куки? Или может сам подход неправильный? Занимаюсь этим впервые.
Ответы (2 шт):
А ведь интересная задачка ?
В том смысле, что проверять количество товаров в наличии через насилие корзины — довольно остроумно. Не знаю зачем это вам, но поехали.
Получаем 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.
Но это оставим на домашнее задание. Принцип, я думаю, теперь понятен, а на описание циклов, возможно, уйдёт не один день.
кол-во товаров можно получить по запросу https://card.wb.ru/cards/v1/detail?appType=1&curr=rub&dest=-1257786&spp=30&nm=15686973, где nm - это номер страницы товара. в ответ приходит json, в нем ищем sizes, а в нем stocks (массив) - тут все остатки по всем складам поставщика. Заголовки в запросе простейшие, куки не нужны. можно просто в браузер вставить строку запросу и получить json.
"stocks": [
{
"wh": 117986,
"dtype": 4,
"qty": 1,
"priority": 46078,
"time1": 5,
"time2": 69
},
{
"wh": 212032,
"dtype": 1,
"qty": 94,
"priority": 54801,
"time1": 25,
"time2": 39
}
],
