【爬虫记录-Tesla迎宾灯货源补货自动提示】 基于python+browsermobproxy+selenium的爬虫+自动发送邮件提醒

一:前言

本人python爬虫仍然在自学中,所以程序有逻辑和设计缺陷的地方欢迎各位大佬指出!以及本文中提到的缺陷,由于对browsermobproxy熟悉程度有限,所以有何解决办法或者修改逻辑可以解决都欢迎指出和评论。

二:任务目的

官方新出的Tesla迎宾灯对于毛胚的Tesla直接有着美观与使用上质的飞跃,奈何过于抢手,每次打开商城都显示无货,但小红书又看到它会偷偷补货,但奈何不知道它确切补货时间,所以,才有了这个自动检测是否有货,并发送邮件提醒。

程序逻辑很简单,5分钟爬一次Tesla的网站,并通过browsermobproxy抓取network里的网络包,并分析包是否有货,如果有货则通过邮箱提醒。(想过短信提示,公众号推送,奈何短信审核太严格,公众号个人也无法模板推送,邮件恰到好处,简单且实用,qq邮箱,收到邮件,微信和qq都会有提示的)

三:爬虫环境

程序运行环境:Ubuntu22.04

java:jdk11

python:10

Browsermob-Proxy:browsermob-proxy-2.1.4

chromedrive:

python三方库版本

【browsermob-proxy 0.8.0;selenium 4.17.2】

其余按需搭建

四:网站分析

Tesla迎宾灯官方网站:Tesla modelY迎宾灯

随后通过F12打开开发调试工具

型号分析

通过定位到型号选择的元素那,可以清楚的看到型号2025108-00-A 为modelY的迎宾灯前门;2025111-00-A为modelY的迎宾灯的后门。

请求body

打卡network可以发现,打开网页初期,其前后端有个数据通讯,https://shop.tesla.cn/inventory.json这个请求接口,根据音译可判断为库存查询,通过观察其请求body,可以发现其就是对迎宾灯库存进行查询。(我postman直接试了试这个接口请求,没有成功,有能力的可以试一试,毕竟直接请求,肯定省时高效)

返回数据

这个是它这个接口的返回数据值,可以发现是个json数组的格式,我们可以看到其每个型号都有“purchasable”字段,此字段来判断是否可购买,我也观察了其他有货的都是True,而迎宾灯因为无货所以是false,所以,我们可以抓取此数据包来判断是否补货可购买了。

五:实操

1.环境搭建

1)jdk11下载

jdk11在Ubuntu环境下的下载和配置网上教程很多,自行搜索下载安装。

2)下载chromedriver

下载网站chromedriver下载,下载后解压放置在程序对应位置即可。

3)下载browsermob-proxy-2.1.4

browsermob-proxy下载

将browsermob-proxy-2.1.4-bin.zip下载后,解压并访问程序目录。

2.代码分析

1)类初始化

在这里设置一些后续自动发送邮件服务的配置,我这里是使用163开启POP3/SMTP服务来进行发送,其中口令,可登入邮箱设置POP3/SMTP/IMAP里开启POP3/SMTP服务可以获取到。并在初始化中,输出日志地址。

def __init__(self):
	#email
	# 第三方 SMTP 服务
	self.mail_host = "smtp.163.com"  		# 设置服务器
	self.mail_user = "xxx@163.com"  		# xxx是你的163邮箱用户名【需修改自己的】
	self.mail_pass = "xxx"  				# 口令是你设置的163授权密码【需修改自己的】
	self.receivers = ['xxx@qq.com']  		# 接收邮件,可设置为你的QQ邮箱或者其他邮箱【需修改自己的】
	#日志
	self.logger = self._get_logger('./log/Tesla.log', 'info')

2)日志配置函数

确定日志等级,以及日志文件输出位置

def _get_logger(self, filename, level='info'):
    # 创建日志对象
    log = logging.getLogger(filename)
    # 设置日志级别
    log.setLevel(level_relations.get(level))
    # 日志输出格式
    fmt = logging.Formatter('%(asctime)s %(thread)d %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
    # 输出到控制台
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setFormatter(fmt)
    # 输出到文件
    # 日志文件按天进行保存,每天一个日志文件
    file_handler = handlers.TimedRotatingFileHandler(filename=filename, when='D', backupCount=1, encoding='utf-8')
    # 按照大小自动分割日志文件,一旦达到指定的大小重新生成文件
    # file_handler = handlers.RotatingFileHandler(filename=filename, maxBytes=1*1024*1024*1024, backupCount=1, encoding='utf-8')
    file_handler.setFormatter(fmt)

    log.addHandler(console_handler)
    log.addHandler(file_handler)
    return log

def LOGI(self, logstr):
    self.logger.info(logstr)

3)Tesla网站爬取

