1

Тема: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Добрый день. На странице есть Х ссылок, хочу их вынимать.
Сейчас получаю .length и потом в цикле по одному их вынимаю:

numEl := PageInst.Evaluate("document.getElementsByClassName('classname').length;").Value
Loop %numEl%
{
	El := PageInst.Evaluate("document.getElementsByClassName('classname')[" . A_Index - 1 . "].href;").Value
	[еще какой то код]
}

В консоли хрома можно сделать вот так:

a = document.getElementsByClassName('classname')
a[0].href

И консоль выводит ссылку первого элемента.
Можно ли это "a" как то вывести в переменную ahk?
PS просто интересно, можно ли как то еще мой скрипт доработать, если нет - то нет.

2

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Вы можете делать инжекты кода любой сложности, а не только разовые запросы к контексту страницы. Например, опишите функцией, или связкой функций, обработку данных на стороне браузера, а клиентским кодом, вызывайте их и обрабатывайте присланные результаты. Кроме того, для большего удобства можно зарегистрировать обработчик сообщений, передав имя этой функции-обработчика последним параметром методу "GetPage()" и выполнив активацию домена консоли Console.enable для инстанса, возвращённого этим методом. После чего, "наладить общение" с браузером, через консоль. В своих поделках пользую библиотеку JSON.ahk от teadrinker(которую, почему-то до сих пор не разместили в коллекции ) и, например, чтобы получить на стороне AHK похожую лексику, как у Вас в примере, потребуется инжект такого кода:


function getElements(className) {
	let a = document.getElementsByClassName(className);
	console.log(JSON.stringify({'destination': 'getElements', 'msg': a}));
}

А для обработки, что-то вроде:


MyMsgFunc(data) {
	if (data.method == "Console.messageAdded") {
		m := JSON.Parse(data.params.message.text)
		if (m.destination == "getElements") {
			MsgBox % m.msg[1].href
		}
	}
}

Имя которой нужно зарегистрировать в качестве обработчика. После этого, PageInst.Evaluate("getElements(""className"")"), совершит вызов функции на странице, отправив в консоль результат вызова, а обработчик-функция на стороне AHK покажет поле "href", первого элемента массива.

3

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

KusochekDobra
Спасибо за информацию. Попробую разобраться.

4

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

KusochekDobra пишет:

которую, почему-то до сих пор не разместили в коллекции

Так там моя только компоновка. Вообще если пользоваться библиотекой Chrome.ahk, то дополнительные средства для обработки JSON не нужны, там уже есть встроенные методы Jxon_Load() и Jxon_Dump().

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

5

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Учитывая, что последняя версия уже не включает в себя, но требует "Jxon.ahk" - это очевидно, но я использую Вашу "компоновку" в своих проектах и стараюсь не изменять хорошим привычкам.

Считаю, что было бы делом правильным, добавить сей труд в список прочих полезных ссылок, чтобы иметь возможность не искать его поиском по форуму при необходимости ссылаться на него в любом ином случае и, чтобы другим участникам была доступна такая альтернатива в их творческом поиске.

Плохого, Вы, этим, точно не сделаете.

6

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

KusochekDobra пишет:

последняя версия уже не включает в себя, но требует "Jxon.ahk"

Не совсем понял, включает же. Или это не последняя?

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

7

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

KusochekDobra пишет:

Считаю, что было бы делом правильным, добавить сей труд в список прочих полезных ссылок, чтобы иметь возможность не искать его поиском по форуму при необходимости ссылаться на него в любом ином случае и, чтобы другим участникам была доступна такая альтернатива в их творческом поиске.

Тогда уже добавлять и вариант без постоянного создавания файла.
ИМХО это не совсем правильно.

8

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker, тут смотрел.
https://i.ibb.co/sKFCqxd/jx.png

Malcev, я не настаиваю. ПМСМ, это весьма хорошее исполнение для такой узкой задачи. Кроме этого, не возьмусь искать ссылку, но, кажется именно Вы в одном из обсуждений давали пояснение к "Jxon.ahk", в котором вычисления производятся не корректно, чего лишена "компоновка" от teadrinker. Я лишь говорю, что это является решением, которое пользуется популярностью(по крайней мере у меня), а раз так, то ему может найтись место среди других материалов, приносящих пользу, а не просто затеряться на просторах форума, только потому лишь, что оно не совсем отвечает ожиданиям. Да и наверняка, в образованной голове teadrenker'a найдутся корректировки для своей "компоновки", чтобы она больше отвечала требованиям, если бы оказалась "на видном месте".

9

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

KusochekDobra пишет:

teadrinker, тут смотрел

А, ну это же не позиционируется, как релиз, и ссылок на эти библиотеки нет. Кроме того, в Jxon.ahk нет методов Jxon_True() и Jxon_False().

KusochekDobra пишет:

Вы можете делать инжекты кода любой сложности, а не только разовые запросы к контексту страницы ... для большего удобства можно зарегистрировать обработчик сообщений

Сам недавно пользовался выводом в консоль и в буфер обмена, но как раз для случаев «разовых запросов» всё оказалось проще. Приведу пример получения ссылок с текстом "AutoHotkey" с главной страницы форума, на всякий случай со своей версией класса Chrome.

oChrome := new Chrome(A_Temp, "http://forum.script-coding.com/")
oPage := oChrome.GetActivePage()
OnExit( exitFunc := Func("CloseChrome").Bind(oPage, oChrome.PID) )
timer := Func("WatchChrome").Bind(oChrome.PID, exitFunc)
SetTimer, % timer, 500
oPage.WaitForLoad() ; чаще всего недостаточно, учтём это в js-скрипте
Loop  {
   Sleep, 50
   res := oPage.Evaluate( JScript() )
} until res.value != ""
MsgBox, % res.value
ExitApp

