爬虫补环境案例---问财网(rpc,jsdom,代理,selenium)

news2024/12/22 15:17:52

目录

一.环境检测

1. 什么是环境检测

2.案例讲解

二 .吐环境脚本

1. 简介

2. 基础使用方法

3.数据返回

4. 完整代理使用

5. 代理封装

6. 封装所有使用方法

jsdom补环境

1. 环境安装

2. 基本使用

3. 添加参数形式

Selenium补环境

1. 简介

2.实战案例

1. 逆向目标

2. 实现代码

3.实现接口

1.实际使用

RPC

1. RPC 简介

补环境案例

找加密位置

开始补环境

方法1:传统补

方法二-jsdom

方法三----利用selenium补环境

方法四:rpc

一.环境检测

1. 什么是环境检测
  • 由于浏览器和node的差别,会导致浏览器的js代码在node没有办法执行,js代码会根据浏览器的这些属性来判断你是不是在真正的浏览器执行的代码,要不是正确的浏览器环境则不会返回正确的数据信息.
  • 拿到代码在node里面执行、经常看到这一类型的错误,提示xxx未定义,其实这一块就是浏览器对象的一些特征
 if (navigator['userAgent']){
    ^
​
ReferenceError: navigator is not defined
2.案例讲解
  • 检测执行代码是否存在navigator, 可以通过补空的方式

navigator = {}
navigator.userAgent = '11111'
​
function ps(){
    if (navigator['userAgent']){
        return 'hello world'
    } else {
        return  '失败'
    }
}
​
console.log(ps());
 
  • 检测属性长度,会根据长度来判断你的数据是否正确,是不是一个空数据

location = {}
location.href = '123123'
function ps(){
    if (location['href'].length > 3){
        return 'hello world'
    } else {
        return  '失败'
    }
}
​
console.log(ps());
  • js异常代码捕获,很多情况下可能js代码会把异常给捕获掉导致我们结果不对
  • 可以输出异常捕获的内容, 或者可以直接把异常捕获的代码直接删除,把错误暴露出来
location = {}
location.host = '12334'
navigator = {}
navigator.userAgent = '1231234'
function pn() {
    // try {
        verify_local()
        if (navigator['userAgent']) {
            return 'hello world'
        }
    // } catch (e) {
    //     console.log(e)
    //     return '错误的数据'
    // }
}
​
function verify_local() {
    if (location.host.length > 2) {
        return 'xxx'
    }
}
​
console.log(pn());
  • 浏览器和node环境差异
  • 在 Node.js 中,exports 是一个用于导出模块中的函数、对象、变量等的对象。
  • 浏览器是undefined
  • 可以删除, 或者可以修改的判断成功
// 浏览器和 node差异
sss = "undefined" != typeof exports ? exports : void 0
console.log(typeof sss);
  • global检测
glb= "undefined" == typeof window ? global:window

二 .吐环境脚本

1. 简介

Proxy可以理解为,在目标对象之前设一层"拦截",外界对该对象的访问,都必须通过这层拦截,可以对外界的访问进行过滤和改写(表示可以用它"代理"某些操作,可以翻为“代理器")。

api地址: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Prox

Proxy对象由两个部分组成:target、handler

target:目标对象handler:是一个对象,声明了代理target的指定行为,支持的拦截操作,一共13种:

  • get(target,propKey,receiver):拦截对象属性的读取。
    • target: 目标对象
    • propKey: 被获取的属性名。
    • receiver: Proxy 或者继承 Proxy 的对象
  • set(target,propKey,value,receiver):拦截对象属性的设置,返回一个布尔值(修改成功)。
    • target: 目标对象
    • propKey : 被获取的属性名。
    • value: 新属性值。
    • receiver: Proxy 或者继承 Proxy 的对象

一般的补环境的是通过运行程序后的undefined报错去一点一点分析,一点一点的去补一些环境,是非常掉头发的。

所以我们使用 Proxy 对全局遍历window、document、navigator等常见环境检测点进行代理,拦截代理对象的读取、函数调用等操作,并通过控制台输出,这样的话我们就能够实现检测环境自吐的功能,后续我们再针对吐出来的环境统一的进行补环境,这样就会方便的多。

2. 基础使用方法
var target = {
    name: 'JACK',
    age: 18,
};
​
var p = new Proxy(target, {
​
    get: function (target, propertyKey, receiver) {
        // 1 原对象
        // 2 访问属性
        // 3 代理器处理对象
        console.log(target, propertyKey, receiver)
    },
    set: function(target,propertyKey,value,receiver){
        // 1. 原对象
        // 2. 设置的属性
        // 3. 设置的值
        // 4. 代理器代理的对象
        console.log(target, propertyKey, value, receiver)
    }
})
p.age
p.user = 'aa'

注意:

我们现在写的代码,已经能够去拦截到取值和设置的操作,但是这个代码会打乱代码的后续运行,还并没有把对应的操作作用在原对象上,怎么解决呢?

3.数据返回

Reflect.get(target, propertyKey, receiver); //查找并返回target对象的name属性,receiver绑定this
Reflect.set(target, propertyKey, value, receiver); //设置target对象的name属性等于value

Reflect.set(target, name, value, receiver) 是 JavaScript 中的 Reflect 对象的一个方法。它用于设置指定对象的属性值,并返回一个布尔值,表示设置是否成功。

参数的含义如下:

  • target:要设置属性值的目标对象。
  • propertyKey:要设置的属性名。
  • value:要设置的属性值。
  • receiver(可选):设置属性时绑定的 this 值。

4. 完整代理使用


var target = {
    name: 'JACK',
    age: 18,
};
​
var p = new Proxy(target, {
​
    get: function (target, propertyKey, receiver) {
        temp = Reflect.get(target, propertyKey, receiver); //查找并返回target对象的name属性,receiver绑定this
        // 1 原对象
        // 2 访问属性
        // 3 代理器处理对象
        // console.log(target, propertyKey, receiver)
        console.log(`对象${target}--> get了属性--> ${propertyKey} 值是--> ${temp}`);
        return temp
    },
    set: function(target,p,value,receiver){
        temp = Reflect.set(target, p, value, receiver);
        // 1. 原对象
        // 2. 设置的属性
        // 3. 设置的值
        // 4. 代理器代理的对象
        // console.log(target, propertyKey, value, receiver)
        console.log("set: ", target, p, target[p]);
        return temp
    }
})
// console.log(p.age);
p.user = 'aa'
console.log(p.user)
​
 

5. 代理封装

// 目标对象(被代理对象)
var target = {
    name: 'JACK',
    age: 18,
    lili:{
        zs:'nana'
        
    }
};
​
function XlProxy(obj,name){
    return new Proxy(obj,{
        get(target, p, receiver) {
            temp = Reflect.get(target,p,receiver)
            console.log(`对象${name}--> get了属性--> ${p} 值是--> ${temp}`);
            if (typeof temp == 'object'){
                 // 对于对象套对象进行挂代理
                 temp = XlProxy(temp,name + '-->' + p)
            }
            return temp
        }
    })
}
sss = XlProxy(target,'target')
sss.name
sss.lili.zs
6. 封装所有使用方法


// 代理器封装
function get_enviroment(proxy_array) {
    for(var i=0; i<proxy_array.length; i++){
        handler = '{\n' +
            '    get: function(target, property, receiver) {\n' +
            '        console.log("方法:", "get  ", "对象:", ' +
            '"' + proxy_array[i] + '" ,' +
            '"  属性:", property, ' +
            '"  属性类型:", ' + 'typeof property, ' +
            // '"  属性值:", ' + 'target[property], ' +
            '"  属性值类型:", typeof target[property]);\n' +
            '        return target[property];\n' +
            '    },\n' +
            '    set: function(target, property, value, receiver) {\n' +
            '        console.log("方法:", "set  ", "对象:", ' +
            '"' + proxy_array[i] + '" ,' +
            '"  属性:", property, ' +
            '"  属性类型:", ' + 'typeof property, ' +
            // '"  属性值:", ' + 'target[property], ' +
            '"  属性值类型:", typeof target[property]);\n' +
            '        return Reflect.set(...arguments);\n' +
            '    }\n' +
            '}'
        eval('try{\n' + proxy_array[i] + ';\n'
        + proxy_array[i] + '=new Proxy(' + proxy_array[i] + ', ' + handler + ')}catch (e) {\n' + proxy_array[i] + '={};\n'
        + proxy_array[i] + '=new Proxy(' + proxy_array[i] + ', ' + handler + ')}')
    }
}
proxy_array = ['window', 'document', 'location', 'navigator', 'history','screen']
get_enviroment(proxy_array)
 

jsdom补环境

参考地址:Create new page · jsdom/jsdom Wiki · GitHub中文文档

jsdom是一个纯粹由 javascript 实现的一系列 web标准,特别是 WHATWG 组织制定的DOM和 HTML 标准,用于在nodejs中使用。大体上来说,该项目的目标是模拟足够的Web浏览器子集,以便用于测试和挖掘真实世界的Web应用程序

1. 环境安装
npm install jsdom --save
2. 基本使用
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
title = dom.window.document.querySelector("p").textContent
console.log(title)
3. 添加参数形式
const dom = new JSDOM(``, {
  url: "http://q.10jqka.com.cn/",
  referrer: "http://q.10jqka.com.cn/",
  contentType: "text/html",
  includeNodeLocations: true,
  storageQuota: 10000000
});

Selenium补环境

1. 简介

Selenium就是一个真实的环境地址,对于我们拿下来的js代码,在node是需要补环境的,但是在浏览器去执行的话,他就是一个真实的浏览器环境,所以可以节省我们扣代码的时间,我们可以把扣下来的代码直接用Selenium来进行访问

2.实战案例
1. 逆向目标
  • 网址:A股市场_同花顺行情中心_同花顺财经网
  • 参数:cookie :v
2. 实现代码
  • 我们把同花顺的js代码直接放到html文件
  • python代码
import os
from selenium import webdriver
​
PRO_DIR = os.path.dirname(os.path.abspath(__file__))
def driver_sig(html_file):
    option = webdriver.ChromeOptions()
    option.add_argument('--disable-blink-features=AutomationControlled')
    option.add_argument('headless')
    driver = webdriver.Chrome(options=option)
    driver.get(PRO_DIR +'\\'+ html_file)
    # time.sleep(2)
    # sig = driver.execute_script('return window.aaa()')
    # print(sig)
    return driver
​
html_file = 'index.html'
driv = driver_sig(html_file)
​
print(driv.execute_script('return window.aaa()'))
​

3.实现接口

pip install flask
from flask import Flask
​
# 创建 Flask 应用实例
app = Flask(__name__)
​
# 定义路由和视图函数
@app.route('/')
def hello():
    return 'Hello, Flask!'
​
# 启动应用
if __name__ == '__main__':
    app.run()
  • Flask 是一个基于 Python 的轻量级、简单易用的 Web 应用框架。它提供了一个灵活且容易扩展的方式来构建 Web 应用程序。以下是一个简单的示例展示了如何使用 Flask 框架创建一个简易的 Web 应用:
  • 在上述示例中,我们首先导入了 Flask 模块,并创建了一个 Flask 应用实例 app。然后,使用 @app.route() 装饰器定义了一个路由以及对应的视图函数。在本例中,根路由 '/' 对应的视图函数是 hello(),它返回了一个简单的字符串 'Hello, Flask!'。最后,通过调用 app.run() 来启动应用。
  • 要运行这个应用,你需要确保已经安装了 Flask 模块。运行应用后,在浏览器中访问 http://localhost:5000/,你将看到输出的 'Hello, Flask!'。
1.实际使用
# -*- coding: utf-8 -*-
​
from flask import Flask, request
​
from selenium import webdriver
import os
from selenium.webdriver.common.by import By
# pip install flask
from flask import Flask, jsonify
​
PRO_DIR = os.path.dirname(os.path.abspath(__file__))
​
​
def driver_sig(html_file):
    option = webdriver.ChromeOptions()
    option.add_argument('--disable-blink-features=AutomationControlled')
    option.add_argument('headless')
    driver = webdriver.Chrome(options=option)
    driver.get(PRO_DIR + '\\' + html_file)
    # time.sleep(2)
    # sig = driver.execute_script('return window.aaa()')
    # print(sig)
    return driver
​
​
html_file = 'index.html'
driv = driver_sig(html_file)
​
# 创建 Flask 应用实例
app = Flask(__name__)
​
​
# 定义路由和视图函数
@app.route('/s', methods=['get', 'post'])
def hello():
    context = {
        # 加载本地地址 生成cookie值
        'v': driv.execute_script('return window.aaa()')
    }
    # 返回cookie值
    return jsonify(context=context)
​
​
​
# 启动应用
if __name__ == '__main__':
    app.run()
​

RPC

1. RPC 简介

为什么要使用RPC技术呢?我们在使用websocket时候可以发现,python在操作的时候,需要创建连接,还需要不断去接受传递数据,非常的麻烦, 那这个时候rpc技术可以帮助到我们,简单来说就是网页直接和rpc服务器进行交互,我们python可以直接调用,rpc暴露的接口,不需要关心,创建连接这一块的问题.

RPC 技术是非常复杂的,对于我们搞爬虫、逆向的来说,不需要完全了解,只需要知道这项技术如何在逆向中应用就行了。

RPC 在逆向中,简单来说就是将本地和浏览器,看做是服务端和客户端,二者之间通过 WebSocket 协议进行 RPC 通信,在浏览器中将加密函数暴露出来,在本地直接调用浏览器中对应的加密函数,从而得到加密结果,不必去在意函数具体的执行逻辑,也省去了扣代码、补环境等操作,可以省去大量的逆向调试时间。

补环境案例

案例站:688262.SH - 同花顺问财

接口:

找加密位置

xhr断点发现段不住,原因就是他不是ajax发包的

请求堆栈里面断点

这个地方的接口是公用的

往上多看几个站,发现是在这里弄的

但是发现这里也是公用的多过几个

这个时候并没有加密,逐行运行

f10一次之后发现就加密了

所以是 document.getElementsByTagName("head")[0].appendChild(d),加密的

f11进去

r是script标签

所以加密的方法就在

L方法里面

把整个文件扣下来

因为他是动态的,所以我们请求这个文件,在改代码

网站的调用方式是

 var d = document.createElement("script");
                    e.cache || n.cache ? d.setAttribute("src", "" + o) : (o += -1 === o.indexOf("?") ? "?" : "&",
                    d.setAttribute("src", "" + o + s + "=" + c)),
                    e.charset && d.setAttribute("charset", e.charset),
                    d.id = f,
                    document.getElementsByTagName("head")[0].appendChild(d),

我们也这么干

 var d = document.createElement("script");
 // # e.cache || n.cache ? d.setAttribute("src", "" + o) : (o += -1 === o.indexOf("?") ? "?" : "&",
  d.setAttribute("src","https://d.10jqka.com.cn/v6/line/17_688262/01/today.jscallback=quotebridge_v6_line_17_688262_01_today"),
  // #e.charset && d.setAttribute("charset", e.charset),
  d.id ="callback_quotebridge_v6_line_17_688262_01_today",
  document.getElementsByTagName("head")[0].appendChild(d)

那我们在浏览器运行成功了,下一步放在node.js里面

运行

开始补环境

方法1:传统补

对比发现真实的环境中q是ture,所以这里逻辑有问题,找到赋值的位置

方法二-jsdom

我们在开头加入

const jsdom = require("jsdom");
const {JSDOM} = jsdom;
var dom = new JSDOM('<!DOCTYPE html><p>hello world</p>')
window = dom.window
document = dom.window.document
Document=dom.window.Document
// console.log(dom.window.document)
navigator = dom.window.navigator
Element=dom.window.Element
// Element =function Element(){}
Element.prototype.insertBefore=function insertBefore(d){
    window.perry=d.src

}

location=dom.window.location
location.href='https://www.iwencai.com/unifiedwap/result?w=688262.SH%20&querytype=stock'
location.hostname='www.iwencai.com'
location.host='www.iwencai.com'
location.protocol='http:'

navigator.userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'

Document.prototype.getElementsByTagName=function getElementsByTagName(){
    return [{
        appendChild:function (d){
            Element.prototype.insertBefore(d)

        }
    }]
}

运行

发现成功,补完了

方法三----利用selenium补环境

运行

也成功了

方法四:rpc

利用rpc技术更快捷,具体使用在我其他blog里面,具体看https://articles.zsxq.com/id_cxu1hc2vub8x.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2240741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DICOM图像解析:深入解析DICOM格式文件的高效读取与处理(续)

目录 一、DICOM图像高效解析 1、处理压缩的像素数据 常见压缩算法及其处理方法 解压缩示例 2、多帧图像的处理 多帧图像解析流程 三维图像的体绘制 3、序列和嵌套数据元素 序列数据的解析 二、错误处理与数据验证 常见错误类型 错误处理策略 三、使用现有的DICOM库…

「QT」高阶篇 之 d-指针 的用法

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「QT」QT5程序设计&#x1f4da;全部专栏「Win」Windows程序设计「IDE」集成开发环境「UG/NX」BlockUI集合「C/C」C/C程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「UG/NX」NX定制…

如何绑定洛谷账号

首先注册洛谷 然后登陆 点击键盘F12 点击加号 点击应用程序 在name中找到__client_id和_uid 再复制相应的value到下图右侧 然后点击confirm即可 愿我们都能成为我们想要去成为的人&#xff01; 花会沿路盛开&#xff0c;我们以后的路也会&#xff01; 追风赶月莫停留&…

5G CPE:为什么活动会场与商铺的网络成为最新选择

在快节奏的现代社会中&#xff0c;无论是举办一场盛大的活动还是经营一家繁忙的商铺&#xff0c;稳定的网络连接都是不可或缺的基石。然而&#xff0c;面对复杂的布线难题或高昂的商业宽带费用&#xff0c;许多场所往往陷入两难境地。幸运的是&#xff0c;5G CPE&#xff08;Cu…

【ACM独立出版|高校主办】第四届信号处理与通信技术国际学术会议(SPCT 2024)

第四届信号处理与通信技术国际学术会议&#xff08;SPCT 2024&#xff09; 2024 4th International Conference on Signal Processing and Communication Technology 2024年12月27-29日 中国深圳 www.icspct.com 会议亮点&#xff1a; 1、ACM独立出版&#xff0c;EI稳…

CPU的性能指标总结(学习笔记)

CPU 性能指标 我们先来回顾下&#xff0c;描述 CPU 的性能指标都有哪些。 首先&#xff0c;最容易想到的应该是 CPU 使用率&#xff0c;这也是实际环境中最常见的一个性能指标。 用户 CPU 使用率&#xff0c;包括用户态 CPU 使用率&#xff08;user&#xff09;和低优先级用…

python 同时控制多部手机

在这个智能时代,我们的手机早已成为生活和工作中不可或缺的工具。无论是管理多个社交媒体账号,还是处理多台设备上的事务,如何更高效地控制多个手机成为了每个人的痛点。 今天带来的这个的软件为你提供了一键控制多部手机的强大功能。无论是办公、娱乐,还是社交,你都能通过…

实用教程:如何无损修改MP4视频时长

如何在UltraEdit中搜索MP4文件中的“mvhd”关键字 引言 在视频编辑和分析领域&#xff0c;有时我们需要深入到视频文件的底层结构中去。UltraEdit&#xff08;UE&#xff09;和UEStudio作为强大的文本编辑器&#xff0c;允许我们以十六进制模式打开和搜索MP4文件。本文将指导…

java项目-jenkins任务的创建和执行

参考内容: jenkins的安装部署以及全局配置 1.编译任务的general 2.源码管理 3.构建里编译打包然后copy复制jar包到运行服务器的路径 clean install -DskipTests -Pdev 中的-Pdev这个参数用于激活 Maven 项目中的特定构建配置&#xff08;Profile&#xff09; 在 pom.xml 文件…

【扩散——BFS】

题目 代码 #include <bits/stdc.h> using namespace std; const int t 2020, off 2020; #define x first #define y second typedef pair<int, int> PII; int dx[] {0, 0, 1, -1}, dy[] {-1, 1, 0, 0}; int dist[6080][6080]; // 0映射到2020&#xff0c;2020…

C++编程:利用环形缓冲区优化 TCP 发送流程,避免 Short Write 问题

文章目录 1. 什么是 Short Write 问题&#xff1f;2. 如何解决 Short Write 问题&#xff1f;2.1 方法 1&#xff1a;将 Socket 设置为阻塞模式2.2 方法 2&#xff1a;用户态维护发送缓冲区 3. 用户态维护发送缓冲区实现3.1 核心要点3.2 代码实现3.3 测试程序 参考文档 1. 什么…

数据网格能替代数据仓库吗?

一、数据网格是什么&#xff1f; 数据网格&#xff1a;是一种新兴的数据管理架构和理念&#xff0c;主要用于解决大规模、复杂数据环境下的数据管理和利用问题。 核心概念&#xff1a; 1、数据即产品&#xff1a;将数据看作一种产品&#xff0c;每个数据域都要对其生产的数据负…

力扣经典面试26题删除有序数组中的重复项1

给你一个非严格递增排列的数组nums&#xff0c;请你原地删除重复出现的元素&#xff0c; 使每个元素 只出现一次&#xff0c;返回删除后数组的新长度。元素的相对顺序 应该保持 一致。然后返回 nums 中唯一元素的个数。 考虑 nums 的唯一元素的数量为 k&#xff0c; 你需要做以…

LLM: AI Mathematical Olympiad (上)

文章目录 一、项目简介二、first place 攻略三、必备知识1、COT思维链技术2、ToRA 四、first place 训练功略五、数据集构建1、COT数据集2、TIR数据集 六、数据集详细技术报告总结 本文较长分成两个部分分析 | ू•ૅω•́)ᵎᵎᵎ 第一部分&#xff1a;预备知识介绍和数据准备…

GA/T1400视图库平台EasyCVR视频融合平台HLS视频协议是什么?

在数字化时代&#xff0c;视频监控系统已成为保障安全、提升效率的关键技术。EasyCVR视频融合云平台&#xff0c;作为TSINGSEE青犀视频在“云边端”架构体系中的重要一环&#xff0c;专为大中型项目设计&#xff0c;提供了一个跨区域、网络化的视频监控综合管理系统平台。它不仅…

给阿里云OSS绑定域名并启用SSL

为什么要这么做&#xff1f; 问题描述&#xff1a; 当用户通过 OSS 域名访问文件时&#xff0c;OSS 会在响应头中增加 Content-Disposition: attachment 和 x-oss-force-download: true&#xff0c;导致文件被强制下载而不是预览。这个问题特别影响在 2022/10/09 之后新开通 OS…

`node-gyp` 无法找到版本为 `10.0.19041.0` 的 Windows SDK

从你提供的错误信息来看&#xff0c;问题出在 node-gyp 无法找到版本为 10.0.19041.0 的 Windows SDK。我们可以尝试以下几种方法来解决这个问题&#xff1a; 完整示例 方法 1&#xff1a;安装指定版本的 Windows SDK 下载并安装 Windows SDK&#xff1a; 访问 Windows SDK 下…

【Hive】【HiveQL】【大数据技术基础】 实验四 HBase shell命令实验

实验四&#xff1a;熟悉常用的HBase操作 实验概览 在本次实验中&#xff0c;我们将深入探索HBase在Hadoop生态系统中的角色&#xff0c;并熟练掌握常用的HBase Shell命令和Java API操作。通过这些实践&#xff0c;我们能够更好地理解HBase的工作原理以及如何在实际项目中应用。…

3D意识(3D Awareness)浅析

一、简介 3D意识&#xff08;3D Awareness&#xff09;主要是指视觉基础模型&#xff08;visual foundation models&#xff09;对于3D结构的意识或感知能力&#xff0c;即这些模型在处理2D图像时是否能够理解和表示出图像中物体或场景的3D结构&#xff0c;其具体体现在编码场景…

快递面单批量导入打印软件小程序下载 佳易王网店快递面单批量打印管理系统操作教程

一、概述 【软件文件资源在文章最后】 快递面单批量导入打印软件小程序下载 快递面单批量打印管理系统操作教程 直接使用快递空白单打印&#xff0c;可以扫描条码并可以查询快递信息&#xff0c;面单内容可以自定义。 可以批量导入批量打印&#xff0c;从而提高效率节省时间…