Python爬虫/SAP-SRM数据采集

一、目标分析

1.1 目标系统

系统版本:SAP系统NetWeaver。SRM主要功能如下图,其中需求预测、采购执行监控、寄售库存监控是业务计划有关的数据,使用频率最高。

​数据采集范围

​SAP/SRM系统界面

1.2 业务痛点

对于使用SRM的供应商来说,他们频繁登录SRM系统多有不便,SRM数据无法与自己公司信息系统对接,导致业务沟通不畅。

业务痛点分析

1.3 业务诉求

对于供应商来说,希望采集SAP-SRM数据,存入数据库,建立业务模型,实现客户计划、生产计划、库存占用之间的数据联动,从而提高效率,降低成本。

​数据方面的诉求

1.4 采集方法

大家问:为何要采用模拟采集?

工程师答:经过我们测试分析,该系统对应页面数据结构复杂,采用数据动态加载,普通采集工具只能采集10条,即便是滚动鼠标循环获取分页也无法采集全部数据,且数据较多时分页获取数据速度非常慢。

方案定制:需求的数据可以通过调整业务参数,点击查询加载数据,数据加载后,导出Excel即可获取原始数据。

二、程序功能

​爬虫定制程序5大功能

网络爬虫,数据采集程序,不仅仅是采集数据,还要实现数据清洗、数据加工,数据对比分析,数据存储。数据采集程序可以按需运行,也可以按指定频率运行。新增业务数据可以通过企业微信进行提醒。

三、程序框架

3.1 工程目录

数采程序工程目录

1、文件夹P10-P50:用于存放采集程序工作期间产生的文件;P90-logs:用于存放程序工作期间运行日志

2、DataSyn_xxx.py采集程序入口文件,在此文件中定义了数据处理需要经历的步骤(后面附有详细代码)

3、Logger.py程序日志模块

4、企业wx_xxx.py,qywx模块,用于发送消息提醒和文件附件

5、数据库ORM_xxx.py,对象实体映射,用于保存数据到数据库

6、文件P10-P50,数据处理模块,用于数据各阶段的分段处理,会被主程序DataSyn_xxx.py调用执行,从而完成各种功能

7、sysconfig.ini,存放WEB/DB的配置信息,服务器地址、用户名、密码

3.2 程序代码

import os
import time
from P10数据采集_cnhtc import GetWebData
from P20数据清洗_cnhtc import DataCleaning
from P30差异分析_cnhtc import DataDis
from P50业务提醒_cnhtc import SendMsg
from 企业微信_cnhtc import wx
from Logger import Logging
from SpiderManager.AppList import *