JScript()  {   ; js-скриптом ищем ссылки с текстом 'AutoHotkey', результат будет в переменной result
   script =
   (
   {
      const links = document.links;
      let result = '';
      if (links.length > 10)  {                       // если ссылок больше 10, значит страница загрузилась
         for (let i = 0; i < links.length; i++)  {
            if (links[i].innerText === 'AutoHotkey')
               result += (result === '' ? '' : '\n') + links[i].href;
         }
         result = !result ? 0 : result;               // если ничего не найдено, запишем 0
      }
      result;  // здесь результат, который будет возвращён
   }
   )
   Return script
}

WatchChrome(ChromePID, onExitFunc)  {
   Process, Exist, % ChromePID
   if !ErrorLevel  {
      OnExit(onExitFunc, 0)
      ExitApp
   }
}

CloseChrome(oPage, PID)  {
   pTimerFunc := RegisterCallback( "ExitFunc", "F", 0, pObj := Object(arr := [PID]) )
   DllCall("SetTimer", Ptr, A_ScriptHwnd, Ptr, id := 1, UInt, 1000, Ptr, pTimerFunc)
   ObjRelease(pObj)
   oPage.Call("Browser.close")
}

ExitFunc()  {
   DllCall("KillTimer", Ptr, A_ScriptHwnd, Ptr, 1)
   obj := Object(A_EventInfo)
   Process, Exist, % obj[1]
   if ErrorLevel
      Process, Close, % obj[1]
   ExitApp
}

class Chrome
{
   static DebugPort := 9222
   
