├── README.md ├── .gitignore └── nike_robot.py /README.md: -------------------------------------------------------------------------------- 1 | ### nike自动抢鞋子机器人 2 | 3 | > 每次发售的`Air Jordan` 基本都是秒抢,不甘心,一定要自己实现个能够自动抢鞋的 4 | 5 | ----------- 6 | 7 | ##### 目前已经基本完成的小功能 8 | 9 | * 实现自动登录 10 | * 输入鞋子介绍页,分析得出有哪些鞋子在售 11 | 12 | 13 | 14 | 15 | ##### 遇到的小问题 16 | 17 | * 自动添加购物车失败(**添加购物车基本代表抢到了**) 18 | * 第一次分析结果 19 | * 基本就是`cookie` 的问题, 然后反复对`nike`官网的`cookie`键值对做测试,发现,当删除了`AnalysisUserId`, 此时官网也会下单失败,但是响应和程序添加购物车响应不一样, 关键问题时,打印添加购物车请求中的`cookie`时, 有这个键值 20 | 21 | * 第二次分析结果 22 | * 如果是`cookie`的问题, 那么我将下单成功时的`url`拷贝出来,放在浏览器直接请求,肯定可以成功, 因为浏览器会去带上所有必要的`cookie`, 但是很不幸地,浏览器请求还是报`429` 23 | * 这样的话,是可以排除掉`cookie`的原因, 那么不是`cookie`, 只可能是请求头了,最后对比发现,两个请求头,发现下单成功的请求头带了`Referer`, 而下单失败的木有带这个请求头 24 | * 最后加上了这个请求头后,发现居然成功了。。。。。。 25 | 26 | 27 | ##### 接下来要实现的功能 28 | 29 | - [ ] 自动下单, 分析了一下网络请求,目前这个还是比较复杂,🙈 30 | - [ ] 监控微博,实现新品发售提醒 31 | - [ ] 看有没有可能接入洋葱 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask instance folder 58 | instance/ 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # Created by .ignore support plugin (hsz.mobi) 92 | .idea/ 93 | -------------------------------------------------------------------------------- /nike_robot.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | """ 4 | 5 | requests (2.9.1) 6 | python (3.5.2) 7 | 8 | """ 9 | 10 | import requests 11 | import logging 12 | import json 13 | import time 14 | from threading import Thread 15 | import re 16 | # import redis 17 | import sys 18 | import random 19 | import pdb 20 | 21 | logging.basicConfig(level=logging.DEBUG, 22 | format='%(asctime)s ' 23 | '%(threadName)s ' 24 | '%(levelname)s ' 25 | '%(message)s') 26 | LOG = logging.getLogger(__name__) 27 | 28 | HEADERS = { 29 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) ' 30 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 31 | 'Chrome/51.0.2704.106 Safari/537.36' 32 | } 33 | # 登陆check 地址 34 | LOGIN_URL = 'https://www.nike.com/profile/login?Content-Locale=zh_CN' 35 | # 提交订单地址 36 | PUT_ORDER_URL = 'https://secure-store.nike.com/ap/services/' \ 37 | 'jcartService?qty=1&rt=json&view=3&' 38 | MAX_FAIL_TIMES = 100 # 每个下单线程的最大失败重试次数 39 | MAX_RETRY_TIMES = 200 # 每个下单线程的重新提交订单次数 40 | # 是否开启调试模式 41 | DEBUG = False 42 | 43 | # 全局session 44 | session = requests.Session() 45 | 46 | # 添加购物车订阅实例 47 | # r = redis.StrictRedis('localhost', 6379) 48 | # p = r.pubsub() 49 | is_add_cart_success = False 50 | 51 | 52 | class NikeLoginParam(object): 53 | """登录时, 请求body体参数""" 54 | 55 | def __init__(self, username, password, client_id): 56 | super(NikeLoginParam, self).__init__() 57 | self.login = username 58 | self.password = password 59 | # self.client_id = client_id 60 | # self.grant_type = 'password' 61 | self.rememberMe = True 62 | # self.ux_id = 'com.nike.commerce.nikedotcom.web' 63 | 64 | def to_json(self): 65 | return json.dumps(self, default=lambda o: o.__dict__, 66 | sort_keys=True, indent=4) 67 | 68 | def __repr__(self): 69 | return '此次登陆用户名:%s, 密码:%s' % (self.login, self.password) 70 | 71 | 72 | # 73 | # class AddToCartParam(object): 74 | # """下单请求参数""" 75 | # 76 | # def __init__(self, product_id, price, sku_id, line1, line2): 77 | # super(AddToCartParam, self).__init__() 78 | # self.productId = product_id 79 | # self.price = price 80 | # self.skuId = sku_id 81 | # self.skuAndSize = '%s:43' % sku_id 82 | # self.line1 = line1 83 | # self.line2 = line2 84 | 85 | 86 | class ShoeInfo(object): 87 | """鞋子商品信息类""" 88 | 89 | def __init__(self, info_url, info_id, shoe_color): 90 | super(ShoeInfo, self).__init__() 91 | self.info_url = info_url 92 | self.info_id = info_id 93 | self.shoe_color = shoe_color 94 | 95 | def __repr__(self): 96 | return '鞋子ID: %s \n 鞋子颜色: %s \n 鞋子地址:%s ' % (self.info_id, 97 | self.shoe_color, 98 | self.info_url) 99 | 100 | 101 | class RegexMatcher(object): 102 | """正则匹配工具类""" 103 | 104 | # groups = [] 105 | 106 | # value_dict_list = [] 107 | 108 | def __init__(self, regex): 109 | self.groups = [] 110 | self.value_dict_list = [] 111 | self.capture_group_name_reg = re.compile('\(\?P<(?P.*?)>') 112 | self.__parse_reg(regex) 113 | self.regex = re.compile(regex) 114 | 115 | def __parse_reg(self, regex): 116 | for match in self.capture_group_name_reg.finditer(regex): 117 | self.groups.append(match.group('group_name')) 118 | 119 | def match(self, content): 120 | if content is None: 121 | raise AssertionError 122 | for match in self.regex.finditer(content): 123 | d = {} 124 | for group in self.groups: 125 | d[group] = match.group(group) 126 | if len(d) > 0: 127 | self.value_dict_list.append(d) 128 | return self 129 | 130 | def get_value(self, group_name): 131 | if group_name not in self.groups: 132 | raise KeyError 133 | result = [] 134 | if DEBUG: 135 | pdb.set_trace() 136 | for d in self.value_dict_list: 137 | result.append(d[group_name]) 138 | if len(result) == 0: 139 | raise MatchNoResult 140 | return result[0] 141 | 142 | def get_values(self, group_name): 143 | return self.value_dict_list 144 | 145 | def find_with_arg(self, **kwargs): 146 | for d in self.value_dict_list: 147 | is_find = True 148 | for k, v in kwargs.items(): 149 | if d[k] != v: 150 | is_find = False 151 | break 152 | if is_find: 153 | return d 154 | return None 155 | 156 | def __str__(self): 157 | return str(self.value_dict_list) 158 | 159 | 160 | class MatchNoResult(Exception): 161 | """docstring for MatchNoResult""" 162 | pass 163 | 164 | 165 | class AddToCartTask(Thread): 166 | """添加到购物车线程,主要负责鞋子添加到购物车中, 抢鞋的时候,添加购物车成功基本就算抢到了""" 167 | 168 | def __init__(self, param, pd_url): 169 | super(AddToCartTask, self).__init__() 170 | self.param = param 171 | self.pd_url = pd_url 172 | 173 | def run(self): 174 | global is_add_cart_success 175 | retry_times = 1 # 尝试提交订单次数 176 | 177 | HEADERS['Referer'] = self.pd_url # 带上Referer试试能否成功, 不带这个头, 会一直报429 178 | # p.subscribe('addToCart-notice-channel') 179 | while True: 180 | if is_add_cart_success: 181 | break 182 | start = time.time() 183 | LOG.info('[第%s次] 开始尝试添加到购物车了', retry_times) 184 | add_to_cart_url = PUT_ORDER_URL + \ 185 | '_=' + str(int(1000 * time.time())) 186 | if DEBUG: 187 | pdb.set_trace() 188 | res = session.get( 189 | add_to_cart_url, params=self.param, headers=HEADERS) 190 | LOG.info(res.url) 191 | end = time.time() 192 | status_code = res.status_code 193 | # r.publish('addToCart-notice-channel', 'SUCCESS') 194 | if status_code == 200: 195 | res_status_pattern = re.compile('"status" :"(?P.*?)"') 196 | LOG.info(res.text) 197 | res_status_match = res_status_pattern.search(res.text) 198 | if res_status_match: 199 | res_status = res_status_match.group('status') 200 | if res_status == 'success': # 添加到购物车成功 201 | LOG.info('恭喜添加购物车成功.......; 耗时%s秒', (end - start)) 202 | is_add_cart_success = True 203 | # r.publish('addToCart-notice-channel', 'SUCCESS') 204 | break 205 | elif res_status == 'wait': # 下单需要排队,继续下单 206 | random_wait_time = random.uniform(2.0, 3.0) 207 | LOG.info("排队中......, %.2f 秒后继续重试", random_wait_time) 208 | time.sleep(random_wait_time) 209 | elif res_status == 'failure': 210 | # 截取失败信息 211 | fail_message_pattern = re.compile('message"\s:"(.*?)"') 212 | fail_message_match = fail_message_pattern.search( 213 | res.text) 214 | if fail_message_match: 215 | LOG.info('添加购物车失败; 原因:%s', 216 | fail_message_match.group(1)) 217 | # r.publish('addToCart-notice-channel', 'FAIL') 218 | sys.exit(1) 219 | LOG.info('提交订单异常[%s]!', status_code) 220 | time.sleep(1) # 隔一秒进行重新提交 221 | retry_times += 1 222 | 223 | 224 | # 登陆获取关键token参数 225 | def login(param): 226 | LOG.info('开始登陆Nike官网了.......') 227 | session.get('http://www.nike.com/cn/zh_cn') 228 | start = time.time() 229 | res = session.post(LOGIN_URL, data=param.__dict__, headers=HEADERS) 230 | end = time.time() 231 | status_code = res.status_code 232 | if status_code == 200: 233 | if DEBUG: 234 | pdb.set_trace() 235 | # key_token = json.loads(content)['access_token'] 236 | # if json.loads(content)['access_token'] is not None: 237 | # LOG.info(res.cookies) 238 | LOG.info('登陆Nike官网成功,耗时%s秒', (end - start)) 239 | else: 240 | # key_token = None 241 | LOG.error('登陆Nike官网失败[%s]!', status_code) 242 | sys.exit(1) 243 | 244 | 245 | # 清洗html; 去掉空格以及script标签 246 | def clean_html(html_content): 247 | return html_content 248 | 249 | 250 | def get_order_param(product_index_url): 251 | param_d = {'callback': 'nike_Cart_handleJCartResponse', 252 | 'passcode': 'null', 'sizeType': 'null'} 253 | 254 | # 从url 中取出默认的pid 255 | selected_pid = RegexMatcher('\/pid-(?P.*?)\/'). \ 256 | match(product_index_url).get_value('selected_pid') 257 | LOG.info('默选pid: %s', selected_pid) 258 | # 用户选择颜色 259 | pd_content = session.get(product_index_url).text 260 | # LOG.info(pd_content) 261 | pd_area = RegexMatcher('
(?P[\s\S]*?' 262 | 'data-status="IN_STOCK"[\s\S]*?)
'). \ 263 | match(pd_content).get_value('area') 264 | # LOG.debug('产品区域文本:%s', pd_area) 265 | in_stock_matcher = RegexMatcher('' 266 | '[\s\S]*?)"' 267 | '\sdata-productid="' 268 | '(?P\d+)"\stitle="(?P' 269 | '[\s\S]*?)"[\s\S]*?<\/a>').match(pd_area) 270 | LOG.info('提取出的在售信息:%s', in_stock_matcher) 271 | user_select_pid = input('请输入你要选择的pid: ') 272 | if user_select_pid != selected_pid: 273 | new_pd_url = in_stock_matcher.find_with_arg( 274 | pid=user_select_pid, )['pd_url'] 275 | pd_content = session.get(new_pd_url).text # 重新获取一次其他参数 276 | # 构造订单参数 277 | param_area = RegexMatcher('' 279 | '(?P[\s\S]*?)'). \ 280 | match(pd_content). \ 281 | get_value('param_area') 282 | # LOG.debug('提交订单参数区:%s', param_area) 283 | # 通用参数 284 | common_param_matcher = RegexMatcher('.*?)"' 286 | '\s*?(value="(?P.*?)")?\s*?/>'). \ 287 | match(param_area) 288 | LOG.debug('通用参数信息:%s', common_param_matcher) 289 | 290 | for d in common_param_matcher.get_values(''): 291 | if len(d) is not 2: 292 | continue 293 | value = d['value'] 294 | if value is None or len(value) == 0: 295 | value = 'null' 296 | param_d[d['key']] = value 297 | LOG.info(str(param_d)) 298 | # 用户选择尺码和大小 299 | size_matcher = RegexMatcher('.*?)\")?\s' 300 | '*?name=\"skuId\"\s*?value=\"' 301 | '(?P[\s\S]*?):(?P[\s\S]*?)\"'). \ 302 | match(param_area) 303 | # 去掉售罄的尺码; 格式输出稍微舒服点 304 | for d in size_matcher.get_values(''): 305 | extra = d['extra'] 306 | if extra is not None and 'selectBox-disabled' in extra: 307 | continue 308 | LOG.info('目前还有货的尺码大小: %s; sku_id: %s', d['size'], d['sku_id']) 309 | # size_matcher.get_values('') 310 | sku_id = input('请选择你需要尺码对应的sku_id: ') 311 | d = size_matcher.find_with_arg(sku_id=sku_id, ) 312 | LOG.info(str(d)) 313 | param_d['skuId'] = sku_id 314 | param_d['displaySize'] = d['size'] # 有可能要改????? 315 | param_d['skuAndSize'] = '%s:%s' % (sku_id, d['size']) 316 | 317 | return param_d 318 | 319 | 320 | if __name__ == '__main__': 321 | user_name = input('请输入你的nike用户名:') 322 | password = input('请输入你的nike密码:') 323 | nike_login_param = NikeLoginParam( 324 | user_name, password, '') 325 | login(nike_login_param) 326 | pd_url = input('请输入鞋子地址:') 327 | order_param = get_order_param(pd_url) 328 | threads = [] 329 | for i in range(1): 330 | thread = AddToCartTask(order_param, pd_url) 331 | thread.start() 332 | for thread in threads: 333 | thread.join() 334 | # get_order_param( 335 | # 'http://store.nike.com/cn/zh_cn/pd/zoom-all-out-low-%E7%94%B7%E5%AD%90%E8%B7%91%E6%AD%A5%E9%9E%8B/pid-11241589/pgid-11464061') 336 | --------------------------------------------------------------------------------