class cnhtcerp:

    def __init__(self, msg_url=None):
        # 程序名称
        appCode = os.path.split(os.path.abspath(__file__))[0].split("\\")[-1]  # 上级目录名称
        self.AppName = applist[appCode]
        self.logger = Logging().log(level='INFO')
        if not (app_run_start_time <= time.strftime("%H:%M", time.localtime()) <= app_run_end_time):
            self.logger.warning(
                "程序【{}】不在预设运行时段[{}]-[{}],程序结束!\n\n\n".format(self.AppName, app_run_start_time, app_run_end_time))
            return
        self.logger.info("程序:{},开始运行 ... ".format(self.AppName))
        # 各阶段文件夹
        self.msg_url = msg_url
        self.DirName = {'P10': "P10-源数据", 'P20': "P20-数据清洗",
                        'P30': "P30-差异分析", 'P40': "P40-数据存储", 'P50': "P50-更新提醒"}
        self.curPath = os.getcwd()
        self.logger.info("当前程序工作目录:{}".format(self.curPath))
        s = wx()
        try:
            self.__RunApp()
        except Exception as err:
            err_msg = "程序出错提示!\n[系统名称:{}]:\n{}".format(self.AppName, err)
            self.logger.info(err_msg)
            s.send_text(err_msg, "MaLaoShi")
            self.logger.error("程序执行遇到错误!程序退出!\n\n\n")
            exit()

    def __RunApp(self):
        self.__检查文件目录()
        self.__数据采集()
        self.__数据清洗()
        self.__差异分析()
        self.__业务提醒()
        self.logger.info("程序结束\n{}\n\n\n".format("=" * 90))

    def __检查文件目录(self):
        for x in self.DirName.values():
            if not (os.path.exists(os.path.join(x))):
                os.mkdir(os.path.join(x))
                self.logger.info("创建{}目录!".format(x))

    def __数据采集(self):
        AppName = "__数据采集"
        FilePath = self.DirName['P10']
        Dir = os.path.join(self.curPath, FilePath)
        self.logger.info("程序:{},开始运行 ... ".format(AppName))
        self.logger.info("程序工作文件夹:{}".format(Dir))
        GetWebData(Dir)

    def __数据清洗(self):
        AppName = "__数据清洗"
        FilePath1 = self.DirName['P10']  # 源数据文件路径
        FilePath2 = self.DirName['P20']  # 目标数据文件路径
        Dir1 = os.path.join(self.curPath, FilePath1)
        Dir2 = os.path.join(self.curPath, FilePath2)
        self.logger.info("程序:{},开始运行 ... ".format(AppName))
        self.logger.info("源数据文件路径:{}".format(Dir1))
        self.logger.info("目标据文件路径:{}".format(Dir2))
        DataCleaning(Dir1, Dir2)

    def __差异分析(self):
        AppName = "__差异分析"
        FilePath1 = self.DirName['P20']  # 源数据文件路径
        FilePath2 = self.DirName['P30']  # 分析数据文件路径
        Dir1 = os.path.join(self.curPath, FilePath1)
        Dir2 = os.path.join(self.curPath, FilePath2)
        self.logger.info("程序:{},开始运行 ... ".format(AppName))
        self.logger.info("源数据文件路径:{}".format(Dir1))
        self.logger.info("分析数据文件路径:{}".format(Dir2))
        DataDis(Dir1, Dir2)

    def __业务提醒(self):
        AppName = "__业务提醒"
        FilePath1 = self.DirName['P30']  # 源数据文件路径
        FilePath2 = self.DirName['P50']  # 分析数据文件路径
        Dir1 = os.path.join(self.curPath, FilePath1)
        Dir2 = os.path.join(self.curPath, FilePath2)
        self.logger.info("程序:{},开始运行 ... ".format(AppName))
        self.logger.info("源数据文件路径:{}".format(Dir1))
        self.logger.info("目标数据文件路径:{}".format(Dir2))
        if self.msg_url is not None:
            qywx_url = self.msg_url
        else:
            qywx_url = 'qywxURL'

        # 按时段发送消息提醒
        if msg_run_start_time <= time.strftime("%H:%M", time.localtime()) <= msg_run_end_time:
            self.logger.info("提醒时段,发送消息!")
            s = SendMsg(Dir1, Dir2)
            s.send_qywx(qywx_url)
        else:
            self.logger.info("非提醒时段,不发送消息!")


if __name__ == "__main__":
    msg_url = 'qywxURL'
    cnhtcerp()

3.3 代码解析

1、DataSyn的核心意义就是数据采集、数据同步。在这个模块中调用了其他功能模块,分块实现了爬虫程序的各个功能,各个模块既独立又相互衔接

2、【__init__】方法:

(1)判断当前时间是否在允许的运行时间内,如果在允许运行时段内继续运行程序,否则不在运行。

(2)调用了【__RunApp】方法进行数据处理。同时,进行了错误捕捉,程序执行错误会通过企业微信通知管理人员。管理人员结合报错提示信息、报错时间点、程序工作日志,分析报错原因,改进采集程序。

3、【__RunApp】方法

将调用所有的数据处理程序

4、【__检查文件目录】方法

检查程序运行需要的文件夹是否存在,不存在就创建

5、【__数据采集】方法

调用数据采集程序,保存原始数据到指定目录(P10文件夹)

6、【__数据清洗】方法

调用数据清洗程序,从P10文件夹获取数据,数据清洗、数据加工完毕后存入P20文件夹

7、【__差异分析】方法

调用数据差异分析程序,从P20文件夹获取数据、从data in DB中获取数据,两方面数据对比分析出差异,将采集数据每一条记录进行标记(新增、修改、作废、不修改)。差异分析完毕后调用数据存储程序进行数据保存。

8、【__业务提醒】方法

调用提醒程序,发送qywx文字消息和文件。

