Bläddra i källkod

并行化设计

naibo 2 år sedan
förälder
incheckning
074b5e67d9
75 ändrade filer med 2040 tillägg och 870 borttagningar
  1. 5 5
      ElectronJS/main.js
  2. 5 5
      ElectronJS/src/taskGrid/invokeTask.html
  3. 2 2
      ElectronJS/src/taskGrid/taskList.html
  4. 0 0
      ElectronJS/tasks/76.json
  5. 0 0
      ElectronJS/tasks/77.json
  6. 0 0
      ElectronJS/tasks/78.json
  7. 0 0
      ElectronJS/tasks/79.json
  8. 0 0
      ElectronJS/tasks/80.json
  9. 1 1
      ExecuteStage/.vscode/launch.json
  10. 1 1
      ExecuteStage/config.json
  11. 840 808
      ExecuteStage/easyspider_executestage.py
  12. 1137 0
      ExecuteStage/easyspider_executestage_single.py
  13. 26 35
      Extension/manifest_v3/src/background.ts
  14. 2 0
      Readme.md
  15. 16 12
      Releases/EasySpider_windows_amd64/V0.3.1 新特性.txt
  16. 1 1
      Releases/EasySpider_windows_amd64/config.json
  17. 1 0
      Releases/EasySpider_windows_amd64/config2.json
  18. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/0.json
  19. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/1.json
  20. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/10.json
  21. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/11.json
  22. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/12.json
  23. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/13.json
  24. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/14.json
  25. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/15.json
  26. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/16.json
  27. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/17.json
  28. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/18.json
  29. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/19.json
  30. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/2.json
  31. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/20.json
  32. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/21.json
  33. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/22.json
  34. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/23.json
  35. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/24.json
  36. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/25.json
  37. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/26.json
  38. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/27.json
  39. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/28.json
  40. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/29.json
  41. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/3.json
  42. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/30.json
  43. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/31.json
  44. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/32.json
  45. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/33.json
  46. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/34.json
  47. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/35.json
  48. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/36.json
  49. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/37.json
  50. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/38.json
  51. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/39.json
  52. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/4.json
  53. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/40.json
  54. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/41.json
  55. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/42.json
  56. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/43.json
  57. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/44.json
  58. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/45.json
  59. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/46.json
  60. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/47.json
  61. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/48.json
  62. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/49.json
  63. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/5.json
  64. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/50.json
  65. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/51.json
  66. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/52.json
  67. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/6.json
  68. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/7.json
  69. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/8.json
  70. 0 0
      Releases/EasySpider_windows_amd64/execution_instances/9.json
  71. 3 0
      Releases/EasySpider_windows_amd64/readme.txt
  72. 0 0
      Releases/EasySpider_windows_amd64/tasks/4.json
  73. 0 0
      Releases/EasySpider_windows_amd64/tasks/5.json
  74. 0 0
      Releases/EasySpider_windows_amd64/tasks/6.json
  75. 0 0
      Releases/EasySpider_windows_amd64/tasks/7.json

+ 5 - 5
ElectronJS/main.js

@@ -194,20 +194,20 @@ async function beginInvoke(msg, ws) {
                 socket_window.send(msg.message.pipe);
                 console.log("FROM Flowchart: ", JSON.parse(msg.message.pipe));
             }
-        } catch {
-            dialog.showErrorBox("Error", "Please open the flowchart window first");
+        } catch (e) {
+            console.log(e);
         }
     } else if (msg.type == 5) {
-        var child = require('child_process').execFile;
+        let child = require('child_process').execFile;
         // 参数顺序: 1. task id 2. server address 3. saved_file_name 4. "remote" or "local" 5. user_data_folder
         // var parameters = [msg.message.id, server_address];
         let parameters = [];
         console.log(msg.message)
         if (msg.message.user_data_folder == null || msg.message.user_data_folder == undefined || msg.message.user_data_folder == "") {
-            parameters = ["--id", msg.message.id, "--server_address", server_address, "--user_data", 0];
+            parameters = ["--id", "[" + msg.message.id + "]", "--server_address", server_address, "--user_data", 0];
         } else {
             let user_data_folder_path = path.join(task_server.getDir(), msg.message.user_data_folder);
-            parameters = ["--id", msg.message.id, "--server_address", server_address, "--user_data", 1];
+            parameters = ["--id", "[" + msg.message.id + "]", "--server_address", server_address, "--user_data", 1];
             config.user_data_folder = msg.message.user_data_folder;
             config.absolute_user_data_folder = user_data_folder_path;
             fs.writeFileSync(path.join(task_server.getDir(), "config.json"), JSON.stringify(config));

+ 5 - 5
ElectronJS/src/taskGrid/invokeTask.html

@@ -279,7 +279,7 @@
                         if(getOperatingSystemInfo().version == 'macOS'){
                             // app.$data.command = "./easyspider_executestage --id " + result.toString() + " --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
                             changeCommand();
-                                $('#myModal').modal('show');
+                            $('#myModal').modal('show');
                         }
                     });
                 // }
@@ -291,13 +291,13 @@
     function changeCommand() {
         let OSInfo = getOperatingSystemInfo();
         if(OSInfo.version == 'win' && OSInfo.bit == 64){
-            app.$data.command = "./EasySpider/resources/app/chrome_win64/easyspider_executestage.exe --id " + app.$data.ID.toString() + " --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
+            app.$data.command = "./EasySpider/resources/app/chrome_win64/easyspider_executestage.exe --id [" + app.$data.ID.toString() + "] --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
         } else if(OSInfo.version == 'win' && OSInfo.bit == 32){
-            app.$data.command = "./EasySpider/resources/app/chrome_win32/easyspider_executestage.exe --id " + app.$data.ID.toString() + " --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
+            app.$data.command = "./EasySpider/resources/app/chrome_win32/easyspider_executestage.exe --id [" + app.$data.ID.toString() + "] --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
         } else if(OSInfo.version == 'linux'){
-            app.$data.command = "./EasySpider/resources/app/chrome_linux64/easyspider_executestage --id " + app.$data.ID.toString() + " --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
+            app.$data.command = "./EasySpider/resources/app/chrome_linux64/easyspider_executestage --id [" + app.$data.ID.toString() + "] --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
         } else if(OSInfo.version == 'mac'){
-            app.$data.command = "./easyspider_executestage --id " + app.$data.ID.toString() + " --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
+            app.$data.command = "./easyspider_executestage --id [" + app.$data.ID.toString() + "] --user_data " + (app.$data.with_user_data ? "1" : "0") + " --server_address " + app.$data.backEndAddressServiceWrapper;
         }
     }
 

+ 2 - 2
ElectronJS/src/taskGrid/taskList.html

@@ -23,8 +23,8 @@
             <h4 style="text-align: center;">{{"Task List~任务列表" | lang}}</h4>
             <p><a v-if="type==3" href="javascript:void(0)" v-on:click="newTask" class="btn btn-primary">{{"New Task~创建新任务" | lang}}</a></p>
             <div v-if="type != 3" style="margin-bottom: 20px">
-                <a class="btn btn-primary" href="https://github.com/NaiboWang/EasySpider/issues/22" target="_blank">{{"How to run task by schedule~定时执行任务教程" | lang}}</a>
-                <a class="btn btn-primary" href="" target="_blank">{{"How to run task by schedule~定时执行任务教程" | lang}}</a>
+                <a class="btn btn-primary" href="https://github.com/NaiboWang/EasySpider/issues/22" target="_blank">{{"See how to run task by schedule~定时执行任务教程" | lang}}</a>
+                <a class="btn btn-primary" href="https://github.com/NaiboWang/EasySpider/wiki/Run-multiple-tasks-in-parallel" target="_blank">{{"See how to run multiple tasks in parallel~同时执行多个任务教程" | lang}}</a>
             </div>
             <div style="margin-bottom: 10px">
                 <table style="table-layout: auto;" class="table table-hover">

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
ElectronJS/tasks/76.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
ElectronJS/tasks/77.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
ElectronJS/tasks/78.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
ElectronJS/tasks/79.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
ElectronJS/tasks/80.json


+ 1 - 1
ExecuteStage/.vscode/launch.json

@@ -12,7 +12,7 @@
             "console": "integratedTerminal",
             "justMyCode": true,
             // "args": ["--id", "38", "--read_type", "local", "--headless", "1"]