   /*
      Escape a string in a manner suitable for command line parameters
   */
   CliEscape(Param)
   {
      return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """"
   }
   
   /*
      Finds instances of chrome in debug mode and the ports they're running
      on. If no instances are found, returns a false value. If one or more
      instances are found, returns an associative array where the keys are
      the ports, and the values are the full command line texts used to start
      the processes.
      
      One example of how this may be used would be to open chrome on a
      different port if an instance of chrome is already open on the port
      you wanted to used.
      
      ```
      ; If the wanted port is taken, use the largest taken port plus one
      DebugPort := 9222
      if (Chromes := Chrome.FindInstances()).HasKey(DebugPort)
         DebugPort := Chromes.MaxIndex() + 1
      ChromeInst := new Chrome(ProfilePath,,,, DebugPort)
      ```
      
      Another use would be to scan for running instances and attach to one
      instead of starting a new instance.
      
      ```
      if (Chromes := Chrome.FindInstances())
         ChromeInst := {"base": Chrome, "DebugPort": Chromes.MinIndex()}
      else
         ChromeInst := new Chrome(ProfilePath)
      ```
   */
   FindInstances()
   {
      static Needle := "--remote-debugging-port=(\d+)"
      Out := {}
      for Item in ComObjGet("winmgmts:")
         .ExecQuery("SELECT CommandLine FROM Win32_Process"
         . " WHERE Name = 'chrome.exe'")
         if RegExMatch(Item.CommandLine, Needle, Match)
            Out[Match1] := Item.CommandLine
      return Out.MaxIndex() ? Out : False
   }
   
   /*
      ProfilePath - Path to the user profile directory to use. Will use the standard if left blank.
      URLs        - The page or array of pages for Chrome to load when it opens
      Flags       - Additional flags for chrome when launching
      ChromePath  - Path to chrome.exe, will detect from start menu when left blank
      DebugPort   - What port should Chrome's remote debugging server run on
   */
   __New(ProfilePath:="", URLs:="about:blank", Flags:="", ChromePath:="", DebugPort:="")
   {
      ; Verify ProfilePath
      if (ProfilePath != "" && !InStr(FileExist(ProfilePath), "D"))
         throw Exception("The given ProfilePath does not exist")
      this.ProfilePath := ProfilePath
      
      ; Verify ChromePath
      if (ChromePath == "")
         FileGetShortcut, %A_StartMenuCommon%\Programs\Google Chrome.lnk, ChromePath
      if (ChromePath == "")
         RegRead, ChromePath, HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe
      if !FileExist(ChromePath)
         throw Exception("Chrome could not be found")
      this.ChromePath := ChromePath
      
      ; Verify DebugPort
      if (DebugPort != "")
      {
         if DebugPort is not integer
            throw Exception("DebugPort must be a positive integer")
         else if (DebugPort <= 0)
            throw Exception("DebugPort must be a positive integer")
         this.DebugPort := DebugPort
      }
      
      ; Escape the URL(s)
      for Index, URL in IsObject(URLs) ? URLs : [URLs]
         URLString .= " " this.CliEscape(URL)
      
      Run, % this.CliEscape(ChromePath)
      . " --remote-debugging-port=" this.DebugPort
      . (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "")
      . (Flags ? " " Flags : "")    ; --headless to start hidden
      . URLString
      ,,, OutputVarPID
      this.PID := OutputVarPID
   }
   
   /*
      End Chrome by terminating the process.
   */
   Kill()
   {
      Process, Close, % this.PID
   }
   
   /*
      Queries chrome for a list of pages that expose a debug interface.
      In addition to standard tabs, these include pages such as extension
      configuration pages.
   */
   GetPageList()
   {
      http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
      http.open("GET", "http://127.0.0.1:" this.DebugPort "/json")
      http.send()
      return this.Jxon_Load(http.responseText)
   }
   
   GetActivePage(fnCallback := "")  {
      Return new this.Page( this.GetPageList()[1, "webSocketDebuggerUrl"], fnCallback )
   }
   
   /*
      Returns a connection to the debug interface of a page that matches the
      provided criteria. When multiple pages match the criteria, they appear
      ordered by how recently the pages were opened.
      
      Key        - The key from the page list to search for, such as "url" or "title"
      Value      - The value to search for in the provided key
      MatchMode  - What kind of search to use, such as "exact", "contains", "startswith", or "regex"
      Index      - If multiple pages match the given criteria, which one of them to return
      fnCallback - A function to be called whenever message is received from the page
   */
   GetPageBy(Key, Value, MatchMode:="exact", Index:=1, fnCallback:="")
   {
      Count := 0
      for n, PageData in this.GetPageList()
      {
         if (((MatchMode = "exact" && PageData[Key] = Value) ; Case insensitive
            || (MatchMode = "contains" && InStr(PageData[Key], Value))
            || (MatchMode = "startswith" && InStr(PageData[Key], Value) == 1)
            || (MatchMode = "regex" && PageData[Key] ~= Value))
            && ++Count == Index)
            return new this.Page(PageData.webSocketDebuggerUrl, fnCallback)
      }
   }
   
   /*
      Shorthand for GetPageBy("url", Value, "startswith")
   */
   GetPageByURL(Value, MatchMode:="startswith", Index:=1, fnCallback:="")
   {
      return this.GetPageBy("url", Value, MatchMode, Index, fnCallback)
   }
   
   /*
      Shorthand for GetPageBy("title", Value, "startswith")
   */
   GetPageByTitle(Value, MatchMode:="startswith", Index:=1, fnCallback:="")
   {
      return this.GetPageBy("title", Value, MatchMode, Index, fnCallback)
   }
   
   /*
      Shorthand for GetPageBy("type", Type, "exact")
      
      The default type to search for is "page", which is the visible area of
      a normal Chrome tab.
   */
   GetPage(Index:=1, Type:="page", fnCallback:="")
   {
      return this.GetPageBy("type", Type, "exact", Index, fnCallback)
   }
   
   /*
      Connects to the debug interface of a page given its WebSocket URL.
   */
   class Page
   {
      Connected := False
      ID := 0
      Responses := []
      
      /*
         wsurl      - The desired page's WebSocket URL
         fnCallback - A function to be called whenever message is received
      */
      __New(wsurl, fnCallback:="")
      {
         this.fnCallback := fnCallback
         this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False)
         
         ; TODO: Throw exception on invalid objects
         if IsObject(wsurl)
            wsurl := wsurl.webSocketDebuggerUrl
         
         wsurl := StrReplace(wsurl, "localhost", "127.0.0.1")
         this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this}
         this.ws.__New(wsurl)
         
         while !this.Connected
            Sleep, 50
      }
      
      /*
         Calls the specified endpoint and provides it with the given
         parameters.
         
         DomainAndMethod - The endpoint domain and method name for the
         endpoint you would like to call. For example:
         PageInst.Call("Browser.close")
         PageInst.Call("Schema.getDomains")
         
         Params - An associative array of parameters to be provided to the
         endpoint. For example:
         PageInst.Call("Page.printToPDF", {"scale": 0.5 ; Numeric Value
         , "landscape": Chrome.Jxon_True() ; Boolean Value
         , "pageRanges: "1-5, 8, 11-13"}) ; String value
         PageInst.Call("Page.navigate", {"url": "https://autohotkey.com/"})
         
         WaitForResponse - Whether to block until a response is received from
         Chrome, which is necessary to receive a return value, or whether
         to continue on with the script without waiting for a response.
      */
      Call(DomainAndMethod, Params:="", WaitForResponse:=True)
      {
         if !this.Connected
            throw Exception("Not connected to tab")
         
         ; Use a temporary variable for ID in case more calls are made
         ; before we receive a response.
         ID := this.ID += 1
         this.ws.Send(Chrome.Jxon_Dump({"id": ID
         , "params": Params ? Params : {}
         , "method": DomainAndMethod}))
         
         if !WaitForResponse
            return
         
         ; Wait for the response
         this.responses[ID] := False
         while !this.responses[ID]
            Sleep, 50
         
         ; Get the response, check if it's an error
         response := this.responses.Delete(ID)
         if (response.error)
            throw Exception("Chrome indicated error in response",, Chrome.Jxon_Dump(response.error))
         
         return response.result
      }
      
      /*
         Run some JavaScript on the page. For example:
         
         PageInst.Evaluate("alert(""I can't believe it's not IE!"");")
         PageInst.Evaluate("document.getElementsByTagName('button')[0].click();")
      */
      Evaluate(JS)
      {
         try response := this.Call("Runtime.evaluate",
         ( LTrim Join
         {
            "expression": JS,
            "objectGroup": "console",
            "includeCommandLineAPI": Chrome.Jxon_True(),
            "silent": Chrome.Jxon_False(),
            "returnByValue": Chrome.Jxon_False(),
            "userGesture": Chrome.Jxon_True(),
            "awaitPromise": Chrome.Jxon_False()
         }
         ))
         
         if (response.exceptionDetails)
            throw Exception(response.result.description,, Chrome.Jxon_Dump(response.exceptionDetails))
         
         return response.result
      }
      
      /*
         Waits for the page's readyState to match the DesiredState.
         
         DesiredState - The state to wait for the page's ReadyState to match
         Interval     - How often it should check whether the state matches
      */
      WaitForLoad(DesiredState:="complete", Interval:=100)
      {
         while this.Evaluate("document.readyState").value != DesiredState
            Sleep, Interval
      }
      
      /*
         Internal function triggered when the script receives a message on
         the WebSocket connected to the page.
      */
      Event(EventName, Event)
      {
         ; If it was called from the WebSocket adjust the class context
         if this.Parent
            this := this.Parent
         
         ; TODO: Handle Error events
         if (EventName == "Open")
         {
            this.Connected := True
            BoundKeepAlive := this.BoundKeepAlive
            SetTimer, %BoundKeepAlive%, 15000
         }
         else if (EventName == "Message")
         {
            data := Chrome.Jxon_Load(Event.data)
            
            ; Run the callback routine
            fnCallback := this.fnCallback
            if (newData := %fnCallback%(data))
               data := newData
            
            if this.responses.HasKey(data.ID)
               this.responses[data.ID] := data
         }
         else if (EventName == "Close")
         {
            this.Disconnect()
         }
         else if (EventName == "Error")
         {
            throw Exception("Websocket Error!")
         }
      }
      
      /*
         Disconnect from the page's debug interface, allowing the instance
         to be garbage collected.
         
         This method should always be called when you are finished with a
         page or else your script will leak memory.
      */
      Disconnect()
      {
         if !this.Connected
            return
         
         this.Connected := False
         this.ws.Delete("Parent")
         this.ws.Disconnect()
         
         BoundKeepAlive := this.BoundKeepAlive
         SetTimer, %BoundKeepAlive%, Delete
         this.Delete("BoundKeepAlive")
      }
      
      class WebSocket
      {
         __New(WS_URL)
         {
            static wb
            
            ; Create an IE instance
            Gui, +hWndhOld
            Gui, New, +hWndhWnd
            this.hWnd := hWnd
            Gui, Add, ActiveX, vWB, Shell.Explorer
            WB.Silent := true
            Gui, %hOld%: Default
            
            ; Write an appropriate document
            WB.Navigate("about:<!DOCTYPE html><meta http-equiv='X-UA-Compatible'"
            . "content='IE=edge'><body></body>")
            while (WB.ReadyState < 4)
               sleep, 50
            this.document := WB.document
            
            ; Add our handlers to the JavaScript namespace
            this.document.parentWindow.ahk_savews := this._SaveWS.Bind(this)
            this.document.parentWindow.ahk_event := this._Event.Bind(this)
            this.document.parentWindow.ahk_ws_url := WS_URL
            
            ; Add some JavaScript to the page to open a socket
            Script := this.document.createElement("script")
            Script.text := "ws = new WebSocket(ahk_ws_url);`n"
            . "ws.onopen = function(event){ ahk_event('Open', event); };`n"
            . "ws.onclose = function(event){ ahk_event('Close', event); };`n"
            . "ws.onerror = function(event){ ahk_event('Error', event); };`n"
            . "ws.onmessage = function(event){ ahk_event('Message', event); };"
            this.document.body.appendChild(Script)
         }
         
         ; Called by the JS in response to WS events
         _Event(EventName, Event)
         {
            this["On" EventName](Event)
         }
         
         ; Sends data through the WebSocket
         Send(Data)
         {
            this.document.parentWindow.ws.send(Data)
         }
         
         ; Closes the WebSocket connection
         Close(Code:=1000, Reason:="")
         {
            this.document.parentWindow.ws.close(Code, Reason)
         }
         
         ; Closes and deletes the WebSocket, removing
         ; references so the class can be garbage collected
         Disconnect()
         {
            if this.hWnd
            {
               this.Close()
               Gui, % this.hWnd ": Destroy"
               this.hWnd := False
            }
         }
      }
   }
   
   Jxon_Load(ByRef src, args*)
   {
      static q := Chr(34)
      
      key := "", is_key := false
      stack := [ tree := [] ]
      is_arr := { (tree): 1 }
      next := q . "{[01234567890-tfn"
      pos := 0
      while ( (ch := SubStr(src, ++pos, 1)) != "" )
      {
         if InStr(" `t`n`r", ch)
            continue
         if !InStr(next, ch, true)
         {
            ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
            col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))
            
            msg := Format("{}: line {} col {} (char {})"
            ,   (next == "")      ? ["Extra data", ch := SubStr(src, pos)][1]
            : (next == "'")     ? "Unterminated string starting at"
            : (next == "\")     ? "Invalid \escape"
            : (next == ":")     ? "Expecting ':' delimiter"
            : (next == q)       ? "Expecting object key enclosed in double quotes"
            : (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
            : (next == ",}")    ? "Expecting ',' delimiter or object closing '}'"
            : (next == ",]")    ? "Expecting ',' delimiter or array closing ']'"
            : [ "Expecting JSON value(string, number, [true, false, null], object or array)"
            , ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
            , ln, col, pos)
            
            throw Exception(msg, -1, ch)
         }
         
         is_array := is_arr[obj := stack[1]]
         
         if i := InStr("{[", ch)
         {
            val := (proto := args[i]) ? new proto : {}
            is_array? ObjPush(obj, val) : obj[key] := val
            ObjInsertAt(stack, 1, val)
            
            is_arr[val] := !(is_key := ch == "{")
            next := q . (is_key ? "}" : "{[]0123456789-tfn")
         }
         
         else if InStr("}]", ch)
         {
            ObjRemoveAt(stack, 1)
            next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
         }
         
         else if InStr(",:", ch)
         {
            is_key := (!is_array && ch == ",")
            next := is_key ? q : q . "{[0123456789-tfn"
         }
         
         else ; string | number | true | false | null
         {
            if (ch == q) ; string
            {
               i := pos
               while i := InStr(src, q,, i+1)
               {
                  val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
                  static end := A_AhkVersion<"2" ? 0 : -1
                  if (SubStr(val, end) != "\")
                     break
               }
               if !i ? (pos--, next := "'") : 0
                  continue
               
               pos := i ; update pos
               
               val := StrReplace(val,    "\/",  "/")
               , val := StrReplace(val, "\" . q,    q)
               , val := StrReplace(val,    "\b", "`b")
               , val := StrReplace(val,    "\f", "`f")
               , val := StrReplace(val,    "\n", "`n")
               , val := StrReplace(val,    "\r", "`r")
               , val := StrReplace(val,    "\t", "`t")
               
               i := 0
               while i := InStr(val, "\",, i+1)
               {
                  if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
                     continue 2
                  
                  ; \uXXXX - JSON unicode escape sequence
                  xxxx := Abs("0x" . SubStr(val, i+2, 4))
                  if (A_IsUnicode || xxxx < 0x100)
                     val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
               }
               
               if is_key
               {
                  key := val, next := ":"
                  continue
               }
            }
            
            else ; number | true | false | null
            {
               val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
               
               ; For numerical values, numerify integers and keep floats as is.
               ; I'm not yet sure if I should numerify floats in v2.0-a ...
               static number := "number", integer := "integer"
               if val is %number%
               {
                  if val is %integer%
                     val += 0
               }
               ; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
               ; SOMETIMES return strings due to certain optimizations. Since it
               ; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
               else if (val == "true" || val == "false")
                  val := %value% + 0
               ; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
               ; as it would raise an exception in AHK_H(overriding built-in var)
               else if (val == "null")
                  val := ""
               ; any other values are invalid, continue to trigger error
               else if (pos--, next := "#")
                  continue
               
               pos += i-1
            }
            
            is_array? ObjPush(obj, val) : obj[key] := val
            next := obj==tree ? "" : is_array ? ",]" : ",}"
         }
      }
      
      return tree[1]
   }
   
   Jxon_Dump(obj, indent:="", lvl:=1)
   {
      static q := Chr(34)
      
      if IsObject(obj)
      {
         static Type := Func("Type")
         if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "")
            throw Exception("Object type not supported.", -1, Format("<Object at 0x{:p}>", &obj))
         
         prefix := SubStr(A_ThisFunc, 1, InStr(A_ThisFunc, ".",, 0))
         fn_t := prefix "Jxon_True",  obj_t := this ? %fn_t%(this) : %fn_t%()
         fn_f := prefix "Jxon_False", obj_f := this ? %fn_f%(this) : %fn_f%()
         
         if (&obj == &obj_t)
            return "true"
         else if (&obj == &obj_f)
            return "false"
         
         is_array := 0
         for k in obj
            is_array := k == A_Index
         until !is_array
         
         static integer := "integer"
         if indent is %integer%
         {
            if (indent < 0)
               throw Exception("Indent parameter must be a postive integer.", -1, indent)
            spaces := indent, indent := ""
            Loop % spaces
               indent .= " "
         }
         indt := ""
         Loop, % indent ? lvl : 0
            indt .= indent
         
         this_fn := this ? Func(A_ThisFunc).Bind(this) : A_ThisFunc
         lvl += 1, out := "" ; Make #Warn happy
         for k, v in obj
         {
            if IsObject(k) || (k == "")
               throw Exception("Invalid object key.", -1, k ? Format("<Object at 0x{:p}>", &obj) : "<blank>")
            
            if !is_array
               out .= ( ObjGetCapacity([k], 1) ? %this_fn%(k) : q . k . q ) ;// key
            .  ( indent ? ": " : ":" ) ; token + padding
            out .= %this_fn%(v, indent, lvl) ; value
            .  ( indent ? ",`n" . indt : "," ) ; token + indent
         }
         
         if (out != "")
         {
            out := Trim(out, ",`n" . indent)
            if (indent != "")
               out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1)
         }
         
         return is_array ? "[" . out . "]" : "{" . out . "}"
      }
      
      ; Number
      else if (ObjGetCapacity([obj], 1) == "")
         return obj
      
      ; String (null -> not supported by AHK)
      if (obj != "")
      {
         obj := StrReplace(obj,  "\",    "\\")
         , obj := StrReplace(obj,  "/",    "\/")
         , obj := StrReplace(obj,    q, "\" . q)
         , obj := StrReplace(obj, "`b",    "\b")
         , obj := StrReplace(obj, "`f",    "\f")
         , obj := StrReplace(obj, "`n",    "\n")
         , obj := StrReplace(obj, "`r",    "\r")
         , obj := StrReplace(obj, "`t",    "\t")
         
         static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]"
         while RegExMatch(obj, needle, m)
            obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0])))
      }
      
      return q . obj . q
   }
   
   Jxon_True()
   {
      static obj := {}
      return obj
   }
   
   Jxon_False()
   {
      static obj := {}
      return obj
   }
}
Malcev пишет:

Тогда уже добавлять и вариант без постоянного создавания файла.
ИМХО это не совсем правильно.

Почему? Можно сделать, чтобы файл один раз за время работы скрипта создавался. Но ничего страшного, что в папке Temp периодически создаётся маленький текстовый файлик.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

10

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker пишет:

Можно сделать, чтобы файл один раз за время работы скрипта создавался

Можно. Но у тебя это нигде не указано, а я по себе знаю, что не все будут изучать, как библиотека работает если она работает.

teadrinker пишет:

ничего страшного, что в папке Temp периодически создаётся маленький текстовый файлик

Для некоторых ssd хардов может быть неприятно:

у меня диск на контролере JMicron, то есть нет кеша и «если нужно было изменить в случайном месте 4кб данных, им приходилось каждый раз стирать целый блок 64-512кб

https://habr.com/post/96896/
Да и наверняка медленнее будет работать из-за постоянного создания-стирания файла.
Хотя использование невстроенного парсинга JSON это уже и так большой тормоз по сравнения с Selenium или объектной моделью IE.
Ну и зачем создавать файл, когда есть библиотека ActiveScript?

11

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Malcev пишет:

Для некоторых ssd хардов может быть неприятно

Там статья 2010 года, и, по-моему, речь идёт о браке.

Malcev пишет:

это уже и так большой тормоз

Для задач, где это применяется, скорость не критична, и время, которое тратится на парсинг, несравнимо меньше времени, потраченного на загрузку данных из сети.

Malcev пишет:

Ну и зачем создавать файл, когда есть библиотека ActiveScript?

Тут ответ очевиден — компактность кода. Ну и файл вовсе не обязательно создавать, можно просто один раз сохранить на диске.

А вообще если речь идёт о Хроме, JSON лучше парсить нативными методами js, да и вообще все ресурсоёмкие операции можно выполнять силами движка Хрома.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

12 (изменено: Malcev, 2018-12-18 14:44:47)

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker пишет:

Там статья 2010 года, и, по-моему, речь идёт о браке.

Моему ssd уже 7 лет, так что они почти ровесники.
JMicron или нет, не знаю, но по-любому стараюсь его зазря не дырявить.

teadrinker пишет:

Для задач, где это применяется, скорость не критична, и время, которое тратится на парсинг, несравнимо меньше времени, потраченного на загрузку данных из сети.

Не согласен. Например нам надо обнаружить изменение страницы и как только она изменится подать сигнал.
Сравни скорости:

oIE := ComObjCreate("InternetExplorer.Application")
oIE.visible := True
oIE.navigate("http://delfi.lv")
While oIE.readyState != 4 || oIE.document.readyState != "complete" || oIE.busy  
   sleep 10
a := a_tickcount
OuterHTML := oIE.Document.documentElement.OuterHtml
msgbox % a_tickcount - a

и

ChromeInst := new Chrome()
PageInst := ChromeInst.GetPage()
PageInst.Call("Page.navigate", {"url": "http://delfi.lv"})
PageInst.WaitForLoad()
a := a_tickcount
OuterHTML := PageInst.Evaluate("document.documentElement.outerHTML;").Value
msgbox % a_tickcount - a
teadrinker пишет:

Тут ответ очевиден — компактность кода

Поэтому я и предложил если публиковать, то публиковать 2 варианта.
Так как для одних приоритет - компактность кода, для других - выполнение кода без создания лишних файлов.
Ну или если под Active script переделывать не хочется, то внести комментарии по поводу  создания файла один раз и сохранения его на диске.

13 (изменено: teadrinker, 2018-12-18 15:47:59)

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Malcev пишет:

Сравни скорости

Точно не знаю, что там происходит, но просто сравни дальше:

ChromeInst := new Chrome(A_Temp)
PageInst := ChromeInst.GetPage()
PageInst.Call("Page.navigate", {"url": "http://delfi.lv"})
PageInst.WaitForLoad()

a := a_tickcount
res := PageInst.Evaluate("document.documentElement.outerHTML;")
msgbox % a_tickcount - a
MsgBox, % IsObject(res)

a := a_tickcount
str := JSON.Stringify(res)
msgbox % a_tickcount - a
MsgBox, % str

a := a_tickcount
obj := JSON.Parse(str)
msgbox % a_tickcount - a
MsgBox, % obj.value

class JSON
{
   static JS := JSON._GetJScripObject()
   
   Parse(JsonString)  {
      try oJSON := this.JS.("(" JsonString ")")
      catch  {
         MsgBox, Wrong JsonString!
         Return
      }
      Return this._CreateObject(oJSON)
   }
   
   Stringify(obj)  {
      if IsObject( obj )  {
         isArray := true
         for key in obj
            if !( key = A_Index || isArray := false )
               break
            
         for k, v in obj
            str .= ( A_Index = 1 ? "" : "," ) . ( isArray ? "" : this.Stringify(k) . ":" ) . this.Stringify(v)

         return isArray ? "[" str "]" : "{" str "}"
      }
      else if !(obj*1 = "" || RegExMatch(obj, "\s"))
         return obj
      
      for k, v in [["\", "\\"], [A_Tab, "\t"], ["""", "\"""], ["/", "\/"], ["`n", "\n"], ["`r", "\r"], [Chr(12), "\f"], [Chr(08), "\b"]]
         obj := StrReplace( obj, v[1], v[2] )
      
      while RegexMatch( obj, "[^\x20-\x7e]", key )  {
         str := Asc( key )
         val := "\u" . Chr( ( ( str >> 12 ) & 15 ) + ( ( ( str >> 12 ) & 15 ) < 10 ? 48 : 55 ) )
               . Chr( ( ( str >> 8 ) & 15 ) + ( ( ( str >> 8 ) & 15 ) < 10 ? 48 : 55 ) )
               . Chr( ( ( str >> 4 ) & 15 ) + ( ( ( str >> 4 ) & 15 ) < 10 ? 48 : 55 ) )
               . Chr( ( str & 15 ) + ( ( str & 15 ) < 10 ? 48 : 55 ) )
         obj := StrReplace(obj, key, val)
      }
      Return """" obj """"
   }
   
   GetFromUrl(url, body := "", contentType := "", userAgent := "")  {
      ; в случае удачи будет возвращена строка, в случае ошибки — массив с одним элементом-строкой с описанием ошибки
      try  {
         XmlHttp := ComObjCreate("Microsoft.XmlHttp")
         XmlHttp.Open("GET", url, false)
         ( contentType && XmlHttp.SetRequestHeader("Content-Type", contentType) )
         ( userAgent && XmlHttp.SetRequestHeader("User-Agent", userAgent) )
         XmlHttp.Send(body)
      }
      catch e
         Return ["Error!`n" . e.Message]
      status := XmlHttp.Status
      Return status = 200 ? XmlHttp.ResponseText : ["Error! Status: " . status . ", ResponseText: " . XmlHttp.ResponseText]
   }

   _GetJScripObject()  {
      VarSetCapacity(tmpFile, ((MAX_PATH := 260) - 14) << !!A_IsUnicode, 0)
      DllCall("GetTempFileName", Str, A_Temp, Str, "AHK", UInt, 0, Str, tmpFile)
      
      FileAppend,
      (
      <component>
      <public><method name='eval'/></public>
      <script language='JScript'></script>
      </component>
      ), % tmpFile
      
      JS := ObjBindMethod( ComObjGet("script:" . tmpFile), "eval" )
      FileDelete, % tmpFile
      JSON._AddMethods(JS)
      Return JS
   }

   _AddMethods(ByRef JS)  {
      JScript =
      (
         Object.prototype.GetKeys = function () {
            var keys = []
            for (var k in this)
               if (this.hasOwnProperty(k))
                  keys.push(k)
            return keys
         }
         Object.prototype.IsArray = function () {
            var toStandardString = {}.toString
            return toStandardString.call(this) == '[object Array]'
         }
      )
      JS.("delete ActiveXObject; delete GetObject;")
      JS.(JScript)
   }

   _CreateObject(ObjJS)  {
      res := ObjJS.IsArray()
      if (res = "")
         Return ObjJS
      
      else if (res = -1)  {
         obj := []
         Loop % ObjJS.length
            obj[A_Index] := this._CreateObject(ObjJS[A_Index - 1])
      }
      else if (res = 0)  {
         obj := {}
         keys := ObjJS.GetKeys()
         Loop % keys.length
            k := keys[A_Index - 1], obj[k] := this._CreateObject(ObjJS[k])
      }
      Return obj
   }
}

Кстати, обрати внимание:

static JS := JSON._GetJScripObject()

Там файл используется только один раз.

Malcev пишет:

Поэтому я и предложил если публиковать, то публиковать 2 варианта

Ок, давай я один, ты — другой.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

14

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker пишет:

Точно не знаю, что там происходит,

Автор библиотеки сказал, что

The delay is caused by decoding chrome's response from a JSON message into an AHK object. Like with IE, the browser responds with the source immediately. However, IE responds with the text directly, and Chrome responds with a JSON blob that contains the text but must be decoded into a useful form. Because AutoHotkey does not support JSON encode/decode natively, this conversion must be done with a user-made script. That page's source is roughly 200kb, so the decoding process takes a while.

https://autohotkey.com/boards/viewtopic … 16#p218916

teadrinker пишет:

Там файл используется только один раз.

Вот это я пропустил.
Тогда ничего криминального не случится, хотя я всё-равно не люблю лишние файлы.

teadrinker пишет:

Ок, давай я один, ты — другой.

Мне парсинг JSON пока не нужен.
Если будет нужен, то возьмусь, а так лень копаться.

15

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker пишет:

А, ну это же не позиционируется, как релиз, и ссылок на эти библиотеки нет. Кроме того, в Jxon.ahk нет методов Jxon_True() и Jxon_False().

Прошу прощения за дезинформацию. В любом случае, мне было известно о встроенной поддержке JSON, но по душе Ваше творчество.

16

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Создал тему в Коллекции.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

17 (изменено: Gh0sTG0, 2023-01-22 00:22:24)

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker пишет:
ChromeInst := new Chrome(A_Temp)
PageInst := ChromeInst.GetPage()
PageInst.Call("Page.navigate", {"url": "http://delfi.lv"})
PageInst.WaitForLoad()

a := a_tickcount
res := PageInst.Evaluate("document.documentElement.outerHTML;")
msgbox % a_tickcount - a
MsgBox, % IsObject(res)

a := a_tickcount
str := JSON.Stringify(res)
msgbox % a_tickcount - a
MsgBox, % str

a := a_tickcount
obj := JSON.Parse(str)
msgbox % a_tickcount - a
MsgBox, % obj.value

class JSON
{
   static JS := JSON._GetJScripObject()
   
   Parse(JsonString)  {
      try oJSON := this.JS.("(" JsonString ")")
      catch  {
         MsgBox, Wrong JsonString!
         Return
      }
      Return this._CreateObject(oJSON)
   }
   
   Stringify(obj)  {
      if IsObject( obj )  {
         isArray := true
         for key in obj
            if !( key = A_Index || isArray := false )
               break
            
         for k, v in obj
            str .= ( A_Index = 1 ? "" : "," ) . ( isArray ? "" : this.Stringify(k) . ":" ) . this.Stringify(v)

         return isArray ? "[" str "]" : "{" str "}"
      }
      else if !(obj*1 = "" || RegExMatch(obj, "\s"))
         return obj
      
      for k, v in [["\", "\\"], [A_Tab, "\t"], ["""", "\"""], ["/", "\/"], ["`n", "\n"], ["`r", "\r"], [Chr(12), "\f"], [Chr(08), "\b"]]
         obj := StrReplace( obj, v[1], v[2] )
      
      while RegexMatch( obj, "[^\x20-\x7e]", key )  {
         str := Asc( key )
         val := "\u" . Chr( ( ( str >> 12 ) & 15 ) + ( ( ( str >> 12 ) & 15 ) < 10 ? 48 : 55 ) )
               . Chr( ( ( str >> 8 ) & 15 ) + ( ( ( str >> 8 ) & 15 ) < 10 ? 48 : 55 ) )
               . Chr( ( ( str >> 4 ) & 15 ) + ( ( ( str >> 4 ) & 15 ) < 10 ? 48 : 55 ) )
               . Chr( ( str & 15 ) + ( ( str & 15 ) < 10 ? 48 : 55 ) )
         obj := StrReplace(obj, key, val)
      }
      Return """" obj """"
   }
   
   GetFromUrl(url, body := "", contentType := "", userAgent := "")  {
      ; в случае удачи будет возвращена строка, в случае ошибки — массив с одним элементом-строкой с описанием ошибки
      try  {
         XmlHttp := ComObjCreate("Microsoft.XmlHttp")
         XmlHttp.Open("GET", url, false)
         ( contentType && XmlHttp.SetRequestHeader("Content-Type", contentType) )
         ( userAgent && XmlHttp.SetRequestHeader("User-Agent", userAgent) )
         XmlHttp.Send(body)
      }
      catch e
         Return ["Error!`n" . e.Message]
      status := XmlHttp.Status
      Return status = 200 ? XmlHttp.ResponseText : ["Error! Status: " . status . ", ResponseText: " . XmlHttp.ResponseText]
   }

   _GetJScripObject()  {
      VarSetCapacity(tmpFile, ((MAX_PATH := 260) - 14) << !!A_IsUnicode, 0)
      DllCall("GetTempFileName", Str, A_Temp, Str, "AHK", UInt, 0, Str, tmpFile)
      
      FileAppend,
      (
      <component>
      <public><method name='eval'/></public>
      <script language='JScript'></script>
      </component>
      ), % tmpFile
      
      JS := ObjBindMethod( ComObjGet("script:" . tmpFile), "eval" )
      FileDelete, % tmpFile
      JSON._AddMethods(JS)
      Return JS
   }

   _AddMethods(ByRef JS)  {
      JScript =
      (
         Object.prototype.GetKeys = function () {
            var keys = []
            for (var k in this)
               if (this.hasOwnProperty(k))
                  keys.push(k)
            return keys
         }
         Object.prototype.IsArray = function () {
            var toStandardString = {}.toString
            return toStandardString.call(this) == '[object Array]'
         }
      )
      JS.("delete ActiveXObject; delete GetObject;")
      JS.(JScript)
   }

   _CreateObject(ObjJS)  {
      res := ObjJS.IsArray()
      if (res = "")
         Return ObjJS
      
      else if (res = -1)  {
         obj := []
         Loop % ObjJS.length
            obj[A_Index] := this._CreateObject(ObjJS[A_Index - 1])
      }
      else if (res = 0)  {
         obj := {}
         keys := ObjJS.GetKeys()
         Loop % keys.length
            k := keys[A_Index - 1], obj[k] := this._CreateObject(ObjJS[k])
      }
      Return obj
   }
}

Привет. решил попробовать еще разок во всем этом разобраться.
Возник вопрос по скорости:
document.documentElement.outerHTML и должно для google.com выполняться 12 секунд? (считал сам и в msgbox'e цифра 12к+)

Update: Если поменять хтмл всего сайта на хтмл интересующего куска, то все становится гораздо быстрее.
Осталось только разобраться как его дальше парсить на ссылки, тексты и прочее.

18

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Не знаю, не пробовал. В любом случае с моей версией класса это никак не связано.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

19 (изменено: Gh0sTG0, 2023-01-22 01:09:49)

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker
Добавил Update.
Если вынимать только интересующий класс, то все становится на порядки быстрее (главная этого сайта спарсилась за ~50, почти мгновенно).
Но по прежнему немного не въезжаю как его дальше правильно парсить из состояния html текста в состояние "мой набор ссылок и текста"?

http://forum.script-coding.com/viewtopi … 783#p82783 Вот это от тов. serzh82saratov еще актуально? Или уже придуманы более оптимальные способы?

20

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Парсить можно через создание объекта HTMLFILE, как в примере, или через регулярки. В зависимоcти от того, что именно нужно получить, я бы мог привести пример.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

21 (изменено: Gh0sTG0, 2023-01-22 01:32:54)

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker
Да в принципе ничего особенного.
За пример сойдет хоть главная этого сайта. Задачи по типу получить наборами все href+innerText для class="hn" (ссылка на раздел: AutoHotKey — Games, AutoHotKey и.т.д.), к ним получить соответствующие временя+ник последнего комментатора:

document.getElementsByClassName('info-lastpost')[1].children[2].innerText

И все это упаковать в одну строку таблицы в правильном соотношении (только там не 13 пунктов, а много страниц по 50 пунктов).
Я уже типа представляю себе как это оформить достаточно прилично через ComObjCreate("HTMLFile"), но если можно эту проблему решить как то еще...

22

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Можно, получив html документа, создавать ComObjCreate("HTMLFile"), а можно, если используете Chrome.ahk, выполнять всё то же самое через джаваскрипт прямо на странице, и получать результат.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

23

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker
То есть, через джаваскрипт можно одним запросом получить, допустим, результат 5 document.getElementsByClassName быстрее, чем если сделать 5 Page.Evaluate("document.getElementsByClassName...").Value?
Хотя, через ComObjCreate уже вроде как гораздо быстрее выходит, хотя я еще экспериментирую. Вероятно скорость еще идет потому, что работаю а) с гораздо меньшим куском страницы за раз, б) с еще меньшим куском, который заLOOPливается в  работу.

24

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

Ну, используйте HTMLFILE, если вам так проще, не вижу проблемы.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

25

Re: AHK: chrome.ahk GetElementByClassName можно ли не в цикле?

teadrinker
Спасибо за ответ