四、数据采集

4.1 程序代码

import os
import time
import datetime
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium.webdriver.chrome.options import Options
import json
from Logger import Logging
import shutil


class GetWebData:
    def __init__(self, Dir):
        self.Dir = Dir
        self.logger = Logging().log(level="INFO")
        self.__reset_fileDir()
        self.curPath = os.getcwd()
        self.is_headless = 0
        try_times = 1
        while True:
            self.logger.info("进行第 {} 次数据采集 ... ".format(try_times))
            files_count = self.__RunApp()
            if files_count == 3:
                self.logger.info("采集数据成功!程序继续...")
                return
            else:
                self.logger.info("采集数据失败!")
                try_times += 1
                time.sleep(30)
                continue
            if try_times > 3:
                raise "数据采集失败,超过3次!"

    def __reset_fileDir(self):
        dl_path = self.Dir
        self.logger.info("检查文件夹状态 ... ")
        if os.path.exists(dl_path):
            self.logger.info("存在文件夹!")
            files = os.listdir(dl_path)
            file_count = len(files)
            if file_count > 0:
                self.logger.info("文件夹存在文件,文件数量:{}".format(file_count))
                shutil.rmtree(dl_path)
                self.logger.info("删除文件夹!")
                # 创建文件夹
                os.mkdir(dl_path)
                self.logger.info("创建文件夹!")
        else:
            # 创建文件夹
            os.mkdir(dl_path)
            self.logger.info("创建文件夹!")
        time.sleep(1)
        return True

    def __RunApp(self):
        self.logger.info("程序名称:{} ".format("get_web_data"))
        ts = time.time()
        with open(os.path.join(self.curPath, "sysconfig.ini"), "r") as f:
            info = json.loads(f.read())
            url = info['app_host']
            userName = info['app_user']
            password = info['app_pwd']
        # 设置options:文件下载路径
        options = Options()
        prefs = {
            'profile.default_content_settings.popups': 0,
            'download.default_directory': self.Dir,
        }
        options.add_experimental_option('prefs', prefs)
        if self.is_headless == 1:
            # 无窗口运行模式
            options.add_argument('--headless')
        self.logger.info("设置文件下载路径!")
        self.driver = webdriver.Chrome(options=options)  # 启动浏览器!
        self.driver.set_window_size(1466, 1000)
        # self.driver.maximize_window()  # 窗口最大化!

        self.driver.get(url)  # 输入网址!
        time.sleep(3)
        self.logger.info("输入用户名和密码 ... ")
        self.driver.find_element(By.ID, "sap-user").send_keys(userName)
        self.driver.find_element(By.ID, "sap-password").send_keys(password)
        self.driver.find_element(By.ID, "LOGON_BUTTON").click()
        self.logger.info("登录系统!")
        self.__cancel_other_login()  # 检测多余登录并处理
        self.__get_预测订单查询()
        self.__get_库存查询()
        self.__get_采购订单执行监控查询()
        self.__quit_system()
        te = time.time()
        self.logger.info("数据获取完成,耗时: {} s".format(round(te - ts)))
        files_count = len(os.listdir(self.Dir))
        return files_count

    def __cancel_other_login(self):
        self.logger.info("程序名称:{}".format("__cancel_other_login"))
        try:
            e = self.driver.find_element(By.ID, 'SESSION_QUERY_CONTINUE_BUTTON')
            ActionChains(self.driver).move_to_element(e).pause(0.1).perform()
            ActionChains(self.driver).click(e).perform()
            self.logger.warning("识别重复登录:selenium点击继续登录!")
        except Exception as err:
            self.logger.info("未发现重复登录:{}")

    def __get_预测订单查询(self):
        time.sleep(2)
        e1 = self.driver.find_element(By.LINK_TEXT, "需求预测")
        ActionChains(self.driver).move_to_element(e1).pause(0.1).perform()
        ActionChains(self.driver).click(e1).perform()
        self.logger.info("点击按钮,需求预测!")
        time.sleep(2)
        self.driver.switch_to.frame(0)
        self.logger.info("进入iFrame")
        time.sleep(2)
        date_60 = datetime.datetime.today() + datetime.timedelta(days=1) * 60
        date_60_str = date_60.strftime("%Y-%m-%d")
        e_date = self.driver.find_element(By.XPATH, '//*[@id="WD7D"]')
        e_date.clear()
        e_date.send_keys(date_60_str)
        self.logger.info("设置查询截止日期—60天后!")
        e2 = self.driver.find_element(By.ID, "WD85-caption")
        ActionChains(self.driver).move_to_element(e2).pause(0.1).perform()
        ActionChains(self.driver).click(e2).perform()
        self.logger.info("点击按钮,查询按钮!")
        time.sleep(2)
        e2 = self.driver.find_element(By.ID, "WDA9-cnt")
        ActionChains(self.driver).move_to_element(e2).pause(0.1).perform()
        ActionChains(self.driver).click(e2).perform()
        self.logger.info("点击按钮,导出按钮!")
        time.sleep(0.2)
        ActionChains(self.driver).move_by_offset(0, 20).click().perform()
        self.logger.info("点击按钮,导出Excel文件!")
        self.driver.switch_to.parent_frame()

    def __get_库存查询(self):
        time.sleep(2)
        try:
            e0 = self.driver.find_element(By.XPATH, "/html/body/div/div[2]/div/ul/li[1]")
            ActionChains(self.driver).move_to_element(e0).pause(0.1).perform()
            ActionChains(self.driver).click(e0).perform()
            self.logger.info("屏幕太小,订单菜单显示不全处理!")
        except Exception as err:
            self.logger.info("没有发现滚动条:{}".format(err))
        e1 = self.driver.find_element(By.LINK_TEXT, "寄售库存监控")
        ActionChains(self.driver).move_to_element(e1).pause(0.1).perform()
        ActionChains(self.driver).click(e1).perform()
        self.logger.info("点击按钮,需求预测!")
        time.sleep(2)
        self.driver.switch_to.frame(0)
        self.logger.info("进入iFrame")
        time.sleep(2)
        e2 = self.driver.find_element(By.ID, "WD76-caption")
        ActionChains(self.driver).move_to_element(e2).pause(0.1).perform()
        ActionChains(self.driver).click(e2).perform()
        self.logger.info("点击按钮,查询按钮!")
        time.sleep(3)
        e2 = self.driver.find_element(By.ID, "WD9A-cnt")
        ActionChains(self.driver).move_to_element(e2).pause(0.1).perform()
        ActionChains(self.driver).click(e2).perform()
        self.logger.info("点击按钮,导出按钮!")
        time.sleep(0.2)
        ActionChains(self.driver).move_by_offset(0, 20).click().perform()
        self.logger.info("点击按钮,导出Excel文件!")
        self.driver.switch_to.parent_frame()

    def __get_采购订单执行监控查询(self):
        time.sleep(2)
        try:
            e0 = self.driver.find_element(By.XPATH, "/html/body/div/div[2]/div/ul/li[1]")
            ActionChains(self.driver).move_to_element(e0).pause(0.1).perform()
            ActionChains(self.driver).click(e0).perform()
            self.logger.info("屏幕太小,订单菜单显示不全处理!")
        except Exception as err:
            self.logger.info("没有发现滚动条:{}".format(err))
        e1 = self.driver.find_element(By.LINK_TEXT, "采购执行监控")
        ActionChains(self.driver).move_to_element(e1).pause(0.1).perform()
        ActionChains(self.driver).click(e1).perform()
        self.logger.info("点击按钮,采购执行监控!")
        time.sleep(2)
        self.driver.switch_to.frame(0)
        time.sleep(2)
        self.logger.info("进入iFrame")
        date_x = datetime.datetime.today() - datetime.timedelta(days=1) * 30 * 3
        date_x_set = date_x.strftime("%Y-%m-%d")
        s_date = self.driver.find_element(By.XPATH, '//*[@id="WD53"]')
        s_date.clear()
        s_date.send_keys(date_x_set)
        e2 = self.driver.find_element(By.ID, "WD8F-cnt")
        ActionChains(self.driver).move_to_element(e2).pause(0.1).perform()
        ActionChains(self.driver).click(e2).perform()
        self.logger.info("点击按钮,查询按钮!")
        ts = time.time()
        while True:
            try:
                el_path = '/html/body/table/tbody/tr/td/div/div[1]/table/tbody/tr[2]/td/table/tbody/tr/td/div/div/table/tbody/tr[2]/td/div/table/tbody/tr/td/table/tbody/tr[1]/td/table/tbody/tr/td/div/div/span[2]'
                el_find = self.driver.find_element(By.XPATH, el_path)
                if '查询结果:' in el_find.text:
                    self.logger.info("查询完成,耗时:{} s".format(round(time.time() - ts, 2)))
                    break
            except:
                self.logger.info("查询等待 ... ")
                time.sleep(2)
            if time.time() - ts > 60:
                self.logger.warning("查询超时(60秒),退出程序!el_path:{}".format(el_path))
                break

        e2 = self.driver.find_element(By.ID, "WDB3-cnt")
        ActionChains(self.driver).move_to_element(e2).pause(0.1).perform()
        ActionChains(self.driver).click(e2).perform()
        self.logger.info("点击按钮,导出按钮!")
        ActionChains(self.driver).move_by_offset(0, 20).click().perform()
        self.logger.info("点击按钮,导出Excel文件!")
        self.driver.switch_to.parent_frame()
        time.sleep(4)

    def __quit_system(self):
        time.sleep(2)
        logoff = self.driver.find_element(By.ID, "logoff_link")
        ActionChains(self.driver).move_to_element(logoff).click().perform()
        self.logger.info("点击按钮,注销登录!")
        time.sleep(1)
        self.driver.switch_to.frame(1)
        self.logger.info("进入iFrame1")
        confirm = self.driver.find_element(By.XPATH, "/html/body/div[2]/div[5]/ul/li[1]/div[2]")
        ActionChains(self.driver).move_to_element(confirm).click().perform()
        self.logger.info("点击按钮,确定注销登录!")
        time.sleep(2)
        self.driver.quit()
        self.logger.info("关闭浏览器!")