-            "args": ["--id", "54", "--headless", "0"]
+            "args": ["--id", "[21, 22]", "--headless", "0", "--user_data", "1"]
         }
     ]
 }

+ 1 - 1
ExecuteStage/config.json

@@ -2,5 +2,5 @@
     "webserver_address": "http://localhost",
     "webserver_port": 8074,
     "user_data_folder": "./user_data",
-    "absolute_user_data_folder": "/Users/naibowang/Documents/EasySpider/ElectronJS/user_data"
+    "absolute_user_data_folder": "D:\\Documents\\Projects\\EasySpider\\ElectronJS\\user_data"
 }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 840 - 808
ExecuteStage/easyspider_executestage.py


+ 1137 - 0
ExecuteStage/easyspider_executestage_single.py

@@ -0,0 +1,1137 @@
+# -*- coding: utf-8 -*-
+
+# 此脚本已停止维护,新版本已经使用easyspider_executestage.py替代,因为新版本的easyspider_executestage.py可以同时执行多个爬虫,而此脚本只能执行单个爬虫
+# This script has been discontinued. The new version has used easyspider_executestage.py instead, because the new version of easyspider_executestage.py can execute multiple crawlers at the same time, while this script can only execute a single crawler
+
+import atexit
+import io  # 遇到错误退出时应执行的代码
+import json
+from lib2to3.pgen2 import driver
+import re
+import subprocess
+import sys
+from urllib import parse
+import base64
+import hashlib
+import time
+import requests
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.common.action_chains import ActionChains
+from selenium import webdriver
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import NoSuchElementException
+from selenium.common.exceptions import TimeoutException
+from selenium.common.exceptions import StaleElementReferenceException, InvalidSelectorException
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.support.ui import Select
+from selenium.webdriver import ActionChains
+import random
+# import numpy
+import csv
+import os
+from selenium.webdriver.common.by import By
+from commandline_config import Config
+import pytesseract
+from PIL import Image
+import uuid
+
+saveName, log, OUTPUT, browser, SAVED = None, "", "", None, False
+
+desired_capabilities = DesiredCapabilities.CHROME
+desired_capabilities["pageLoadStrategy"] = "none"
+outputParameters = {}
+
+
+class Time:
+    def __init__(self, type1=""):
+        self.t = int(round(time.time() * 1000))
+        self.type = type1
+
+    def end(self):
+        at = int(round(time.time() * 1000))
+        Log(str(self.type)+":"+str(at-self.t))
+
+# 记录log
+
+
+def recordLog(str=""):
+    global log
+    log = log + str + "\n"
+
+
+# 控制台打印log函数
+def Log(text, text2=""):
+    switch = False
+    if switch:
+        print(text, text2)
+
+# 屏幕滚动函数
+
+
+
+def download_image(url, save_directory):
+    # 定义浏览器头信息
+    headers = {
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+    }
+    
+    # 发送 GET 请求获取图片数据
+    response = requests.get(url, headers=headers)
+
+    # 检查响应状态码是否为成功状态
+    if response.status_code == requests.codes.ok:
+        # 提取文件名
+        file_name = url.split('/')[-1]
+        
+        # 生成唯一的新文件名
+        new_file_name = str(uuid.uuid4()) + '_' + file_name
+        
+        # 构建保存路径
+        save_path = os.path.join(save_directory, new_file_name)
+        
+        # 保存图片到本地
+        with open(save_path, 'wb') as file:
+            file.write(response.content)
+        
+        print("图片已成功下载到:", save_path)
+        print("The image has been successfully downloaded to:", save_path)
+    else:
+        print("下载图片失败,请检查此图片链接是否有效:", url)
+        print("Failed to download image, please check if this image link is valid:", url)
+
+
+def scrollDown(para, rt=""):
+    scrollType = int(para["scrollType"])
+    try:
+        if scrollType != 0 and para["scrollCount"] > 0:  # 控制屏幕向下滚动
+            for i in range(para["scrollCount"]):
+                Log("Wait for set second after screen scrolling")
+                body = browser.find_element(By.CSS_SELECTOR, "body")
+                if scrollType == 1:
+                    body.send_keys(Keys.PAGE_DOWN)
+                elif scrollType == 2:
+                    body.send_keys(Keys.END)
+                time.sleep(para["scrollWaitTime"])  # 下拉完等待
+    except TimeoutException:
+        Log('time out after set seconds when scrolling. ')
+        recordLog('time out after set seconds when scrolling')
+        browser.execute_script('window.stop()')
+        if scrollType != 0 and para["scrollCount"] > 0:  # 控制屏幕向下滚动
+            for i in range(para["scrollCount"]):
+                Log("Wait for set second after screen scrolling")
+                body = browser.find_element(By.CSS_SELECTOR, "body")
+                if scrollType == 1:
+                    body.send_keys(Keys.PGDN)
+                elif scrollType == 2:
+                    body.send_keys(Keys.END)
+                time.sleep(para["scrollWaitTime"])  # 下拉完等待
+        if rt != "":
+            rt.end()
+
+def execute_code(codeMode, code, max_wait_time, element=None):
+    output = ""
+    if code == "":
+        return ""
+    if max_wait_time == 0:
+        max_wait_time = 999999
+    # print(codeMode, code)
+    if int(codeMode) == 0:
+        recordLog("Execute JavaScript:" + code)
+        recordLog("执行JavaScript:" + code)
+        browser.set_script_timeout(max_wait_time)
+        try:
+            output = browser.execute_script(code)
+        except:
+            output = ""
+            recordLog("JavaScript execution failed")
+    elif int(codeMode) == 2:
+        recordLog("Execute JavaScript for element:" + code)
+        recordLog("对元素执行JavaScript:" + code)
+        browser.set_script_timeout(max_wait_time)
+        try:
+            output = browser.execute_script(code, element)
+        except:
+            output = ""
+            recordLog("JavaScript execution failed")
+    elif int(codeMode) == 1:
+        recordLog("Execute System Call:" + code)
+        recordLog("执行系统命令:" + code)
+        # 执行系统命令,超时时间为5秒
+        try:
+            output = subprocess.run(code, capture_output=True, text=True, timeout=max_wait_time, encoding="utf-8")
+            # 输出命令返回值
+            output = output.stdout
+            print(output)
+        except subprocess.TimeoutExpired:
+            # 命令执行时间超过5秒,抛出异常
+            recordLog("Command timed out")
+            recordLog("命令执行超时")
+        except:
+            recordLog("Command execution failed")
+            recordLog("命令执行失败")
+    return str(output)
+
+def customOperation(node, loopValue, loopPath, index):
+    paras = node["parameters"]
+    codeMode = int(paras["codeMode"])
+    code = paras["code"]
+    max_wait_time = int(paras["waitTime"])
+    if codeMode == 2:  # 使用循环的情况下,传入的clickPath就是实际的xpath
+        try:
+            elements = browser.find_elements(By.XPATH, loopPath)
+            element = elements[index]
+            output = execute_code(codeMode, code, max_wait_time, element)
+        except:
+            output = ""
+            print("JavaScript execution failed")
+    else:
+        output = execute_code(codeMode, code, max_wait_time)
+    recordASField = int(paras["recordASField"])
+    if recordASField:
+        global OUTPUT, outputParameters
+        outputParameters[node["title"]] = output
+        line = []
+        for value in outputParameters.values():
+            line.append(value)
+            print(value[:15], " ", end="")
+        print("")
+        OUTPUT.append(line)
+
+def switchSelect(para, loopValue):
+    optionMode = int(para["optionMode"])
+    optionValue = para["optionValue"]
+    try:
+        dropdown = Select(browser.find_element(By.XPATH, para["xpath"]))
+        try:
+            if optionMode == 0:
+                # 获取当前选中的选项索引
+                current_index = dropdown.options.index(dropdown.first_selected_option)
+                # 计算下一个选项的索引
+                next_index = (current_index + 1) % len(dropdown.options)
+                # 选择下一个选项
+                dropdown.select_by_index(next_index)
+            elif optionMode == 1:
+                dropdown.select_by_index(int(optionValue))
+            elif optionMode == 2:
+                dropdown.select_by_value(optionValue)
+            elif optionMode == 3:
+                dropdown.select_by_visible_text(optionValue)
+        except:
+            print("切换下拉框选项失败:", para["xpath"], para["optionMode"], para["optionValue"])
+            print("Failed to change drop-down box option:", para["xpath"], para["optionMode"], para["optionValue"])
+    except:
+        print("找不到下拉框元素:", para["xpath"])
+        print("Cannot find drop-down box element:", para["xpath"])
+
+
+def moveToElement(para, loopElement=None, loopPath="", index=0):
+    time.sleep(0.1)  # 移动之前等待0.1秒
+    if para["useLoop"]:  # 使用循环的情况下,传入的clickPath就是实际的xpath
+        path = loopPath
+    else:
+        index = 0
+        path = para["xpath"]  # 不然使用元素定义的xpath
+    try:
+        elements = browser.find_elements(By.XPATH, path)
+        element = elements[index]
+        try:
+            ActionChains(browser).move_to_element(element).perform()
+        except:
+            print("移动鼠标到元素失败:", para["xpath"])
+            print("Failed to move mouse to element:", para["xpath"])
+    except:
+        print("找不到元素:", para["xpath"])
+        print("Cannot find element:", para["xpath"])
+
+
+# 执行节点关键函数部分
+def executeNode(nodeId, loopValue="", loopPath="", index=0):
+    node = procedure[nodeId]
+    WebDriverWait(browser, 10).until
+    # 等待元素出现才进行操作,10秒内未出现则报错
+    (EC.visibility_of_element_located((By.XPATH, node["parameters"]["xpath"])))
+
+    # 根据不同选项执行不同操作
+    if node["option"] == 0 or node["option"] == 10:  # root操作,条件分支操作
+        for i in node["sequence"]:  # 从根节点开始向下读取
+            executeNode(i, loopValue, loopPath, index)
+    elif node["option"] == 1:  # 打开网页操作
+        recordLog("openPage")
+        openPage(node["parameters"], loopValue)
+    elif node["option"] == 2:  # 点击元素
+        recordLog("Click")
+        clickElement(node["parameters"], loopValue, loopPath, index)
+    elif node["option"] == 3:  # 提取数据
+        recordLog("getData")
+        getData(node["parameters"], loopValue, node["isInLoop"],
+                parentPath=loopPath, index=index)
+        saveData()
+    elif node["option"] == 4:  # 输入文字
+        inputInfo(node["parameters"], loopValue)
+    elif node["option"] == 5:  # 自定义操作
+        customOperation(node, loopValue, loopPath, index)
+        saveData()
+    elif node["option"] == 6:  # 切换下拉框
+        switchSelect(node["parameters"], loopValue)
+    elif node["option"] == 7:  # 鼠标移动到元素上
+        moveToElement(node["parameters"], loopValue, loopPath, index)
+    elif node["option"] == 8:  # 循环
+        recordLog("loop")
+        loopExcute(node, loopValue, loopPath, index)  # 执行循环
+    elif node["option"] == 9:  # 条件分支
+        recordLog("judge")
+        judgeExcute(node, loopValue, loopPath, index)
+
+    # 执行完之后进行等待
+    if node["option"] != 0:
+        waitTime = 0.01  # 默认等待0.01秒
+        if node["parameters"]["wait"] > 1:
+            waitTime = node["parameters"]["wait"]
+        time.sleep(waitTime)
+        Log("Wait seconds after node executing: ", waitTime)
+
+
+# 对判断条件的处理
+def judgeExcute(node, loopElement, clickPath="", index=0):
+    # rt = Time("IF Condition")
+    global bodyText  # 引入bodyText
+    executeBranchId = 0  # 要执行的BranchId
+    for i in node["sequence"]:
+        cnode = procedure[i]  # 获得条件分支
+        tType = int(cnode["parameters"]["class"])  # 获得判断条件类型
+        if tType == 0:  # 什么条件都没有
+            executeBranchId = i
+            break
+        elif tType == 1:  # 当前页面包含文本
+            try:
+                if bodyText.find(cnode["parameters"]["value"]) >= 0:
+                    executeBranchId = i
+                    break
+            except:  # 找不到元素下一个条件
+                continue
+        elif tType == 2:  # 当前页面包含元素
+            try:
+                if browser.find_element(By.XPATH, cnode["parameters"]["value"]):
+                    executeBranchId = i
+                    break
+            except:  # 找不到元素或者xpath写错了,下一个条件
+                continue
+        elif tType == 3:  # 当前循环元素包括文本
+            try:
+                if loopElement.text.find(cnode["parameters"]["value"]) >= 0:
+                    executeBranchId = i
+                    break
+            except:  # 找不到元素或者xpath写错了,下一个条件
+                continue
+        elif tType == 4:  # 当前循环元素包括元素
+            try:
+                if loopElement.find_element(By.XPATH, cnode["parameters"]["value"][1:]):
+                    executeBranchId = i
+                    break
+            except:  # 找不到元素或者xpath写错了,下一个条件
+                continue
+        elif tType <= 7:  # JS命令返回值
+            if tType == 5:  # JS命令返回值等于
+                output = execute_code(0, cnode["parameters"]["code"], cnode["parameters"]["waitTime"])
+            elif tType == 6:  # System
+                output = execute_code(1, cnode["parameters"]["code"], cnode["parameters"]["waitTime"])
+            elif tType == 7:  # 针对当前循环项的JS命令返回值
+                output = execute_code(2, cnode["parameters"]["code"], cnode["parameters"]["waitTime"], loopElement)
+            try:
+                if output.find("rue") != -1: # 如果返回值中包含true
+                    code = 1
+                else:
+                    code = int(output)
+            except:
+                code = 0
+            if code > 0:
+                executeBranchId = i
+                break
+    # rt.end()
+    if executeBranchId != 0:
+        executeNode(executeBranchId, loopElement, clickPath, index)
+
+def get_output_code(output):
+    try:
+        if output.find("rue") != -1: # 如果返回值中包含true
+            code = 1
+        else:
+            code = int(output)
+    except:
+        code = 0
+    return code
+
+# 对循环的处理
+def loopExcute(node, loopValue, clickPath="", index=0):
+    time.sleep(0.1)  # 第一次执行循环的时候强制等待1秒
+    # Log("循环执行前等待0.1秒")
+    Log("Wait 0.1 second before loop")
+    global history
+    thisHandle = browser.current_window_handle  # 记录本次循环内的标签页的ID
+    thisHistoryLength = browser.execute_script(
+        'return history.length')  # 记录本次循环内的history的length
+    history["index"] = thisHistoryLength
+    history["handle"] = thisHandle
+
+    if int(node["parameters"]["loopType"]) == 0:  # 单个元素循环
+        # 无跳转标签页操作
+        count = 0  # 执行次数
+        while True:  # do while循环
+            try:
+                finished = False
+                element = browser.find_element(
+                    By.XPATH, node["parameters"]["xpath"])
+                for i in node["sequence"]:  # 挨个执行操作
+                    executeNode(i, element, node["parameters"]["xpath"], 0)
+                finished = True
+                Log("click: ", node["parameters"]["xpath"])
+                recordLog("click:" + node["parameters"]["xpath"])
+            except NoSuchElementException:
+                # except:
+                print("Single loop element not found: ", node["parameters"]["xpath"])
+                print("找不到要循环的单个元素: ", node["parameters"]["xpath"])
+                recordLog("Single loop element not found: " + node["parameters"]["xpath"])
+                for i in node["sequence"]:  # 不带点击元素的把剩余的如提取数据的操作执行一遍
+                    if node["option"] != 2:
+                        executeNode(i, None, node["parameters"]["xpath"], 0)
+                finished = True
+                break  # 如果找不到元素,退出循环
+            finally:
+                if not finished:
+                    print("\n\n-------Retrying-------\n\n")
+                    Log("-------Retrying-------: ",
+                        node["parameters"]["xpath"])
+                    recordLog("clickNotFound:" + node["parameters"]["xpath"])
+                    for i in node["sequence"]:  # 不带点击元素的把剩余的如提取数据的操作执行一遍
+                        if node["option"] != 2:
+                            executeNode(i, None, node["parameters"]["xpath"], 0)
+                    break  # 如果找不到元素,退出循环
+            count = count + 1
+            Log("Page: ", count)
+            recordLog("Page:" + str(count))
+            # print(node["parameters"]["exitCount"], "-------")
+            if node["parameters"]["exitCount"] == count:  # 如果达到设置的退出循环条件的话
+                break
+            if int(node["parameters"]["breakMode"]) > 0:  # 如果设置了退出循环的脚本条件
+                output = execute_code(int(node["parameters"]["breakMode"]) -1, node["parameters"]["breakCode"], node["parameters"]["breakCodeWaitTime"])
+                code = get_output_code(output)
+                if code <= 0:
+                    break
+    elif int(node["parameters"]["loopType"]) == 1:  # 不固定元素列表
+        try:
+            elements = browser.find_elements(By.XPATH,
+                                             node["parameters"]["xpath"])
+            if len(elements) == 0:
+                print("Loop element not found: ", node["parameters"]["xpath"])
+                print("找不到循环元素: ", node["parameters"]["xpath"])
+                recordLog("pathNotFound: " + node["parameters"]["xpath"])
+            for index in range(len(elements)):
+                for i in node["sequence"]:  # 挨个顺序执行循环里所有的操作
+                    executeNode(i, elements[index],
+                               node["parameters"]["xpath"], index)
+                if browser.current_window_handle != thisHandle:  # 如果执行完一次循环之后标签页的位置发生了变化
+                    while True:  # 一直关闭窗口直到当前标签页
+                        browser.close()  # 关闭使用完的标签页
+                        browser.switch_to.window(browser.window_handles[-1])
+                        if browser.current_window_handle == thisHandle:
+                            break
+                if history["index"] != thisHistoryLength and history[
+                        "handle"] == browser.current_window_handle:  # 如果执行完一次循环之后历史记录发生了变化,注意当前页面的判断
+                    difference = thisHistoryLength - \
+                        history["index"]  # 计算历史记录变化差值
+                    browser.execute_script(
+                        'history.go(' + str(difference) + ')')  # 回退历史记录
+                    if node["parameters"]["historyWait"] > 2:  # 回退后要等待的时间
+                        time.sleep(node["parameters"]["historyWait"])
+                    else:
+                        time.sleep(2)
+                    # 切换历史记录等待2秒或者:
+                    Log("Change history back time or:",
+                        node["parameters"]["historyWait"])
+                    browser.execute_script('window.stop()')
+                if int(node["parameters"]["breakMode"]) > 0:  # 如果设置了退出循环的脚本条件
+                    output = execute_code(int(node["parameters"]["breakMode"]) -1, node["parameters"]["breakCode"], node["parameters"]["breakCodeWaitTime"])
+                    code = get_output_code(output)
+                    if code <= 0:
+                        break
+        except NoSuchElementException:
+            print("Loop element not found: ", node["parameters"]["xpath"])
+            print("找不到循环元素: ", node["parameters"]["xpath"])
+            recordLog("pathNotFound: " + node["parameters"]["xpath"])
+        except Exception as e:
+            raise
+    elif int(node["parameters"]["loopType"]) == 2:  # 固定元素列表
+        for path in node["parameters"]["pathList"].split("\n"):  # 千万不要忘了分割!!
+            try:
+                element = browser.find_element(By.XPATH, path)
+                for i in node["sequence"]:  # 挨个执行操作
+                    executeNode(i, element, path, 0)
+                if browser.current_window_handle != thisHandle:  # 如果执行完一次循环之后标签页的位置发生了变化
+                    while True:  # 一直关闭窗口直到当前标签页
+                        browser.close()  # 关闭使用完的标签页
+                        browser.switch_to.window(browser.window_handles[-1])
+                        if browser.current_window_handle == thisHandle:
+                            break
+                if history["index"] != thisHistoryLength and history[
+                        "handle"] == browser.current_window_handle:  # 如果执行完一次循环之后历史记录发生了变化,注意当前页面的判断
+                    difference = thisHistoryLength - \
+                        history["index"]  # 计算历史记录变化差值
+                    browser.execute_script(
+                        'history.go(' + str(difference) + ')')  # 回退历史记录
+                    if node["parameters"]["historyWait"] > 2:  # 回退后要等待的时间
+                        time.sleep(node["parameters"]["historyWait"])
+                    else:
+                        time.sleep(2)
+                    Log("Change history back time or:",
+                        node["parameters"]["historyWait"])
+                    browser.execute_script('window.stop()')
+            except NoSuchElementException:
+                print("Loop element not found: ", path)
+                print("找不到循环元素: ", path)
+                recordLog("pathNotFound: " + path)
+                continue  # 循环中找不到元素就略过操作
+            except Exception as e:
+                raise
+            if int(node["parameters"]["breakMode"]) > 0:  # 如果设置了退出循环的脚本条件
+                output = execute_code(int(node["parameters"]["breakMode"]) -1, node["parameters"]["breakCode"], node["parameters"]["breakCodeWaitTime"])
+                code = get_output_code(output)
+                if code <= 0:
+                    break
+    elif int(node["parameters"]["loopType"]) == 3:  # 固定文本列表
+        textList = node["parameters"]["textList"].split("\n")
+        for text in textList:
+            recordLog("input: " + text)
+            for i in node["sequence"]:  # 挨个执行操作
+                executeNode(i, text, "", 0)
+            if int(node["parameters"]["breakMode"]) > 0:  # 如果设置了退出循环的脚本条件
+                output = execute_code(int(node["parameters"]["breakMode"]) -1, node["parameters"]["breakCode"], node["parameters"]["breakCodeWaitTime"])
+                code = get_output_code(output)
+                if code <= 0:
+                    break
+    elif int(node["parameters"]["loopType"]) == 4:  # 固定网址列表
+        # tempList = node["parameters"]["textList"].split("\r\n")
+        urlList = list(
+            filter(isnull, node["parameters"]["textList"].split("\n")))  # 去空行
+        # urlList = []
+        # for url in tempList:
+        #     if url != "":
+        #         urlList.append(url)
+        for url in urlList:
+            recordLog("input: " + url)
+            for i in node["sequence"]:
+                executeNode(i, url, "", 0)
+            if int(node["parameters"]["breakMode"]) > 0:  # 如果设置了退出循环的脚本条件
+                output = execute_code(int(node["parameters"]["breakMode"]) -1, node["parameters"]["breakCode"], node["parameters"]["breakCodeWaitTime"])
+                code = get_output_code(output)
+                if code <= 0:
+                    break
+    elif int(node["parameters"]["loopType"]) <= 6:  # 命令返回值
+        while True:  # do while循环
+            if int(node["parameters"]["loopType"]) == 5:  # JS
+                output = execute_code(0, node["parameters"]["code"], node["parameters"]["waitTime"])
+            elif int(node["parameters"]["loopType"]) == 6:  # System
+                output = execute_code(1, node["parameters"]["code"], node["parameters"]["waitTime"])
+            code = get_output_code(output)
+            if code <= 0:
+                break
+            for i in node["sequence"]:  # 挨个执行操作
+                executeNode(i, code, node["parameters"]["xpath"], 0)
+    history["index"] = thisHistoryLength
+    history["handle"] = browser.current_window_handle
+    scrollDown(node["parameters"])
+
+
+# 打开网页事件
+def openPage(para, loopValue):
+    # rt = Time("打开网页")
+    time.sleep(2)  # 打开网页后强行等待至少2秒
+    global links
+    global urlId
+    global history
+    global outputParameters
+    # try:
+    #     firstTime = True
+    #     for handle in browser.window_handles:
+    #         browser.switch_to.window(handle)
+    #         if (not firstTime):
+    #             browser.close()
+    #         firstTime = False
+    # except:
+    #     return
+    if len(browser.window_handles) > 1:
+        browser.switch_to.window(browser.window_handles[-1])  # 打开网页操作从第1个页面开始
+        browser.close()
+    browser.switch_to.window(browser.window_handles[0])  # 打开网页操作从第1个页面开始
+    history["handle"] = browser.current_window_handle
+    if para["useLoop"]:
+        url = loopValue
+    else:
+        url = links[urlId]
+    try:
+        maxWaitTime = int(para["maxWaitTime"])
+    except:
+        maxWaitTime = 10 # 默认最大等待时间为10秒
+    try:
+        browser.set_page_load_timeout(maxWaitTime)  # 加载页面最大超时时间
+        browser.set_script_timeout(maxWaitTime)
+        browser.get(url)
+        Log('Loading page: ' + url)
+        recordLog('Loading page: ' + url)
+    except TimeoutException:
+        Log('time out after set seconds when loading page: ' + url)
+        recordLog('time out after set seconds when loading page: ' + url)
+        browser.execute_script('window.stop()')
+        # rt.end()
+    try:
+        history["index"] = browser.execute_script("return history.length")
+    except TimeoutException:
+        browser.execute_script('window.stop()')
+        history["index"] = browser.execute_script("return history.length")
+        # rt.end()
+    scrollDown(para)  # 控制屏幕向下滚动
+    if containJudge:
+        global bodyText  # 每次执行点击,输入元素和打开网页操作后,需要更新bodyText
+        try:
+            bodyText = browser.find_element(By.CSS_SELECTOR, "body").text
+            Log('URL Page: ' + url)
+            recordLog('URL Page: ' + url)
+        except TimeoutException:
+            Log('Time out after set seconds when getting body text: ' + url)
+            recordLog('Time out after set seconds when getting body text:: ' + url)
+            browser.execute_script('window.stop()')
+            time.sleep(1)
+            Log("Need to wait 1 second to get body text")
+            # 再执行一遍
+            bodyText = browser.find_element(By.CSS_SELECTOR, "body").text
+            # rt.end()
+        except Exception as e:
+            Log(e)
+            recordLog(str(e))
+
+    # clear output parameters
+    for key in outputParameters:
+        outputParameters[key] = ""
+
+    # rt.end()
+
+
+# 键盘输入事件
+def inputInfo(para, loopValue):
+    time.sleep(0.1)  # 输入之前等待0.1秒
+    Log("Wait 1 second before input")
+    try:
+        textbox = browser.find_element(By.XPATH, para["xpath"])
+        #     textbox.send_keys(Keys.CONTROL, 'a')
+        #     textbox.send_keys(Keys.BACKSPACE)
+        execute_code(2, para["beforeJS"], para["beforeJSWaitTime"], textbox) # 执行前置JS
+        # Send the HOME key
+        textbox.send_keys(Keys.HOME)
+        # Send the SHIFT + END key combination
+        textbox.send_keys(Keys.SHIFT, Keys.END)
+        # Send the DELETE key
+        textbox.send_keys(Keys.DELETE)
+        if para["useLoop"]:
+            textbox.send_keys(loopValue)
+        else:
+            textbox.send_keys(para["value"])
+        execute_code(2, para["afterJS"], para["afterJSWaitTime"], textbox) # 执行后置js
+        global bodyText  # 每次执行点击,输入元素和打开网页操作后,需要更新bodyText
+        bodyText = browser.find_element(By.CSS_SELECTOR, "body").text
+    except:
+        print("Cannot find input box element:" +
+            para["xpath"] + ", please try to set the wait time before executing this operation")
+        print("找不到输入框元素:" + para["xpath"] + ",请尝试在执行此操作前设置等待时间")
+        recordLog("Cannot find input box element:" +
+                  para["xpath"] + "Please try to set the wait time before executing this operation")
+
+
+# 点击元素事件
+def clickElement(para, loopElement=None, clickPath="", index=0):
+    global history
+    time.sleep(0.1)  # 点击之前等待0.1秒
+    # rt = Time("Click Element")
+    Log("Wait 0.1 second before clicking element")
+    if para["useLoop"]:  # 使用循环的情况下,传入的clickPath就是实际的xpath
+        path = clickPath
+    else:
+        path = para["xpath"]  # 不然使用元素定义的xpath
+    try:
+        maxWaitTime = int(para["maxWaitTime"])
+    except:
+        maxWaitTime = 10
+    browser.set_page_load_timeout(maxWaitTime)  # 加载页面最大超时时间
+    browser.set_script_timeout(maxWaitTime)
+    # 点击前对该元素执行一段JavaScript代码
+    try:
+        element = browser.find_element(By.XPATH, path)
+        if para["beforeJS"] != "":
+            execute_code(2, para["beforeJS"], para["beforeJSWaitTime"], element)
+    except:
+        print("Cannot find element:" +
+            path + ", please try to set the wait time before executing this operation")
+        print("找不到要点击的元素:" + path + ",请尝试在执行此操作前设置等待时间")
+        recordLog("Cannot find element:" +
+                  path + ", please try to set the wait time before executing this operation")
+    tempHandleNum = len(browser.window_handles)  # 记录之前的窗口位置
+    try:
+        script = 'var result = document.evaluate(`' + path + \
+            '`, document, null, XPathResult.ANY_TYPE, null);for(let i=0;i<arguments[0];i++){result.iterateNext();} result.iterateNext().click();'
+        browser.execute_script(script, str(index))  # 用js的点击方法
+    except TimeoutException:
+        Log('time out after set seconds when loading clicked page')
+        recordLog('time out after set seconds when loading clicked page')
+        browser.execute_script('window.stop()')
+        # rt.end()
+    except Exception as e:
+        Log(e)
+        recordLog(str(e))
+    time.sleep(0.5)  # 点击之后等半秒
+    Log("Wait 0.5 second after clicking element")
+    time.sleep(random.uniform(1, 2))  # 生成一个a到b的小数等待时间
+    # 点击前对该元素执行一段JavaScript代码
+    try:
+        if para["afterJS"] != "":
+            element = browser.find_element(By.XPATH, path)
+            execute_code(2, para["afterJS"], para["afterJSWaitTime"], element)
+    except:
+        print("Cannot find element:" + path)
+        recordLog("Cannot find element:" +
+                  path + ", please try to set the wait time before executing this operation")
+        print("找不到要点击的元素:" + path + ",请尝试在执行此操作前设置等待时间")
+    if tempHandleNum != len(browser.window_handles):  # 如果有新标签页的行为发生
+        browser.switch_to.window(browser.window_handles[-1])  # 跳转到新的标签页
+        history["handle"] = browser.current_window_handle
+        try:
+            history["index"] = browser.execute_script("return history.length")
+        except TimeoutException:
+            browser.execute_script('window.stop()')
+            history["index"] = browser.execute_script("return history.length")
+            # rt.end()
+    else:
+        try:
+            history["index"] = browser.execute_script("return history.length")
+        except TimeoutException:
+            browser.execute_script('window.stop()')
+            history["index"] = browser.execute_script("return history.length")
+            # rt.end()
+        # 如果打开了新窗口,切换到新窗口
+    scrollDown(para)  # 根据参数配置向下滚动
+    if containJudge:  # 有判断语句才执行以下操作
+        global bodyText  # 每次执行点击,输入元素和打开网页操作后,需要更新bodyText
+        try:
+            bodyText = browser.find_element(By.CSS_SELECTOR, "body").text
+        except TimeoutException:
+            Log('time out after 10 seconds when getting body text')
+            recordLog('time out after 10 seconds when getting body text')
+            browser.execute_script('window.stop()')
+            time.sleep(1)
+            Log("wait one second after get body text")
+            # 再执行一遍
+            bodyText = browser.find_element(By.CSS_SELECTOR, "body").text
+            # rt.end()
+        except Exception as e:
+            Log(e)
+            recordLog(str(e))
+    # rt.end()
+
+def get_content(p, element):
+    global saveName
+    content = ""
+    # 先处理特殊节点类型
+    if p["nodeType"] == 2:
+        if element.get_attribute("href") != None:
+            content = element.get_attribute("href")
+        else:
+            content = ""
+    elif p["nodeType"] == 3:
+        if element.get_attribute("value") != None:
+            content = element.get_attribute("value")
+        else:
+            content = ""
+    elif p["nodeType"] == 4:  # 图片
+        if element.get_attribute("src") != None:
+            content = element.get_attribute("src")
+        else:
+            content = ""
+        try:
+            downloadPic = p["downloadPic"]
+        except:
+            downloadPic = 0
+        if downloadPic == 1:
+            download_image(content, "Data/" +saveName + "/")
+    else: # 普通节点
+        if p["contentType"] == 0:
+            content = element.text
+        elif p["contentType"] == 1:  # 只采集当期元素下的文本,不包括子元素
+            command = 'var arr = [];\
+            var content = arguments[0];\
+            for(var i = 0, len = content.childNodes.length; i < len; i++) {\
+                if(content.childNodes[i].nodeType === 3){  \
+                    arr.push(content.childNodes[i].nodeValue);\
+                }\
+            }\
+            var str = arr.join(" "); \
+            return str;'
+            content = browser.execute_script(command, element).replace(
+                "\n", "").replace("\\s+", " ")
+        elif p["contentType"] == 2:
+            content = element.get_attribute('innerHTML')
+        elif p["contentType"] == 3:
+            content = element.get_attribute('outerHTML')
+        elif p["contentType"] == 4:
+            # 获取元素的背景图片地址
+            bg_url = element.value_of_css_property('background-image')
+            # 清除背景图片地址中的多余字符
+            bg_url = bg_url.replace('url("', '').replace('")', '')
+            content = bg_url
+        elif p["contentType"] == 5:
+            content = browser.current_url
+        elif p["contentType"] == 6:
+            content = browser.title
+        elif p["contentType"] == 7:
+            # 获取整个网页的高度和宽度
+            height = browser.execute_script("return document.body.scrollHeight");
+            width = browser.execute_script("return document.body.scrollWidth");
+            # 调整浏览器窗口的大小
+            browser.set_window_size(width, height)
+            element.screenshot("Data/" +saveName + "/"+ str(time.time()) + ".png")
+        elif p["contentType"] == 8:
+            try:
+                screenshot = element.screenshot_as_png
+                screenshot_stream = io.BytesIO(screenshot)
+                # 使用Pillow库打开截图,并转换为灰度图像
+                image = Image.open(screenshot_stream).convert('L')
+                # 使用Tesseract OCR引擎识别图像中的文本
+                text = pytesseract.image_to_string(image,  lang='chi_sim+eng')
+                content = text
+            except Exception as e:
+                content = "OCR Error"
+                print("To use OCR, You need to install Tesseract-OCR and add it to the environment variable PATH: https://tesseract-ocr.github.io/tessdoc/Installation.html")
+                print("要使用OCR识别功能,你需要安装Tesseract-OCR并将其添加到环境变量PATH中:https://blog.csdn.net/u010454030/article/details/80515501")
+        elif p["contentType"] == 9:
+            content = execute_code(2, p["JS"], p["JSWaitTime"], element)
+        elif p["contentType"] == 10: # 下拉框选中的值
+            try:
+                select_element = Select(element)
+                content = select_element.first_selected_option.get_attribute("value")
+            except:
+                content = ""
+        elif p["contentType"] == 11: # 下拉框选中的文本
+            try:
+                select_element = Select(element)
+                content = select_element.first_selected_option.text
+            except:
+                content = ""
+    return content
+
+
+# 提取数据事件
+def getData(para, loopElement, isInLoop=True, parentPath="", index=0):
+    if not isInLoop and para["wait"] == 0:
+        time.sleep(1)  # 如果提取数据字段不在循环内而且设置的等待时间为0,默认等待1秒
+        Log("Wait 1 second before extracting data")
+    # rt = Time("Extract Data")
+    for p in para["paras"]:
+        content = ""
+        if not (p["contentType"] == 5 or p["contentType"] == 6):  # 如果不是页面标题或URL,去找元素
+            try:
+                if p["relative"]:  # 是否相对xpath
+                    if p["relativeXPath"] == "":  # 相对xpath有时候就是元素本身,不需要二次查找
+                        element = loopElement
+                    else:
+                        if p["relativeXPath"].find("//") >= 0:  # 如果字串里有//即子孙查找,则不动语句
+                            full_path = "(" + parentPath + \
+                                p["relativeXPath"] + ")" + \
+                                "[" + str(index + 1) + "]"
+                            element = browser.find_element(By.XPATH, full_path)
+                        else:
+                            element = loopElement.find_element(By.XPATH,
+                                                               p["relativeXPath"][1:])
+                else:
+                    element = browser.find_element(By.XPATH, p["relativeXPath"])
+            except (NoSuchElementException, InvalidSelectorException):  # 找不到元素的时候,使用默认值
+                # print(p)
+                try:
+                    content = p["default"]
+                except Exception as e:
+                    content = ""
+                outputParameters[p["name"]] = content
+                try:
+                    if not dataNotFoundKeys[p["name"]]:
+                        print('Element %s not found with parameter name %s when extracting data, use default, this error will only show once' % (p["relativeXPath"], p["name"]))
+                        print("提取数据操作时,字段名 %s 对应XPath %s 未找到,使用默认值,本字段将不再重复报错" % (p["name"], p["relativeXPath"]))
+                        dataNotFoundKeys[p["name"]] = True
+                        recordLog('Element %s not found, use default' % p["relativeXPath"])
+                except:
+                    pass
+                continue
+            except TimeoutException:  # 超时的时候设置超时值
+                Log('time out after set seconds when getting data')
+                recordLog('time out after set seconds when getting data')
+                browser.execute_script('window.stop()')
+                if p["relative"]:  # 是否相对xpath
+                    if p["relativeXPath"] == "":  # 相对xpath有时候就是元素本身,不需要二次查找
+                        element = loopElement
+                    else:
+                        element = loopElement.find_element(By.XPATH,
+                                                           p["relativeXPath"][1:])
+                else:
+                    element = browser.find_element(By.XPATH, p["relativeXPath"])
+                # rt.end()
+        else:
+            element = browser.find_element(By.XPATH, "//body")
+        try:
+            execute_code(2, p["beforeJS"], p["beforeJSWaitTime"], element) # 执行前置js
+            content = get_content(p, element)
+        except StaleElementReferenceException:  # 发生找不到元素的异常后,等待几秒重新查找
+            recordLog('StaleElementReferenceException:'+p["relativeXPath"])
+            time.sleep(3)
+            try:
+                if p["relative"]:  # 是否相对xpath
+                    if p["relativeXPath"] == "":  # 相对xpath有时候就是元素本身,不需要二次查找
+                        element = loopElement
+                        recordLog('StaleElementReferenceException:loopElement')
+                    else:
+                        element = loopElement.find_element(By.XPATH,
+                                                           p["relativeXPath"][1:])
+                        recordLog(
+                            'StaleElementReferenceException:loopElement+relativeXPath')
+                else:
+                    element = browser.find_element(
+                        By.XPATH, p["relativeXPath"])
+                    recordLog('StaleElementReferenceException:relativeXPath')
+                content = get_content(p, element)
+            except StaleElementReferenceException:
+                recordLog('StaleElementReferenceException:'+p["relativeXPath"])
+                continue  # 再出现类似问题直接跳过
+        outputParameters[p["name"]] = content
+        execute_code(2, p["afterJS"], p["afterJSWaitTime"], element) # 执行后置JS
+    global OUTPUT
+    line = []
+    for value in outputParameters.values():
+        line.append(value)
+        print(value[:15], " ", end="")
+    print("")
+    OUTPUT.append(line)
+    # rt.end()
+
+
+# 判断字段是否为空
+def isnull(s):
+    return len(s) != 0
+
+
+def saveData(exit=False):
+    global saveName, log, OUTPUT, browser
+    if exit == True or len(OUTPUT) >= 100: # 每100条保存一次
+        with open("Data/"+saveName + '_log.txt', 'a', encoding='utf-8-sig') as file_obj:
+            file_obj.write(log)
+            file_obj.close()
+        with open("Data/"+saveName + '.csv', 'a', encoding='utf-8-sig', newline="") as f:
+            f_csv = csv.writer(f)
+            for line in OUTPUT:
+                f_csv.writerow(line)
+            f.close()
+        OUTPUT = []
+        log = ""
+        
+
[email protected]
+def clean():
+    global saveName, log, OUTPUT, browser, SAVED
+    saveData(exit=True)
+    browser.quit()
+    sys.exit(0)
+
+
+if __name__ == '__main__':
+    config = {
+        "id": 0,
+        "server_address": "http://localhost:8074",
+        "saved_file_name": "",
+        "read_type": "remote",
+        "user_data": False,
+        "config_folder": "",
+        "config_file_name": "config.json",
+        "headless": False,
+        "version": "0.3.1",
+    }
+    c = Config(config)
+    print(c)
+    options = Options()
+    driver_path = "chromedriver.exe"
+    import platform
+    print(sys.platform, platform.architecture())
+    option = webdriver.ChromeOptions()
+    if not os.path.exists(os.getcwd()+"/Data"):
+        os.mkdir(os.getcwd()+"/Data")
+    if sys.platform == "darwin" and platform.architecture()[0] == "64bit":
+            options.binary_location = "EasySpider.app/Contents/Resources/app/chrome_mac64.app/Contents/MacOS/Google Chrome"
+            # MacOS需要用option而不是options!
+            option.binary_location = "EasySpider.app/Contents/Resources/app/chrome_mac64.app/Contents/MacOS/Google Chrome"
+            driver_path = "EasySpider.app/Contents/Resources/app/chromedriver_mac64"
+            # options.binary_location = "chrome_mac64.app/Contents/MacOS/Google Chrome"
+            # # MacOS需要用option而不是options!
+            # option.binary_location = "chrome_mac64.app/Contents/MacOS/Google Chrome"
+            # driver_path = os.getcwd()+ "/chromedriver_mac64"
+            print(driver_path)
+    elif os.path.exists(os.getcwd()+"/EasySpider/resources"): # 打包后的路径
+        print("Finding chromedriver in EasySpider",
+              os.getcwd()+"/EasySpider")
+        if sys.platform == "win32" and platform.architecture()[0] == "32bit":
+            options.binary_location = os.path.join(
+                os.getcwd(), "EasySpider/resources/app/chrome_win32/chrome.exe")  # 指定chrome位置
+            driver_path = os.path.join(
+                os.getcwd(), "EasySpider/resources/app/chrome_win32/chromedriver_win32.exe")
+        elif sys.platform == "win32" and platform.architecture()[0] == "64bit":
+            options.binary_location = os.path.join(
+                os.getcwd(), "EasySpider/resources/app/chrome_win64/chrome.exe")
+            driver_path = os.path.join(
+                os.getcwd(), "EasySpider/resources/app/chrome_win64/chromedriver_win64.exe")
+        elif sys.platform == "linux" and platform.architecture()[0] == "64bit":
+            options.binary_location = "EasySpider/resources/app/chrome_linux64/chrome"
+            driver_path = "EasySpider/resources/app/chrome_linux64/chromedriver_linux64"
+        else:
+            print("Unsupported platform")
+            sys.exit()
+        print("Chrome location:", options.binary_location)
+        print("Chromedriver location:", driver_path)
+    elif os.path.exists(os.getcwd()+"/../ElectronJS"): 
+        if os.getcwd().find("ElectronJS") >= 0:  # 软件dev用
+            print("Finding chromedriver in EasySpider",
+                os.getcwd())
+            option.binary_location = "chrome_win64/chrome.exe"
+            driver_path = "chrome_win64/chromedriver_win64.exe"
+        else: # 直接在executeStage文件夹内使用python easyspider_executestage.py时的路径
+            print("Finding chromedriver in EasySpider",
+                os.getcwd()+"/ElectronJS")
+            option.binary_location = "../ElectronJS/chrome_win64/chrome.exe"  # 指定chrome位置
+            driver_path = "../ElectronJS/chrome_win64/chromedriver_win64.exe"
+    elif os.getcwd().find("ExecuteStage") >= 0:  # 如果直接执行
+        print("Finding chromedriver in ./Chrome",
+              os.getcwd()+"/Chrome")
+        options.binary_location = "./Chrome/chrome.exe"  # 指定chrome位置
+        # option.binary_location = "C:\\Users\\q9823\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"
+        driver_path = "./Chrome/chromedriver.exe"
+    else:
+        options.binary_location = "./chrome.exe"  # 指定chrome位置
+        driver_path = "./chromedriver.exe"
+
+    option.add_experimental_option(
+        'excludeSwitches', ['enable-automation'])  # 以开发者模式
+
+    # user_data_dir = r''  # 注意没有Default!
+
+    # options.add_argument('--user-data-dir='+p)
+
+    # 总结:
+    # 0. 带Cookie需要用userdatadir
+    # 1. chrome_options才是配置用户文件和chrome文件地址的正确选项
+    # 2. User Profile文件夹的路径是:C:\Users\用户名\AppData\Local\Google\Chrome\User Data不要加Default
+    # 3. 就算User Profile相同,chrome版本不同所存储的cookie信息也不同,也不能爬
+    # 4. TMALL如果一直弹出验证码,而且无法通过验证,那么需要在其他浏览器上用
+    if c.user_data:
+        with open(c.config_folder + c.config_file_name,"r", encoding='utf-8') as f:
+            config = json.load(f)
+            absolute_user_data_folder = config["absolute_user_data_folder"]
+            print("\nAbsolute_user_data_folder:",absolute_user_data_folder,"\n")
+        option.add_argument(f'--user-data-dir={absolute_user_data_folder}')  # TMALL 反扒
+        option.add_argument("--profile-directory=Default")
+    if c.headless:
+        print("Headless mode")
+        print("无头模式")
+        option.add_argument("--headless")
+        options.add_argument("--headless")
+
+    # options.add_argument(
+    #     '--user-data-dir=C:\\Users\\q9823\\AppData\\Local\\Google\\Chrome\\User Data')  # TMALL 反扒
+    option.add_argument(
+        "--disable-blink-features=AutomationControlled")  # TMALL 反扒
+    options.add_argument("--disable-blink-features=AutomationControlled")  # TMALL 反扒
+    print(options)
+    browser = webdriver.Chrome(
+        options=options, chrome_options=option, executable_path=driver_path)
+    stealth_path = driver_path[:driver_path.find("chromedriver")] + "stealth.min.js"
+    with open(stealth_path, 'r') as f:
+        js = f.read()
+        print("Loading stealth.min.js")
+    browser.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': js}) # TMALL 反扒
+
+    wait = WebDriverWait(browser, 10)
+    browser.get('about:blank')
+    id = c.id
+    print("id: ", id)
+    if c.saved_file_name != "":
+        saveName = "task_" + str(id) + "_" + c.saved_file_name  # 保存文件的名字
+    else:
+        saveName = "task_" + str(id) + "_" + \
+            str(random.randint(0, 999999999))  # 保存文件的名字
+    print("saveName: ", saveName)
+    os.mkdir("Data/" + saveName)  # 创建保存文件夹用来保存截图
+    backEndAddress = c.server_address
+    if c.read_type == "remote":
+        print("remote")
+        content = requests.get(backEndAddress + "/queryExecutionInstance?id=" + str(id))
+        service = json.loads(content.text)  # 加载服务信息
+    else:
+        print("local")
+        with open("execution_instances/" + str(id) + ".json", 'r', encoding='utf-8') as f:
+            content = f.read()
+            service = json.loads(content)  # 加载服务信息
+    print("name: ", service["name"])
+    procedure = service["graph"]  # 程序执行流程
+    try:
+        if service["version"] != c.version:
+            print("版本不一致,请使用" + service["version"] + "版本的EasySpider运行该任务")
+            print("Version not match, please use EasySpider " + service["version"] + " to run this task")
+            browser.quit()
+            sys.exit()
+    except:
+        print("版本不一致,请使用v0.2.0版本的EasySpider运行该任务")
+        print("Version not match, please use EasySpider v0.2.0 to run this task")
+        browser.quit()
+        sys.exit()
+    links = list(filter(isnull, service["links"].split("\n")))  # 要执行的link的列表
+    OUTPUT = []  # 采集的数据
+    OUTPUT.append([])  # 添加表头
+    containJudge = service["containJudge"]  # 是否含有判断语句
+    bodyText = ""  # 记录bodyText
+    tOut = service["outputParameters"]  # 生成输出参数对象
+    outputParameters = {}
+    dataNotFoundKeys = {}  # 记录没有找到数据的key
+    log = ""  # 记下现在总共开了多少个标签页
+    history = {"index": 0, "handle": None}  # 记录页面现在所以在的历史记录的位置
+    SAVED = False  # 记录是否已经存储了
+    for para in tOut:
+        outputParameters[para["name"]] = ""
+        dataNotFoundKeys[para["name"]] = False
+        OUTPUT[0].append(para["name"])
+    # 挨个执行程序
+    urlId = 0  # 全局记录变量
+    for i in range(len(links)):
+        executeNode(0)
+        urlId = urlId + 1
+
+    files = os.listdir("Data/" + saveName)
+
+    # 如果目录为空,则删除该目录
+    if not files:
+        os.rmdir("Data/" + saveName)
+
+    print("Done!")
+    print("执行完成!")
+    recordLog("Done!")
+    # dataPath = os.path.abspath(os.path.join(os.getcwd(), "../Data"))
+    # with open("Data/"+saveName + '_log.txt', 'a', encoding='utf-8-sig') as file_obj:
+    #     file_obj.write(log)
+    #     file_obj.close()
+    # with open("Data/"+saveName + '.csv', 'a', encoding='utf-8-sig', newline="") as f:
+    #     f_csv = csv.writer(f)
+    #     for line in OUTPUT:
+    #         f_csv.writerow(line)
+    #     f.close()
+    saveData()
+    browser.quit()

