├── .DS_Store ├── .github └── workflows │ └── main.yml ├── README.md ├── __init__.py ├── __manifest__.py ├── __pycache__ └── __init__.cpython-37.pyc ├── controllers ├── Main.py ├── __init__.py └── __pycache__ │ ├── Main.cpython-37.pyc │ └── __init__.cpython-37.pyc ├── data └── jd_cookie.xml ├── models ├── .DS_Store ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-37.pyc │ └── jd_access_token.cpython-37.pyc ├── jd_access_token.py └── tools │ ├── .DS_Store │ ├── .idea │ ├── .gitignore │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── jd_maotai_seckill-master.iml │ ├── misc.xml │ └── modules.xml │ ├── README.md │ ├── __pycache__ │ ├── config.cpython-37.pyc │ ├── exception.cpython-37.pyc │ ├── jd_logger.cpython-37.pyc │ ├── jd_spider_requests.cpython-37.pyc │ ├── timer.cpython-37.pyc │ └── util.cpython-37.pyc │ ├── config.ini │ ├── config.py │ ├── exception.py │ ├── jd_logger.py │ ├── jd_spider_requests.py │ ├── main.py │ ├── qr_code.png │ ├── requirements.txt │ ├── timer.py │ └── util.py ├── security ├── ir.model.access.csv └── security.xml ├── static ├── .DS_Store └── description │ └── icon.png └── views ├── jd_access_token.xml └── menu_main.xml /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - uses: actions/checkout@v2 28 | 29 | # Runs a single command using the runners shell 30 | - name: Run a one-line script 31 | run: echo Hello, world! 32 | 33 | # Runs a set of commands using the runners shell 34 | - name: Run a multi-line script 35 | run: | 36 | echo Add other actions to build, 37 | echo test, and deploy your project. 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 当前模块重点 2 | --------------- 3 | 4 | 功能说明:基于odoo-13,自动登录京东获取cookie 5 | 6 | 1.新增账户配置模块,每天自动更新cookie 7 | 8 | 2.基本架构布置 9 | 10 | 11 | 12 | 功能点:自动抢购茅台 13 | ---------------------- 14 | 定时任务设置 15 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author:Cyril 4 | 5 | from . import controllers 6 | from . import models 7 | from . import data 8 | -------------------------------------------------------------------------------- /__manifest__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author:Cyril 4 | 5 | { 6 | 'name': "京东茅台助手", 7 | 'summary': 'design by Cyril', 8 | 'version': '1.0', 9 | 'category': "基础", 10 | 'sequence': 10, 11 | 'author': 'Cyril', 12 | 'website': 'https://github.com/cyril0216/jd-master', 13 | 'depends': ['base'], 14 | 'data': [ 15 | 'security/security.xml', 16 | 'security/ir.model.access.csv', 17 | 'data/jd_cookie.xml', 18 | 'views/menu_main.xml', 19 | 'views/jd_access_token.xml', 20 | ], 21 | 'demo': [ 22 | # 'demo/demo.xml', 23 | ], 24 | 'qweb': [ 25 | # 'static/src/xml/test.xml', 26 | ], 27 | 'installable': True, 28 | 'application': True, 29 | 'auto_install': False, 30 | 'description': """ 31 | 32 | """, 33 | } 34 | -------------------------------------------------------------------------------- /__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /controllers/Main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author:Cyril 4 | 5 | import os 6 | from jinja2 import Environment, FileSystemLoader 7 | 8 | global BASE_DIR 9 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 10 | global env 11 | templateLoader = FileSystemLoader(searchpath=BASE_DIR + "/templates") 12 | env = Environment(loader=templateLoader) 13 | import logging 14 | 15 | _logger = logging.getLogger(__name__) 16 | 17 | import odoo 18 | from odoo import http 19 | from odoo.http import request 20 | from odoo.addons.web.controllers.main import WebClient, Binary, Home 21 | import json 22 | 23 | 24 | class Website(Home): 25 | pass 26 | -------------------------------------------------------------------------------- /controllers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author:Cyril 4 | 5 | 6 | from . import Main -------------------------------------------------------------------------------- /controllers/__pycache__/Main.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/controllers/__pycache__/Main.cpython-37.pyc -------------------------------------------------------------------------------- /controllers/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/controllers/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /data/jd_cookie.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 京东更新cookie 7 | 100 8 | minutes 9 | -1 10 | 11 | 12 | model.get_token() 13 | code 14 | 15 | 16 | -------------------------------------------------------------------------------- /models/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/.DS_Store -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author:Cyril 4 | 5 | from . import jd_access_token 6 | 7 | 8 | -------------------------------------------------------------------------------- /models/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /models/__pycache__/jd_access_token.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/__pycache__/jd_access_token.cpython-37.pyc -------------------------------------------------------------------------------- /models/jd_access_token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Author:Cyril 4 | 5 | from datetime import datetime, timedelta 6 | from functools import partial 7 | from itertools import groupby 8 | 9 | from odoo import api, fields, models, SUPERUSER_ID, _ 10 | from odoo.exceptions import AccessError, UserError, ValidationError 11 | from odoo.tools.misc import formatLang, get_lang 12 | from odoo.osv import expression 13 | from odoo.tools import float_is_zero, float_compare 14 | from werkzeug.urls import url_encode 15 | import requests, json 16 | 17 | class JD_access_token(models.Model): 18 | _name = "jd_access_token" 19 | _description = "京东账户相关" 20 | _log_access = True 21 | _check_worflow_auto = False 22 | 23 | 24 | account = fields.Char(string='账户') 25 | passwd = fields.Integer(string='密码') 26 | cookie = fields.Char(string='cookie') 27 | 28 | 29 | def get_token(self): 30 | 31 | 32 | pass 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /models/tools/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/tools/.DS_Store -------------------------------------------------------------------------------- /models/tools/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /models/tools/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /models/tools/.idea/jd_maotai_seckill-master.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /models/tools/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /models/tools/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /models/tools/README.md: -------------------------------------------------------------------------------- 1 | # Jd_Seckill 2 | 3 | ## 特别声明: 4 | 5 | * 本仓库发布的`jd_seckill`项目中涉及的任何脚本,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。 6 | 7 | * 本项目内所有资源文件,禁止任何公众号、自媒体进行任何形式的转载、发布。 8 | 9 | * `huanghyw` 对任何脚本问题概不负责,包括但不限于由任何脚本错误导致的任何损失或损害. 10 | 11 | * 间接使用脚本的任何用户,包括但不限于建立VPS或在某些行为违反国家/地区法律或相关法规的情况下进行传播, `huanghyw` 对于由此引起的任何隐私泄漏或其他后果概不负责。 12 | 13 | * 请勿将`jd_seckill`项目的任何内容用于商业或非法目的,否则后果自负。 14 | 15 | * 如果任何单位或个人认为该项目的脚本可能涉嫌侵犯其权利,则应及时通知并提供身份证明,所有权证明,我们将在收到认证文件后删除相关脚本。 16 | 17 | * 以任何方式查看此项目的人或直接或间接使用`jd_seckill`项目的任何脚本的使用者都应仔细阅读此声明。`huanghyw` 保留随时更改或补充此免责声明的权利。一旦使用并复制了任何相关脚本或`jd_seckill`项目,则视为您已接受此免责声明。 18 | 19 | * 您必须在下载后的24小时内从计算机或手机中完全删除以上内容。 20 | 21 | * 本项目遵循`GPL-3.0 License`协议,如果本特别声明与`GPL-3.0 License`协议有冲突之处,以本特别声明为准。 22 | 23 | > ***您使用或者复制了本仓库且本人制作的任何代码或项目,则视为`已接受`此声明,请仔细阅读*** 24 | > ***您在本声明未发出之时点使用或者复制了本仓库且本人制作的任何代码或项目且此时还在使用,则视为`已接受`此声明,请仔细阅读*** 25 | 26 | ## 简介 27 | 通过我这段时间的使用(2020-12-12至2020-12-17),证实这个脚本确实能抢到茅台。我自己三个账号抢了四瓶,帮两个朋友抢了4瓶。 28 | 大家只要确认自己配置文件没有问题,Cookie没有失效,坚持下去总能成功的。 29 | 30 | 根据这段时间大家的反馈,除了茅台,其它不需要加购物车的商品也不能抢。具体原因还没有进行排查,应该是京东非茅台商品抢购流程发生了变化。 31 | 为了避免耽误大家的时间,先不要抢购非茅台商品。 32 | 等这个问题处理好了,会上线新版本。 33 | 34 | 35 | ## 暗中观察 36 | 37 | 根据12月14日以来抢茅台的日志分析,大胆推断再接再厉返回Json消息中`resultCode`与小白信用的关系。 38 | 这里主要分析出现频率最高的`90016`和`90008`。 39 | 40 | ### 样例JSON 41 | ```json 42 | {'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 90016, 'skuId': 0, 'success': False} 43 | {'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 90008, 'skuId': 0, 'success': False} 44 | ``` 45 | 46 | ### 数据统计 47 | 48 | | 案例 | 小白信用 | 90016 | 90008 | 抢到耗时 | 49 | | ---- | ---- | ---- | ---- | ---- | 50 | | 张三 | 63.8 | 59.63% | 40.37% | 暂未抢到 | 51 | | 李四 | 92.9 | 72.05% | 27.94% | 4天 | 52 | | 王五 | 99.6 | 75.70% | 24.29% | 暂未抢到 | 53 | | 赵六 | 103.4 | 91.02% | 8.9% | 2天 | 54 | 55 | ### 猜测 56 | 推测返回90008是京东的风控机制,代表这次请求直接失败,不参与抢购。 57 | 小白信用越低越容易触发京东的风控。 58 | 59 | 从数据来看小白信用与风控的关系大概每十分为一个等级,所以赵六基本上没有被拦截,李四和王五的拦截几率相近,张三的拦截几率最高。 60 | 61 | 风控放行后才会进行抢购,这时候用的应该是水库计数模型,假设无法一次性拿到所有数据的情况下来尽量的做到抢购成功用户的均匀分布,这样就和概率相关了。 62 | 63 | > 综上,张三想成功有点困难,小白信用是100+的用户成功几率最大。 64 | 65 | ## 主要功能 66 | 67 | - 登陆京东商城([www.jd.com](http://www.jd.com/)) 68 | - 用京东APP扫码给出的二维码 69 | - 预约茅台 70 | - 定时自动预约 71 | - 秒杀预约后等待抢购 72 | - 定时开始自动抢购 73 | 74 | ## 运行环境 75 | 76 | - [Python 3](https://www.python.org/) 77 | 78 | ## 第三方库 79 | 80 | - 需要使用到的库已经放在requirements.txt,使用pip安装的可以使用指令 81 | `pip install -r requirements.txt` 82 | - 如果国内安装第三方库比较慢,可以使用以下指令进行清华源加速 83 | `pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/` 84 | 85 | ## 使用教程 86 | #### 1. 推荐Chrome浏览器 87 | #### 2. 网页扫码登录,或者账号密码登录 88 | #### 3. 填写config.ini配置信息 89 | (1)`eid`和`fp`找个普通商品随便下单,然后抓包就能看到,这两个值可以填固定的 90 | > 随便找一个商品下单,然后进入结算页面,打开浏览器的调试窗口,切换到控制台Tab页,在控制台中输入变量`_JdTdudfp`,即可从输出的Json中获取`eid`和`fp`。 91 | > 不会的话参考原作者的issue https://github.com/zhou-xiaojun/jd_mask/issues/22 92 | 93 | (2)`sku_id`,`DEFAULT_USER_AGENT` 94 | > `sku_id`已经按照茅台的填好。 95 | > `cookies_string` 现在已经不需要填写了 96 | > `DEFAULT_USER_AGENT` 可以用默认的。谷歌浏览器也可以浏览器地址栏中输入about:version 查看`USER_AGENT`替换 97 | 98 | (3)配置一下时间 99 | > 现在不强制要求同步最新时间了,程序会自动同步京东时间 100 | >> 但要是电脑时间快慢了好几个小时,最好还是同步一下吧 101 | 102 | 以上都是必须的. 103 | > tips: 104 | > 在程序开始运行后,会检测本地时间与京东服务器时间,输出的差值为本地时间-京东服务器时间,即-50为本地时间比京东服务器时间慢50ms。 105 | > 本代码的执行的抢购时间以本地电脑/服务器时间为准 106 | 107 | (4)修改抢购瓶数 108 | > 代码中默认抢购瓶数为2,且无法在配置文件中修改 109 | > 如果一个月内抢购过一瓶,最好修改抢购瓶数为1 110 | > 具体修改为:在`jd_spider_requests.py`文件中搜索`self.seckill_num = 2`,将`2`改为`1` 111 | 112 | #### 4.运行main.py 113 | 根据提示选择相应功能即可 114 | 115 | #### 5.抢购结果确认 116 | 抢购是否成功通常在程序开始的一分钟内可见分晓! 117 | 搜索日志,出现“抢购成功,订单号xxxxx",代表成功抢到了,务必半小时内支付订单!程序暂时不支持自动停止,需要手动STOP! 118 | 若两分钟还未抢购成功,基本上就是没抢到!程序暂时不支持自动停止,需要手动STOP! 119 | 120 | ## 打赏 121 | 不用再打赏了,抢到茅台的同学请保持这份喜悦,没抢到的继续加油 :) 122 | 123 | ## 感谢 124 | ##### 非常感谢原作者 https://github.com/zhou-xiaojun/jd_mask 提供的代码 125 | ##### 也非常感谢 https://github.com/wlwwu/jd_maotai 进行的优化 126 | -------------------------------------------------------------------------------- /models/tools/__pycache__/config.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/tools/__pycache__/config.cpython-37.pyc -------------------------------------------------------------------------------- /models/tools/__pycache__/exception.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/tools/__pycache__/exception.cpython-37.pyc -------------------------------------------------------------------------------- /models/tools/__pycache__/jd_logger.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/tools/__pycache__/jd_logger.cpython-37.pyc -------------------------------------------------------------------------------- /models/tools/__pycache__/jd_spider_requests.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/tools/__pycache__/jd_spider_requests.cpython-37.pyc -------------------------------------------------------------------------------- /models/tools/__pycache__/timer.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/tools/__pycache__/timer.cpython-37.pyc -------------------------------------------------------------------------------- /models/tools/__pycache__/util.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/tools/__pycache__/util.cpython-37.pyc -------------------------------------------------------------------------------- /models/tools/config.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | # eid, fp参数 3 | eid = "DYJCGUILOGVI55HBEAGKQFT5C3X7IYEEGDZILVUQ65GGPFIMDAB7JMNJ4EXEIQX6E7FJVEFLAHSK4G54VTANTMB5E4" 4 | fp = "dc677fa7156b4e6308d68d2fe5c68d22" 5 | 6 | # 商品id 7 | sku_id = 100012043978 8 | # 设定时间 # 2020-12-09 10:00:00.100000 9 | buy_time = 09:59:59.500 10 | # 默认UA 11 | DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 12 | random_useragent = false 13 | 14 | [account] 15 | payment_pwd = "" 16 | 17 | [messenger] 18 | enable = false 19 | sckey = 20 | -------------------------------------------------------------------------------- /models/tools/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import configparser 3 | 4 | 5 | class Config(object): 6 | def __init__(self, config_file='config.ini'): 7 | self._path = os.path.join(os.getcwd(), config_file) 8 | if not os.path.exists(self._path): 9 | raise FileNotFoundError("No such file: config.ini") 10 | self._config = configparser.ConfigParser() 11 | self._config.read(self._path, encoding='utf-8-sig') 12 | self._configRaw = configparser.RawConfigParser() 13 | self._configRaw.read(self._path, encoding='utf-8-sig') 14 | 15 | def get(self, section, name): 16 | return self._config.get(section, name) 17 | 18 | def getRaw(self, section, name): 19 | return self._configRaw.get(section, name) 20 | 21 | 22 | global_config = Config() 23 | -------------------------------------------------------------------------------- /models/tools/exception.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | 5 | class SKException(Exception): 6 | 7 | def __init__(self, message): 8 | super().__init__(message) 9 | -------------------------------------------------------------------------------- /models/tools/jd_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | ''' 4 | 日志模块 5 | ''' 6 | LOG_FILENAME = 'jd_seckill.log' 7 | logger = logging.getLogger() 8 | 9 | 10 | def set_logger(): 11 | logger.setLevel(logging.INFO) 12 | formatter = logging.Formatter('%(asctime)s - %(process)d-%(threadName)s - ' 13 | '%(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s') 14 | console_handler = logging.StreamHandler() 15 | console_handler.setFormatter(formatter) 16 | logger.addHandler(console_handler) 17 | file_handler = logging.handlers.RotatingFileHandler( 18 | LOG_FILENAME, maxBytes=10485760, backupCount=5, encoding="utf-8") 19 | file_handler.setFormatter(formatter) 20 | logger.addHandler(file_handler) 21 | 22 | set_logger() -------------------------------------------------------------------------------- /models/tools/jd_spider_requests.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | import requests 4 | import functools 5 | import json 6 | import os 7 | import pickle 8 | 9 | from lxml import etree 10 | from jd_logger import logger 11 | from timer import Timer 12 | from config import global_config 13 | from concurrent.futures import ProcessPoolExecutor 14 | from exception import SKException 15 | from util import ( 16 | parse_json, 17 | send_wechat, 18 | wait_some_time, 19 | response_status, 20 | save_image, 21 | open_image 22 | ) 23 | 24 | 25 | class SpiderSession: 26 | """ 27 | Session相关操作 28 | """ 29 | def __init__(self): 30 | self.cookies_dir_path = "./cookies/" 31 | self.user_agent = global_config.getRaw('config', 'DEFAULT_USER_AGENT') 32 | 33 | self.session = self._init_session() 34 | 35 | def _init_session(self): 36 | session = requests.session() 37 | session.headers = self.get_headers() 38 | return session 39 | 40 | def get_headers(self): 41 | return {"User-Agent": self.user_agent, 42 | "Accept": "text/html,application/xhtml+xml,application/xml;" 43 | "q=0.9,image/webp,image/apng,*/*;" 44 | "q=0.8,application/signed-exchange;" 45 | "v=b3", 46 | "Connection": "keep-alive"} 47 | 48 | def get_user_agent(self): 49 | return self.user_agent 50 | 51 | def get_session(self): 52 | """ 53 | 获取当前Session 54 | :return: 55 | """ 56 | return self.session 57 | 58 | def get_cookies(self): 59 | """ 60 | 获取当前Cookies 61 | :return: 62 | """ 63 | return self.get_session().cookies 64 | 65 | def set_cookies(self, cookies): 66 | self.session.cookies.update(cookies) 67 | 68 | def load_cookies_from_local(self): 69 | """ 70 | 从本地加载Cookie 71 | :return: 72 | """ 73 | cookies_file = '' 74 | if not os.path.exists(self.cookies_dir_path): 75 | return False 76 | for name in os.listdir(self.cookies_dir_path): 77 | if name.endswith(".cookies"): 78 | cookies_file = '{}{}'.format(self.cookies_dir_path, name) 79 | break 80 | if cookies_file == '': 81 | return False 82 | with open(cookies_file, 'rb') as f: 83 | local_cookies = pickle.load(f) 84 | self.set_cookies(local_cookies) 85 | 86 | def save_cookies_to_local(self, cookie_file_name): 87 | """ 88 | 保存Cookie到本地 89 | :param cookie_file_name: 存放Cookie的文件名称 90 | :return: 91 | """ 92 | cookies_file = '{}{}.cookies'.format(self.cookies_dir_path, cookie_file_name) 93 | directory = os.path.dirname(cookies_file) 94 | if not os.path.exists(directory): 95 | os.makedirs(directory) 96 | with open(cookies_file, 'wb') as f: 97 | pickle.dump(self.get_cookies(), f) 98 | 99 | 100 | class QrLogin: 101 | """ 102 | 扫码登录 103 | """ 104 | def __init__(self, spider_session: SpiderSession): 105 | """ 106 | 初始化扫码登录 107 | 大致流程: 108 | 1、访问登录二维码页面,获取Token 109 | 2、使用Token获取票据 110 | 3、校验票据 111 | :param spider_session: 112 | """ 113 | self.qrcode_img_file = 'qr_code.png' 114 | 115 | self.spider_session = spider_session 116 | self.session = self.spider_session.get_session() 117 | 118 | self.is_login = False 119 | self.refresh_login_status() 120 | 121 | def refresh_login_status(self): 122 | """ 123 | 刷新是否登录状态 124 | :return: 125 | """ 126 | self.is_login = self._validate_cookies() 127 | 128 | def _validate_cookies(self): 129 | """ 130 | 验证cookies是否有效(是否登陆) 131 | 通过访问用户订单列表页进行判断:若未登录,将会重定向到登陆页面。 132 | :return: cookies是否有效 True/False 133 | """ 134 | url = 'https://order.jd.com/center/list.action' 135 | payload = { 136 | 'rid': str(int(time.time() * 1000)), 137 | } 138 | try: 139 | resp = self.session.get(url=url, params=payload, allow_redirects=False) 140 | if resp.status_code == requests.codes.OK: 141 | return True 142 | except Exception as e: 143 | logger.error("验证cookies是否有效发生异常", e) 144 | return False 145 | 146 | def _get_login_page(self): 147 | """ 148 | 获取PC端登录页面 149 | :return: 150 | """ 151 | url = "https://passport.jd.com/new/login.aspx" 152 | page = self.session.get(url, headers=self.spider_session.get_headers()) 153 | return page 154 | 155 | def _get_qrcode(self): 156 | """ 157 | 缓存并展示登录二维码 158 | :return: 159 | """ 160 | url = 'https://qr.m.jd.com/show' 161 | payload = { 162 | 'appid': 133, 163 | 'size': 147, 164 | 't': str(int(time.time() * 1000)), 165 | } 166 | headers = { 167 | 'User-Agent': self.spider_session.get_user_agent(), 168 | 'Referer': 'https://passport.jd.com/new/login.aspx', 169 | } 170 | resp = self.session.get(url=url, headers=headers, params=payload) 171 | 172 | if not response_status(resp): 173 | logger.info('获取二维码失败') 174 | return False 175 | 176 | save_image(resp, self.qrcode_img_file) 177 | logger.info('二维码获取成功,请打开京东APP扫描') 178 | open_image(self.qrcode_img_file) 179 | return True 180 | 181 | def _get_qrcode_ticket(self): 182 | """ 183 | 通过 token 获取票据 184 | :return: 185 | """ 186 | url = 'https://qr.m.jd.com/check' 187 | payload = { 188 | 'appid': '133', 189 | 'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)), 190 | 'token': self.session.cookies.get('wlfstk_smdl'), 191 | '_': str(int(time.time() * 1000)), 192 | } 193 | headers = { 194 | 'User-Agent': self.spider_session.get_user_agent(), 195 | 'Referer': 'https://passport.jd.com/new/login.aspx', 196 | } 197 | resp = self.session.get(url=url, headers=headers, params=payload) 198 | 199 | if not response_status(resp): 200 | logger.error('获取二维码扫描结果异常') 201 | return False 202 | 203 | resp_json = parse_json(resp.text) 204 | if resp_json['code'] != 200: 205 | logger.info('Code: %s, Message: %s', resp_json['code'], resp_json['msg']) 206 | return None 207 | else: 208 | logger.info('已完成手机客户端确认') 209 | return resp_json['ticket'] 210 | 211 | def _validate_qrcode_ticket(self, ticket): 212 | """ 213 | 通过已获取的票据进行校验 214 | :param ticket: 已获取的票据 215 | :return: 216 | """ 217 | url = 'https://passport.jd.com/uc/qrCodeTicketValidation' 218 | headers = { 219 | 'User-Agent': self.spider_session.get_user_agent(), 220 | 'Referer': 'https://passport.jd.com/uc/login?ltype=logout', 221 | } 222 | 223 | resp = self.session.get(url=url, headers=headers, params={'t': ticket}) 224 | if not response_status(resp): 225 | return False 226 | 227 | resp_json = json.loads(resp.text) 228 | if resp_json['returnCode'] == 0: 229 | return True 230 | else: 231 | logger.info(resp_json) 232 | return False 233 | 234 | def login_by_qrcode(self): 235 | """ 236 | 二维码登陆 237 | :return: 238 | """ 239 | self._get_login_page() 240 | 241 | # download QR code 242 | if not self._get_qrcode(): 243 | raise SKException('二维码下载失败') 244 | 245 | # get QR code ticket 246 | ticket = None 247 | retry_times = 85 248 | for _ in range(retry_times): 249 | ticket = self._get_qrcode_ticket() 250 | if ticket: 251 | break 252 | time.sleep(2) 253 | else: 254 | raise SKException('二维码过期,请重新获取扫描') 255 | 256 | # validate QR code ticket 257 | if not self._validate_qrcode_ticket(ticket): 258 | raise SKException('二维码信息校验失败') 259 | 260 | self.refresh_login_status() 261 | 262 | logger.info('二维码登录成功') 263 | 264 | 265 | class JdSeckill(object): 266 | 267 | def __init__(self): 268 | self.spider_session = SpiderSession() 269 | self.spider_session.load_cookies_from_local() 270 | self.qrlogin = QrLogin(self.spider_session) 271 | 272 | # 初始化信息 273 | self.sku_id = global_config.getRaw('config', 'sku_id') 274 | self.seckill_num = 1 275 | self.seckill_init_info = dict() 276 | self.seckill_url = dict() 277 | self.seckill_order_data = dict() 278 | self.timers = Timer() 279 | 280 | self.session = self.spider_session.get_session() 281 | self.user_agent = self.spider_session.user_agent 282 | self.nick_name = None 283 | 284 | def login_by_qrcode(self): 285 | """ 286 | :return: 287 | """ 288 | if self.qrlogin.is_login: 289 | logger.info('登录成功') 290 | return 291 | 292 | self.qrlogin.login_by_qrcode() 293 | 294 | if self.qrlogin.is_login: 295 | self.nick_name = self.get_username() 296 | self.spider_session.save_cookies_to_local(self.nick_name) 297 | else: 298 | raise SKException("登录失败!") 299 | 300 | def check_login(func): 301 | """ 302 | 用户登陆态校验装饰器。若用户未登陆,则调用扫码登陆 303 | """ 304 | @functools.wraps(func) 305 | def new_func(self, *args, **kwargs): 306 | if not self.qrlogin.is_login: 307 | logger.info("{0} 需登陆后调用,开始扫码登陆".format(func.__name__)) 308 | self.login_by_qrcode() 309 | return func(self, *args, **kwargs) 310 | return new_func 311 | 312 | @check_login 313 | def reserve(self): 314 | """ 315 | 预约 316 | """ 317 | self._reserve() 318 | 319 | @check_login 320 | def seckill(self): 321 | """ 322 | 抢购 323 | """ 324 | self._seckill() 325 | 326 | @check_login 327 | def seckill_by_proc_pool(self, work_count=5): 328 | """ 329 | 多进程 330 | work_count:进程数量 331 | """ 332 | with ProcessPoolExecutor(work_count) as pool: 333 | for i in range(work_count): 334 | pool.submit(self.seckill) 335 | 336 | def _reserve(self): 337 | """ 338 | 预约 339 | """ 340 | while True: 341 | try: 342 | self.make_reserve() 343 | break 344 | except Exception as e: 345 | logger.info('预约发生异常!', e) 346 | wait_some_time() 347 | 348 | def _seckill(self): 349 | """ 350 | 抢购 351 | """ 352 | while True: 353 | try: 354 | self.request_seckill_url() 355 | while True: 356 | self.request_seckill_checkout_page() 357 | self.submit_seckill_order() 358 | except Exception as e: 359 | logger.info('抢购发生异常,稍后继续执行!', e) 360 | wait_some_time() 361 | 362 | def make_reserve(self): 363 | """商品预约""" 364 | logger.info('商品名称:{}'.format(self.get_sku_title())) 365 | url = 'https://yushou.jd.com/youshouinfo.action?' 366 | payload = { 367 | 'callback': 'fetchJSON', 368 | 'sku': self.sku_id, 369 | '_': str(int(time.time() * 1000)), 370 | } 371 | headers = { 372 | 'User-Agent': self.user_agent, 373 | 'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id), 374 | } 375 | resp = self.session.get(url=url, params=payload, headers=headers) 376 | resp_json = parse_json(resp.text) 377 | reserve_url = resp_json.get('url') 378 | self.timers.start() 379 | while True: 380 | try: 381 | self.session.get(url='https:' + reserve_url) 382 | logger.info('预约成功,已获得抢购资格 / 您已成功预约过了,无需重复预约') 383 | if global_config.getRaw('messenger', 'enable') == 'true': 384 | success_message = "预约成功,已获得抢购资格 / 您已成功预约过了,无需重复预约" 385 | send_wechat(success_message) 386 | break 387 | except Exception as e: 388 | logger.error('预约失败正在重试...') 389 | 390 | def get_username(self): 391 | """获取用户信息""" 392 | url = 'https://passport.jd.com/user/petName/getUserInfoForMiniJd.action' 393 | payload = { 394 | 'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)), 395 | '_': str(int(time.time() * 1000)), 396 | } 397 | headers = { 398 | 'User-Agent': self.user_agent, 399 | 'Referer': 'https://order.jd.com/center/list.action', 400 | } 401 | 402 | resp = self.session.get(url=url, params=payload, headers=headers) 403 | 404 | try_count = 5 405 | while not resp.text.startswith("jQuery"): 406 | try_count = try_count - 1 407 | if try_count > 0: 408 | resp = self.session.get(url=url, params=payload, headers=headers) 409 | else: 410 | break 411 | wait_some_time() 412 | # 响应中包含了许多用户信息,现在在其中返回昵称 413 | # jQuery2381773({"imgUrl":"//storage.360buyimg.com/i.imageUpload/xxx.jpg","lastLoginTime":"","nickName":"xxx","plusStatus":"0","realName":"xxx","userLevel":x,"userScoreVO":{"accountScore":xx,"activityScore":xx,"consumptionScore":xxxxx,"default":false,"financeScore":xxx,"pin":"xxx","riskScore":x,"totalScore":xxxxx}}) 414 | return parse_json(resp.text).get('nickName') 415 | 416 | def get_sku_title(self): 417 | """获取商品名称""" 418 | url = 'https://item.jd.com/{}.html'.format(global_config.getRaw('config', 'sku_id')) 419 | resp = self.session.get(url).content 420 | x_data = etree.HTML(resp) 421 | sku_title = x_data.xpath('/html/head/title/text()') 422 | return sku_title[0] 423 | 424 | def get_seckill_url(self): 425 | """获取商品的抢购链接 426 | 点击"抢购"按钮后,会有两次302跳转,最后到达订单结算页面 427 | 这里返回第一次跳转后的页面url,作为商品的抢购链接 428 | :return: 商品的抢购链接 429 | """ 430 | url = 'https://itemko.jd.com/itemShowBtn' 431 | payload = { 432 | 'callback': 'jQuery{}'.format(random.randint(1000000, 9999999)), 433 | 'skuId': self.sku_id, 434 | 'from': 'pc', 435 | '_': str(int(time.time() * 1000)), 436 | } 437 | headers = { 438 | 'User-Agent': self.user_agent, 439 | 'Host': 'itemko.jd.com', 440 | 'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id), 441 | } 442 | while True: 443 | resp = self.session.get(url=url, headers=headers, params=payload) 444 | resp_json = parse_json(resp.text) 445 | if resp_json.get('url'): 446 | # https://divide.jd.com/user_routing?skuId=8654289&sn=c3f4ececd8461f0e4d7267e96a91e0e0&from=pc 447 | router_url = 'https:' + resp_json.get('url') 448 | # https://marathon.jd.com/captcha.html?skuId=8654289&sn=c3f4ececd8461f0e4d7267e96a91e0e0&from=pc 449 | seckill_url = router_url.replace( 450 | 'divide', 'marathon').replace( 451 | 'user_routing', 'captcha.html') 452 | logger.info("抢购链接获取成功: %s", seckill_url) 453 | return seckill_url 454 | else: 455 | logger.info("抢购链接获取失败,稍后自动重试") 456 | wait_some_time() 457 | 458 | def request_seckill_url(self): 459 | """访问商品的抢购链接(用于设置cookie等""" 460 | logger.info('用户:{}'.format(self.get_username())) 461 | logger.info('商品名称:{}'.format(self.get_sku_title())) 462 | self.timers.start() 463 | self.seckill_url[self.sku_id] = self.get_seckill_url() 464 | logger.info('访问商品的抢购连接...') 465 | headers = { 466 | 'User-Agent': self.user_agent, 467 | 'Host': 'marathon.jd.com', 468 | 'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id), 469 | } 470 | self.session.get( 471 | url=self.seckill_url.get( 472 | self.sku_id), 473 | headers=headers, 474 | allow_redirects=False) 475 | 476 | def request_seckill_checkout_page(self): 477 | """访问抢购订单结算页面""" 478 | logger.info('访问抢购订单结算页面...') 479 | url = 'https://marathon.jd.com/seckill/seckill.action' 480 | payload = { 481 | 'skuId': self.sku_id, 482 | 'num': self.seckill_num, 483 | 'rid': int(time.time()) 484 | } 485 | headers = { 486 | 'User-Agent': self.user_agent, 487 | 'Host': 'marathon.jd.com', 488 | 'Referer': 'https://item.jd.com/{}.html'.format(self.sku_id), 489 | } 490 | self.session.get(url=url, params=payload, headers=headers, allow_redirects=False) 491 | 492 | def _get_seckill_init_info(self): 493 | """获取秒杀初始化信息(包括:地址,发票,token) 494 | :return: 初始化信息组成的dict 495 | """ 496 | logger.info('获取秒杀初始化信息...') 497 | url = 'https://marathon.jd.com/seckillnew/orderService/pc/init.action' 498 | data = { 499 | 'sku': self.sku_id, 500 | 'num': self.seckill_num, 501 | 'isModifyAddress': 'false', 502 | } 503 | headers = { 504 | 'User-Agent': self.user_agent, 505 | 'Host': 'marathon.jd.com', 506 | } 507 | resp = self.session.post(url=url, data=data, headers=headers) 508 | 509 | resp_json = None 510 | try: 511 | resp_json = parse_json(resp.text) 512 | except Exception: 513 | raise SKException('抢购失败,返回信息:{}'.format(resp.text[0: 128])) 514 | 515 | return resp_json 516 | 517 | def _get_seckill_order_data(self): 518 | """生成提交抢购订单所需的请求体参数 519 | :return: 请求体参数组成的dict 520 | """ 521 | logger.info('生成提交抢购订单所需参数...') 522 | # 获取用户秒杀初始化信息 523 | self.seckill_init_info[self.sku_id] = self._get_seckill_init_info() 524 | init_info = self.seckill_init_info.get(self.sku_id) 525 | default_address = init_info['addressList'][0] # 默认地址dict 526 | invoice_info = init_info.get('invoiceInfo', {}) # 默认发票信息dict, 有可能不返回 527 | token = init_info['token'] 528 | data = { 529 | 'skuId': self.sku_id, 530 | 'num': self.seckill_num, 531 | 'addressId': default_address['id'], 532 | 'yuShou': 'true', 533 | 'isModifyAddress': 'false', 534 | 'name': default_address['name'], 535 | 'provinceId': default_address['provinceId'], 536 | 'cityId': default_address['cityId'], 537 | 'countyId': default_address['countyId'], 538 | 'townId': default_address['townId'], 539 | 'addressDetail': default_address['addressDetail'], 540 | 'mobile': default_address['mobile'], 541 | 'mobileKey': default_address['mobileKey'], 542 | 'email': default_address.get('email', ''), 543 | 'postCode': '', 544 | 'invoiceTitle': invoice_info.get('invoiceTitle', -1), 545 | 'invoiceCompanyName': '', 546 | 'invoiceContent': invoice_info.get('invoiceContentType', 1), 547 | 'invoiceTaxpayerNO': '', 548 | 'invoiceEmail': '', 549 | 'invoicePhone': invoice_info.get('invoicePhone', ''), 550 | 'invoicePhoneKey': invoice_info.get('invoicePhoneKey', ''), 551 | 'invoice': 'true' if invoice_info else 'false', 552 | 'password': global_config.get('account', 'payment_pwd'), 553 | 'codTimeType': 3, 554 | 'paymentType': 4, 555 | 'areaCode': '', 556 | 'overseas': 0, 557 | 'phone': '', 558 | 'eid': global_config.getRaw('config', 'eid'), 559 | 'fp': global_config.getRaw('config', 'fp'), 560 | 'token': token, 561 | 'pru': '' 562 | } 563 | 564 | return data 565 | 566 | def submit_seckill_order(self): 567 | """提交抢购(秒杀)订单 568 | :return: 抢购结果 True/False 569 | """ 570 | url = 'https://marathon.jd.com/seckillnew/orderService/pc/submitOrder.action' 571 | payload = { 572 | 'skuId': self.sku_id, 573 | } 574 | try: 575 | self.seckill_order_data[self.sku_id] = self._get_seckill_order_data() 576 | except Exception as e: 577 | logger.info('抢购失败,无法获取生成订单的基本信息,接口返回:【{}】'.format(str(e))) 578 | return False 579 | 580 | logger.info('提交抢购订单...') 581 | headers = { 582 | 'User-Agent': self.user_agent, 583 | 'Host': 'marathon.jd.com', 584 | 'Referer': 'https://marathon.jd.com/seckill/seckill.action?skuId={0}&num={1}&rid={2}'.format( 585 | self.sku_id, self.seckill_num, int(time.time())), 586 | } 587 | resp = self.session.post( 588 | url=url, 589 | params=payload, 590 | data=self.seckill_order_data.get( 591 | self.sku_id), 592 | headers=headers) 593 | resp_json = None 594 | try: 595 | resp_json = parse_json(resp.text) 596 | except Exception as e: 597 | logger.info('抢购失败,返回信息:{}'.format(resp.text[0: 128])) 598 | return False 599 | # 返回信息 600 | # 抢购失败: 601 | # {'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 60074, 'skuId': 0, 'success': False} 602 | # {'errorMessage': '抱歉,您提交过快,请稍后再提交订单!', 'orderId': 0, 'resultCode': 60017, 'skuId': 0, 'success': False} 603 | # {'errorMessage': '系统正在开小差,请重试~~', 'orderId': 0, 'resultCode': 90013, 'skuId': 0, 'success': False} 604 | # 抢购成功: 605 | # {"appUrl":"xxxxx","orderId":820227xxxxx,"pcUrl":"xxxxx","resultCode":0,"skuId":0,"success":true,"totalMoney":"xxxxx"} 606 | if resp_json.get('success'): 607 | order_id = resp_json.get('orderId') 608 | total_money = resp_json.get('totalMoney') 609 | pay_url = 'https:' + resp_json.get('pcUrl') 610 | logger.info('抢购成功,订单号:{}, 总价:{}, 电脑端付款链接:{}'.format(order_id, total_money, pay_url)) 611 | if global_config.getRaw('messenger', 'enable') == 'true': 612 | success_message = "抢购成功,订单号:{}, 总价:{}, 电脑端付款链接:{}".format(order_id, total_money, pay_url) 613 | send_wechat(success_message) 614 | return True 615 | else: 616 | logger.info('抢购失败,返回信息:{}'.format(resp_json)) 617 | if global_config.getRaw('messenger', 'enable') == 'true': 618 | error_message = '抢购失败,返回信息:{}'.format(resp_json) 619 | send_wechat(error_message) 620 | return False 621 | -------------------------------------------------------------------------------- /models/tools/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from jd_spider_requests import JdSeckill 3 | 4 | 5 | if __name__ == '__main__': 6 | a = """ 7 | 功能列表: 8 | 9 | 1.预约商品 10 | 2.秒杀抢购商品 11 | """ 12 | print(a) 13 | 14 | jd_seckill = JdSeckill() 15 | choice_function = input('请选择:') 16 | if choice_function == '1': 17 | jd_seckill.reserve() 18 | elif choice_function == '2': 19 | jd_seckill.seckill_by_proc_pool() 20 | else: 21 | print('没有此功能') 22 | sys.exit(1) 23 | 24 | -------------------------------------------------------------------------------- /models/tools/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/models/tools/qr_code.png -------------------------------------------------------------------------------- /models/tools/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2020.4.5.1 2 | chardet==3.0.4 3 | idna==2.9 4 | lxml==4.5.1 5 | requests==2.23.0 6 | urllib3==1.25.9 7 | -------------------------------------------------------------------------------- /models/tools/timer.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import time 3 | import requests 4 | import json 5 | 6 | from datetime import datetime 7 | from jd_logger import logger 8 | from config import global_config 9 | 10 | 11 | class Timer(object): 12 | def __init__(self, sleep_interval=0.5): 13 | # '2018-09-28 22:45:50.000' 14 | # buy_time = 2020-12-22 09:59:59.500 15 | buy_time_everyday = global_config.getRaw('config', 'buy_time').__str__() 16 | localtime = time.localtime(time.time()) 17 | self.buy_time = datetime.strptime( 18 | localtime.tm_year.__str__() + '-' + localtime.tm_mon.__str__() + '-' + localtime.tm_mday.__str__() 19 | + ' ' + buy_time_everyday, 20 | "%Y-%m-%d %H:%M:%S.%f") 21 | self.buy_time_ms = int(time.mktime(self.buy_time.timetuple()) * 1000.0 + self.buy_time.microsecond / 1000) 22 | self.sleep_interval = sleep_interval 23 | 24 | self.diff_time = self.local_jd_time_diff() 25 | 26 | def jd_time(self): 27 | """ 28 | 从京东服务器获取时间毫秒 29 | :return: 30 | """ 31 | url = 'https://a.jd.com//ajax/queryServerData.html' 32 | ret = requests.get(url).text 33 | js = json.loads(ret) 34 | return int(js["serverTime"]) 35 | 36 | def local_time(self): 37 | """ 38 | 获取本地毫秒时间 39 | :return: 40 | """ 41 | return int(round(time.time() * 1000)) 42 | 43 | def local_jd_time_diff(self): 44 | """ 45 | 计算本地与京东服务器时间差 46 | :return: 47 | """ 48 | return self.local_time() - self.jd_time() 49 | 50 | def start(self): 51 | logger.info('正在等待到达设定时间:{},检测本地时间与京东服务器时间误差为【{}】毫秒'.format(self.buy_time, self.diff_time)) 52 | while True: 53 | # 本地时间减去与京东的时间差,能够将时间误差提升到0.1秒附近 54 | # 具体精度依赖获取京东服务器时间的网络时间损耗 55 | if self.local_time() - self.diff_time >= self.buy_time_ms: 56 | logger.info('时间到达,开始执行……') 57 | break 58 | else: 59 | time.sleep(self.sleep_interval) 60 | -------------------------------------------------------------------------------- /models/tools/util.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import requests 4 | import os 5 | import time 6 | 7 | from config import global_config 8 | 9 | USER_AGENTS = [ 10 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", 11 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36", 12 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", 13 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", 14 | "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36", 15 | "Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", 16 | "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", 17 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36", 18 | "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36", 19 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", 20 | "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", 21 | "Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", 22 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", 23 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", 24 | "Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", 25 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36", 26 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36", 27 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2309.372 Safari/537.36", 28 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2117.157 Safari/537.36", 29 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36", 30 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1866.237 Safari/537.36", 31 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/4E423F", 32 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36 Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10", 33 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.517 Safari/537.36", 34 | "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36", 35 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", 36 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", 37 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36", 38 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36", 39 | "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36", 40 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36", 41 | "Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36", 42 | "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36", 43 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36", 44 | "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1467.0 Safari/537.36", 45 | "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1464.0 Safari/537.36", 46 | "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1500.55 Safari/537.36", 47 | "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", 48 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", 49 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", 50 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", 51 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", 52 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", 53 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36", 54 | "Mozilla/5.0 (X11; NetBSD) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", 55 | "Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", 56 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17", 57 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17", 58 | "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15", 59 | "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14" 60 | ] 61 | 62 | 63 | def parse_json(s): 64 | begin = s.find('{') 65 | end = s.rfind('}') + 1 66 | return json.loads(s[begin:end]) 67 | 68 | 69 | def get_random_useragent(): 70 | """生成随机的UserAgent 71 | :return: UserAgent字符串 72 | """ 73 | return random.choice(USER_AGENTS) 74 | 75 | 76 | def wait_some_time(): 77 | time.sleep(random.randint(100, 300) / 1000) 78 | 79 | 80 | def send_wechat(message): 81 | """推送信息到微信""" 82 | url = 'http://sc.ftqq.com/{}.send'.format(global_config.getRaw('messenger', 'sckey')) 83 | payload = { 84 | "text":'抢购结果', 85 | "desp": message 86 | } 87 | headers = { 88 | 'User-Agent':global_config.getRaw('config', 'DEFAULT_USER_AGENT') 89 | } 90 | requests.get(url, params=payload, headers=headers) 91 | 92 | 93 | def response_status(resp): 94 | if resp.status_code != requests.codes.OK: 95 | print('Status: %u, Url: %s' % (resp.status_code, resp.url)) 96 | return False 97 | return True 98 | 99 | 100 | def open_image(image_file): 101 | if os.name == "nt": 102 | os.system('start ' + image_file) # for Windows 103 | else: 104 | if os.uname()[0] == "Linux": 105 | if "deepin" in os.uname()[2]: 106 | os.system("deepin-image-viewer " + image_file) # for deepin 107 | else: 108 | os.system("eog " + image_file) # for Linux 109 | else: 110 | os.system("open " + image_file) # for Mac 111 | 112 | 113 | def save_image(resp, image_file): 114 | with open(image_file, 'wb') as f: 115 | for chunk in resp.iter_content(chunk_size=1024): 116 | f.write(chunk) 117 | -------------------------------------------------------------------------------- /security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" 2 | "access_jd_access_token_read","jd_access_token","model_jd_access_token","base.group_user",1,1,1,1 3 | -------------------------------------------------------------------------------- /security/security.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 京东账户相关 6 | 7 | 8 | 订单配置 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/static/.DS_Store -------------------------------------------------------------------------------- /static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyril0216/jd-master/8f72cad5a88457d74752b4d8771b1f606458ad8c/static/description/icon.png -------------------------------------------------------------------------------- /views/jd_access_token.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jd_access_token.form 6 | jd_access_token 7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | jd_access_token.tree 27 | jd_access_token 28 | 1 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 京东账户相关 39 | ir.actions.act_window 40 | jd_access_token 41 | tree,form 42 | 43 |

44 | 创建账户. 45 |

46 |

47 | 48 |

49 |
50 |
51 | 52 |
53 |
-------------------------------------------------------------------------------- /views/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------