if __name__ == "__main__":
    ts = time.time()
    Dir = 'C:\Python\pydoc\cnhtcerp\P10-源数据'
    s = GetWebData(Dir)
    te = time.time()
    print("执行完成!耗时:{} s".format(round(te - ts, 2)))

4.2 代码解析

1、【__init__】方法

在此方法中运行【__RunApp】方法,程序开始采集数据,采集成功后将原始数据保存到指定文件,通过判断采集文件个数来确定数据是否全部采集成功。如果未全部采集成功,将继续运行采集程序。运行3次后无法采集成功时,程序报错,通过qywx通知管理员。

2、【__reset_fileDir】方法,重设文件夹,主要是查看指定文件夹是否为空,不为空则清空。

3、【__RunApp】方法,在此方法中,使用Chrome浏览器,登录目标系统,采集数据,注销登录,关闭浏览器。

4、【__cancel_other_login】方法,检测是否存在重复登录,如果存在继续本次登录,注销其他登录。

5、【__get_预测订单查询】、【__get_库存查询】、【__get_采购订单执行监控查询】方法,用于采集具体页面数据。由于查询日期跨度大,数据量大,加载时间长,因网络不同而已,所以在【__get_采购订单执行监控查询】方法中检测页面数据是否加载完毕,加载完毕则可导出数据,否则继续等待页面加载完成,超过60秒未加载完成退出本方法。 6、【__quit_system】方法,注销登录,退出浏览器。

五、运行结果

5.1 采集数据同步到数据库

程序按预设频率定时运行,原始数据采集后,经过数据清洗,差异对比分析,最后将数据增量写入数据库中。

​采集数据存入数据库中

5.2 业务提醒

业务数据有更新时发送qywx提醒,让业务相关人员做到心中有数,业务开展,计划调整,胸有成竹。

业务提醒

5.3 业务模型建立

采集数据不是目的,如何最大化利用数据,发挥数据价值才是最终目的。本案例利用采集数据,建立库存当量(最低库存、最高库存),结合ERP存货现存量、采购在途量、生产订单在制量等数据,计算该存货在该时点的最小/最大可入库量。利用报表工具,将计划数据共享给生产、采购、营销等部门,指导生产、采购、营销协同工作,因为库存当量动态调整,所以库存占用趋于合理化,相比以前存货占用下降了30%,交付效率提高了20%。

5.4 视频演示

数据采集视频

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老马也是

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值