这个函数就是对网页进行爬虫,并获取network中对应接口的返回值。【但是这个函数有个弊端,如果系统里还跑有其他的java程序,运行此函数会造成隐患,由于我发现开启Proxy服务,就算在最后执行server.stop()也无法关闭java子进程,随着程序运行,会导致内存越来越高越来越高,所以,我每次执行这个函数时候直接暴力的将java进程给关了,如果有什么好的建议可以评论或私信给我,感谢!】

   def Tesla(self):
   	sResult = ""
       # 开启Proxy(这里地址是你刚才安装的对应地址)
       server = Server(r'/home/Tesla/Browsermob-Proxy/browsermob-proxy-2.1.4/bin/browsermob-proxy')
       server.start()
       proxy = server.create_proxy({'trustAllServers': True})

       # 模拟加载网页
       s = Service('./chromedriver')
       chrome_options = Options()
       chrome_options.add_argument('--proxy-server={}'.format(proxy.proxy))

	#无头进入
       chrome_options.add_argument('--headless')
       # 网站做了拒绝无头访问的反爬机制,添加此头可以迷惑网站
       chrome_options.add_argument('user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
	# 禁止加载图片
    chrome_options.add_argument('blink-settings=imagesEnabled=false')
    # 禁用加载视频
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
	#不使用GPU加速
       chrome_options.add_argument("--disable-gpu")
       chrome_options.add_argument("--no-sandbox")
       chrome_options.add_argument("--disable-dev-shm-usage")
	# 禁用扩展插件并实现窗口最大化
       chrome_options.add_argument('--ignore-certificate-errors')

       driver = webdriver.Chrome(options=chrome_options, service=s)
		#设置隐式等待时间为10秒
        driver.implicitly_wait(10)
        #设置加载超时等待
        driver.set_page_load_timeout(20)
       
       proxy.new_har("Tesla", options={'captureHeaders': True, 'captureContent': True})

	try:
		driver.get('https://shop.tesla.cn/product/model-y-puddle-light?sku=2025108-00-A')
  		except Exception as err:
	#self.logger.info('加载异常:超时!')
           self.logger.info(err)
       finally:
           sleep(1)
           try:
               result = proxy.har
               # print(result)
               for entry in result['log']['entries']:
                   _url = entry['request']['url']
                   # 根据URL找到数据接口
                   if "https://shop.tesla.cn/inventory.json" in _url:
                       _response = entry['response']
                       sResult = _response['content']['text']
                       # 获取接口返回内容
                       # print(sResult)
           except Exception as subErr:
               self.logger.info('抓包提取感兴趣包内容出错,异常为:')
               self.logger.info(subErr)
           finally:
               driver.close()
               driver.quit()
               server.stop()
               # 只适用于平台裸机
               subprocess.run(["pkill", "-f", "java"])
               self.logger.info('对Tesla货源进行了一次查询')
       return sResult

4)解析返回值,提取是否可购买

def parseJson(self, jsonStr):
	#判断是否为空
	if jsonStr == '':
		self.logger.info('库存通讯包内容为空!')
		return 0
    #用于记录哪些有货
    nResult = 0
    nIndex = 0

    # 使用正则表达式匹配花括号内的内容
    matches = re.findall(r'\{.*?\}', jsonStr)

    # 提取每个花括号中键为 "purchasable" 的值
    for match in matches:
        data = json.loads(match)
        purchasable_value = data.get("purchasable")
        output_value = 0 if purchasable_value is False else 1

        if nIndex == 0:
            if output_value == 1:
                nResult = 1
        elif nResult == 1:
            if output_value == 1:
                nResult = 3
        else:
            if output_value == 1:
                nResult = 2

        nIndex += 1
	TeslaMessage.LOGI('程序没死,成功解析了返回的报文!')
	return nResult

5)自动发送邮件