+ 26 - 35
Extension/manifest_v3/src/background.ts

@@ -1,5 +1,5 @@
-// //此变量用于监听是否加载了新的页面(包括新窗口打开),如果是,增加变量值,用于传回后台。
-//
+//此变量用于监听是否加载了新的页面(包括新窗口打开),如果是,增加变量值,用于传回后台。
+
 // var tabList = []; //用来记录打开的新的tab的id
 // var nowTabId = null;
 // var nowTabIndex = 0; //重要变量!!
@@ -49,36 +49,27 @@
 //         ws.send(JSON.stringify(message));
 //     }
 // });
-// let testValue = Math.floor(Math.random() * (99999999)).toString();
-// // 打开一个 web socket
-// let ws = new WebSocket("ws://localhost:8084");
-// ws.onopen = function() {
-//     // Web Socket 已连接上,使用 send() 方法发送数据
-//     console.log("已连接");
-//     let message = {
-//         type: 0, //消息类型,0代表链接操作
-//         message: {
-//             id: 0, //socket id
-//             testValue: testValue,
-//         }
-//     };
-//     this.send(JSON.stringify(message));
-// };
-// ws.onmessage = function(evt) {
-//     evt = JSON.parse(evt.data);
-//     if (evt["type"] == "0") { //0代表更新参数添加索引值
-//         chrome.storage.local.set({ "parameterNum": parseInt(evt["value"]) }); //修改值
-//     }
-// };
-// ws.onclose = function() {
-//     // 关闭 websocket
-//     console.log("连接已关闭...");
-//     let message = {
-//         type: 500, //消息类型,0代表链接操作
-//         message: {
-//             id: 0, //socket id
-//             testValue: testValue,
-//         }
-//     };
-//     this.send(JSON.stringify(message));
-// };
+
+// 打开一个 web socket
+let ws = new WebSocket("ws://localhost:8084");
+ws.onopen = function() {
+    // Web Socket 已连接上,使用 send() 方法发送数据
+    console.log("已连接");
+    let message = {
+        type: 0, //消息类型,0代表链接操作
+        message: {
+            id: 0, //socket id
+        }
+    };
+    this.send(JSON.stringify(message));
+};
+ws.onmessage = function(evt) {
+    evt = JSON.parse(evt.data);
+    if (evt["type"] == "0") { //0代表更新参数添加索引值
+        chrome.storage.local.set({ "parameterNum": parseInt(evt["value"]) }); //修改值
+    }
+};
+ws.onclose = function() {
+    // 关闭 websocket
+    console.log("连接已关闭...");
+};