def sendemail(self, nStoreResult):
    self.logger.info('Tesla进行了补货,邮件通知!')
    AStr = "程序出错!"
    BStr = "程序出错!"

    if nStoreResult == 1:
        AStr = "ModelY前门迎宾灯有货了!"
        BStr = "后门仍未补货!"
    elif nStoreResult == 2:
        AStr = "前门仍未补货!"
        BStr = "ModelY后门迎宾灯有货了!"
    elif nStoreResult == 3:
        AStr = "ModelY前门迎宾灯有货了!"
        BStr = "ModelY后门迎宾灯有货了!"

    sendStr = AStr + BStr

    email = MIMEText(sendStr, 'plain', 'utf-8')
    email['Subject'] = 'Tesla迎宾灯补货提醒'  # 定义邮件主题
    email['From'] = self.mail_user  # 发件人
    email['To'] = ','.join(self.receivers)  # 收件人(可以添加多个,若只有一个收件人,可直接写邮箱号)

    try:
        smtpObj = smtplib.SMTP()
        smtpObj.connect(self.mail_host, 25)  # 25 为 SMTP 端口号
        smtpObj.login(self.mail_user, self.mail_pass)
        smtpObj.sendmail(self.mail_user, self.receivers, email.as_string())
        print("邮件发送成功")
    except smtplib.SMTPException:
        print("Error: 无法发送邮件")

3.程序缺陷及待优化问题

【重要!!!】

由于这里使用browsermob-proxy来进行抓取network中的包,它是基于Java的代理服务。它每次启动服务都会生成一个java的子进程,但是在python执行stop停止服务后,其子进程依然存在,并且,当我循环周期的去执行后,此进程内存会不断升高,所以,目前我采取了一个暴力的方法,直接将系统进程中java杀死(由于我系统服务器是个裸机,所以并没有什么影响,但是,当系统中还运行其他java程序会导致很大的隐患)

所以你们有什么好的建议和意见可以私信或评论,感谢各位大佬!

六:完整代码

'''
功能:自动获取Tesla迎宾灯是否有货,有货则自动邮件推送信息
'''
import re
import json
import requests
import time
import subprocess
import browsermobproxy
from browsermobproxy import Server
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium import webdriver
from time import sleep

import smtplib
from email.mime.text import MIMEText
from email.header import Header

import sys
import logging
from logging import handlers

level_relations = {
    'debug': logging.DEBUG,
    'info': logging.INFO,
    'warning': logging.WARNING,
    'error': logging.ERROR,
    'crit': logging.CRITICAL}