+ 2 - 0
Readme.md

@@ -55,6 +55,8 @@ Bilibili/B站视频教程:
 
 [如何切换IP池和使用隧道IP - 打开详情页采集案例](https://www.bilibili.com/video/BV1KT411t79n)
 
+[如何同时执行多个任务(并行多开)](https://www.bilibili.com/video/BV13c411G7LE/)
+
 
 Refer to [Youtube Playlist](https://youtube.com/playlist?list=PL0kEFEkWrT7mt9MUlEBV2DTo1QsaanUTp) to see the video tutorials of EasySpider.
 

+ 16 - 12
Releases/EasySpider_windows_amd64/V0.3.1 新特性.txt

@@ -6,6 +6,8 @@ B站最新版特性视频已上传,新视频非常有用,推荐大家观看
 
 [【重要】自定义条件判断之使用循环项内的JS命令返回值 - 第二弹](https://www.bilibili.com/video/BV1mu411x7Nn/)
 
+[如何同时执行多个任务(并行多开)](https://www.bilibili.com/video/BV13c411G7LE/)
+
 [如何执行自己写的JS代码和系统代码 (自定义操作)](https://www.bilibili.com/video/BV1qs4y1z7Hc/)
 
 [如何自定义循环和判断条件 - 第一弹](https://www.bilibili.com/video/BV1Ys4y1z777/)
@@ -45,15 +47,17 @@ B站最新版特性视频已上传,新视频非常有用,推荐大家观看
 10. 大幅增加使用提示和说明,使软件更易用(如增加了iframe标签的处理方式说明,各个选项的参数意义,以及循环项XPath的修改说明等等)。
 11. 执行命令时增加了如何用命令行执行任务的提示:[https://github.com/NaiboWang/EasySpider/wiki/Argument-Instruction](https://github.com/NaiboWang/EasySpider/wiki/Argument-Instruction)。
 ![image](https://github.com/NaiboWang/EasySpider/assets/30287768/a9e774df-e345-4d51-b7c9-2c4dac0ec624)
-12. 增加无头模式,即无浏览器界面模式配置。
-13. 修复了使用用户配置浏览器模式下的中文路径不能正确识别的问题。
-14. 修复了条件分支没有无条件分支时会卡死的问题。
-15. 修复了保存任务后会输入框卡死的问题。
-16. 打开网页操作和点击元素操作新增设置页面最长加载等待时间。
-17. 增加了鼠标移动到元素功能。
-18. 找不到元素时会提示。
-19. 修复网页滚动Bug。
-20. 任务名称初始化为第一次进入页面的标题值。
-21. 增加版本更新提示。
-22. 应要求增加出品方信息。
-23. 更新chrome版本为113。
+12. 增加并行多开模式。
+13. 增加无头模式,即无浏览器界面模式配置。
+14. 修复了使用用户配置浏览器模式下的中文路径不能正确识别的问题。
+15. 修复了条件分支没有无条件分支时会卡死的问题。
+16. 修复了保存任务后会输入框卡死的问题。
+17. 打开网页操作和点击元素操作新增设置页面最长加载等待时间。
+18. 增加了鼠标移动到元素功能。
+19. 找不到元素时会提示。
+20. 修复网页滚动Bug。
+21. 增加新增提取数据字段操作。
+22. 任务名称初始化为第一次进入页面的标题值。
+23. 增加版本更新提示。
+24. 应要求增加出品方信息。
+25. 更新chrome版本为113。

+ 1 - 1
Releases/EasySpider_windows_amd64/config.json

@@ -1 +1 @@
-{"webserver_address":"http://localhost","webserver_port":8074,"user_data_folder":"./user_data","absolute_user_data_folder":"D:\\Documents\\Projects\\EasySpider\\Releases\\EasySpider_windows_amd64\\user_data"}
+{"webserver_address":"http://localhost","webserver_port":8074,"user_data_folder":"./user_data2","absolute_user_data_folder":"D:\\Documents\\Projects\\EasySpider\\Releases\\EasySpider_windows_amd64\\user_data2"}

+ 1 - 0
Releases/EasySpider_windows_amd64/config2.json

@@ -0,0 +1 @@
+{"webserver_address":"http://localhost","webserver_port":8074,"user_data_folder":"./user_data2","absolute_user_data_folder":"D:\\Documents\\Projects\\EasySpider\\Releases\\EasySpider_windows_amd64\\user_data2"}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/0.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/1.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/10.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/11.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/12.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/13.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/14.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/15.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/16.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/17.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/18.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/19.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/2.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/20.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/21.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/22.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/23.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/24.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/25.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/26.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/27.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/28.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/29.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/3.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/30.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/31.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/32.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/33.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/34.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/35.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/36.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/37.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/38.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/39.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/4.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/40.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/41.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/42.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/43.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/44.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/45.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/46.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/47.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/48.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/49.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/5.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/50.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/51.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/52.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/6.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/7.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/8.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/execution_instances/9.json


+ 3 - 0
Releases/EasySpider_windows_amd64/readme.txt

@@ -1,3 +1,6 @@
+视频教程:https://www.bilibili.com/video/BV1Fk4y1L7xX/
+Video Tutorial: https://youtube.com/playlist?list=PL0kEFEkWrT7mt9MUlEBV2DTo1QsaanUTp
+
 这个软件绝对不是特洛伊木马/病毒!如果被像 Windows Defender 这样的杀毒软件误认为是病毒,请进行恢复,或者打开“EasySpider.bat”来运行我们的软件。
 
 The software is totally not trojan/virus! If mistaken by antivirus software such as windows defender as a virus, please recover it, or open "EasySpider.bat" to run our software instead.

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/tasks/4.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/tasks/5.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/tasks/6.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
Releases/EasySpider_windows_amd64/tasks/7.json


Vissa filer visades inte eftersom för många filer har ändrats