class TeslaAutoSendMessage:
    def __init__(self):
        #email
        # 第三方 SMTP 服务
        self.mail_host = "smtp.163.com"  # 设置服务器
        self.mail_user = "xxx@163.com"  # xxx是你的163邮箱用户名
        self.mail_pass = "xxx"  # 口令是你设置的163授权密码
        self.receivers = ['xxx@qq.com']  # 接收邮件,可设置为你的QQ邮箱或者其他邮箱

        #日志
        self.logger = self._get_logger('./log/Tesla.log', 'info')

    def Tesla(self):
        sResult = ""
        # 开启Proxy
        server = Server(r'/home/Tesla/Browsermob-Proxy/browsermob-proxy-2.1.4/bin/browsermob-proxy')
        server.start()
        proxy = server.create_proxy({'trustAllServers': True})

        # 模拟加载网页
        s = Service('./chromedriver')
        chrome_options = Options()
        chrome_options.add_argument('--proxy-server={}'.format(proxy.proxy))
        chrome_options.add_argument('--headless')
        # 网站做了拒绝无头访问的反爬机制,添加此头可以迷惑网站
        chrome_options.add_argument('user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
		# 禁止加载图片
        chrome_options.add_argument('blink-settings=imagesEnabled=false')
        # 禁用加载视频
        chrome_options.add_argument("--disable-blink-features=AutomationControlled")
        chrome_options.add_argument("--disable-gpu")
        chrome_options.add_argument("--no-sandbox")
        chrome_options.add_argument("--disable-dev-shm-usage")
        chrome_options.add_argument('--ignore-certificate-errors')
        # chrome_options.add_argument('--ignore-urlfetcher-cert-requests')

        driver = webdriver.Chrome(options=chrome_options, service=s)
        #设置隐式等待时间为10秒
        driver.implicitly_wait(10)
        #设置加载超时等待
        driver.set_page_load_timeout(20)

        proxy.new_har("Tesla", options={'captureHeaders': True, 'captureContent': True})

        try:
            driver.get('https://shop.tesla.cn/product/model-y-puddle-light?sku=2025108-00-A')
        except Exception as err:
            #self.logger.info('加载异常:超时!')
            self.logger.info(err)
        finally:
            sleep(1)
            try:
                result = proxy.har
                # print(result)
                for entry in result['log']['entries']:
                    _url = entry['request']['url']
                    # 根据URL找到数据接口
                    if "https://shop.tesla.cn/inventory.json" in _url:
                        _response = entry['response']
                        sResult = _response['content']['text']
                        # 获取接口返回内容
                        # print(sResult)
            except Exception as subErr:
                self.logger.info('抓包提取感兴趣包内容出错,异常为:')
                self.logger.info(subErr)
            finally:
                driver.close()
                driver.quit()
                server.stop()
                # 只适用于平台裸机
                subprocess.run(["pkill", "-f", "java"])
                self.logger.info('对Tesla货源进行了一次查询')

        return sResult

    #解析json
    def parseJson(self, jsonStr):

        if jsonStr == '':
            self.logger.info('库存通讯包内容为空!')
            return 0

        #用于记录哪些有货
        nResult = 0
        nIndex = 0

        # 使用正则表达式匹配花括号内的内容
        matches = re.findall(r'\{.*?\}', jsonStr)

        # 提取每个花括号中键为 "purchasable" 的值
        for match in matches:
            data = json.loads(match)
            purchasable_value = data.get("purchasable")
            output_value = 0 if purchasable_value is False else 1

            if nIndex == 0:
                if output_value == 1:
                    nResult = 1
            elif nResult == 1:
                if output_value == 1:
                    nResult = 3
            else:
                if output_value == 1:
                    nResult = 2

            nIndex += 1

        TeslaMessage.LOGI('程序没死,成功解析了返回的报文!')
        return nResult

    def sendemail(self, nStoreResult):
        self.logger.info('Tesla进行了补货,邮件通知!')
        AStr = "程序出错!"
        BStr = "程序出错!"

        if nStoreResult == 1:
            AStr = "ModelY前门迎宾灯有货了!"
            BStr = "后门仍未补货!"
        elif nStoreResult == 2:
            AStr = "前门仍未补货!"
            BStr = "ModelY后门迎宾灯有货了!"
        elif nStoreResult == 3:
            AStr = "ModelY前门迎宾灯有货了!"
            BStr = "ModelY后门迎宾灯有货了!"

        sendStr = AStr + BStr

        email = MIMEText(sendStr, 'plain', 'utf-8')
        email['Subject'] = 'Tesla迎宾灯补货提醒'  # 定义邮件主题
        email['From'] = self.mail_user  # 发件人
        email['To'] = ','.join(self.receivers)  # 收件人(可以添加多个,若只有一个收件人,可直接写邮箱号)

        try:
            smtpObj = smtplib.SMTP()
            smtpObj.connect(self.mail_host, 25)  # 25 为 SMTP 端口号
            smtpObj.login(self.mail_user, self.mail_pass)
            smtpObj.sendmail(self.mail_user, self.receivers, email.as_string())
            print("邮件发送成功")
        except smtplib.SMTPException:
            print("Error: 无法发送邮件")

    def _get_logger(self, filename, level='info'):
        # 创建日志对象
        log = logging.getLogger(filename)
        # 设置日志级别
        log.setLevel(level_relations.get(level))
        # 日志输出格式
        fmt = logging.Formatter('%(asctime)s %(thread)d %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
        # 输出到控制台
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setFormatter(fmt)
        # 输出到文件
        # 日志文件按天进行保存,每天一个日志文件
        file_handler = handlers.TimedRotatingFileHandler(filename=filename, when='D', backupCount=1, encoding='utf-8')
        # 按照大小自动分割日志文件,一旦达到指定的大小重新生成文件
        # file_handler = handlers.RotatingFileHandler(filename=filename, maxBytes=1*1024*1024*1024, backupCount=1, encoding='utf-8')
        file_handler.setFormatter(fmt)

        log.addHandler(console_handler)
        log.addHandler(file_handler)
        return log

    def LOGI(self, logstr):
        self.logger.info(logstr)

if __name__ == '__main__':
    # 初始化参数
    current_timestamp = 0       #记录当前时间
    # TokenTime = 0               #记录获取token的时间
    AskTime = 0                 #记录请求货源时间(5分钟)

    # 实例化类
    # current_timestamp = time.time()
    TeslaMessage = TeslaAutoSendMessage()
    # TokenTime = current_timestamp

    while True:
        current_timestamp = time.time()
        if AskTime == 0 or (current_timestamp - AskTime) >= 60:
            TeslaMessage.LOGI('程序没死,1分钟了,开始工作了!')
            result = TeslaMessage.Tesla()
            bTesla = TeslaMessage.parseJson(result)
            if bTesla != 0:
                TeslaMessage.LOGI('卧槽,又货了,赶紧发邮件提醒!')
                TeslaMessage.sendemail(bTesla)
            AskTime = current_timestamp
            TimeStr = '当前请求时间戳{}'.format(AskTime)
            TeslaMessage.LOGI(TimeStr)

        sleep(1)

七:结果

经过今天对存在一些问题得优化,终于今天成功收到提示,买到了。
在这里插入图片描述

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值