[Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No IE!

Post your working scripts, libraries and tools for AHK v1.1 and older
Vaggeto
Posts: 24
Joined: 14 Dec 2020, 23:52

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by Vaggeto » 21 Dec 2020, 17:36

burque505 wrote:
21 Dec 2020, 09:06
hi @Vaggeto, for multi-line JS I prefer continuation sections (keeps me from trashing the syntax with double-quotes inline), though it appears lots of other people do not. Will something like this work for you?

Code: Select all

iframeJs =
(
var iframe = document.getElementById('iframe-id');
var test2 = iframe.contentDocument.getElementById('test2');
)
PageInstance.Evaluate(iframeJs)
Regards,
burque505
Awesome, thanks!

I also just connected it as one line without the () and that worked fine as well, but this is much more viewer friendly.
Vampirtc
Posts: 2
Joined: 08 Jan 2021, 18:02

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by Vampirtc » 08 Jan 2021, 18:12

I need help writting the following scenario:

I need to move data from one webpage to another. That part is mostly done, but I'm having problems with the following part:
Chrome needs to start when the AHK GUI loads if its not already started (dont start a new session or a new tab if its lodaded)
AHK needs to check if both pages are loaded and if I'm signed in to both (this can be checked with for example trying an url that is only accessible if you are signed in, otherwise its auto redirect to sign in page).
If either page is not loaded open that URL.
If the user is not signed in (or was signed out due to activity) either warn the user to sign in or automate sign in.
User avatar
Xeo786
Posts: 759
Joined: 09 Nov 2015, 02:43
Location: Karachi, Pakistan

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by Xeo786 » 15 Jan 2021, 10:23

done Experiment about reading table with Chrome.ahk
and I find out two ways the slower and faster

slow way

Code: Select all

table2array(page,id)
{
	;table row
	js = document.querySelector('#%id%').querySelectorAll('tr').length
	rows := page.evaluate(js).value
	;table columns
	js = document.querySelector('#%id%').querySelectorAll('tr')[0].querySelectorAll('td').length
	columns := page.evaluate(js).value
	obj := {}
	
	loop, % rows
	{
		row := a_index -1 
		loop, % columns
		{	
			col := a_index - 1
			js = document.querySelector('#%id%').querySelectorAll('tr')[%row%].querySelectorAll('td')[%col%].innerText
			obj[row,col] := page.evaluate(js).value
		}
	}
	return obj
}

fasterway

Code: Select all


table := page.evaluate("document.querySelector('#ctl00_ContentPlaceHolder2_dgSubmitted').outerHTML")
myTableArray := table2arrayEx("ctl00_ContentPlaceHolder2_dgSubmitted",table.value)

table2arrayEx(id,html) {
	d := ComObjCreate("HTMLFile")
	d.write(html)

	tObj := []
	loop % d.getElementById(id).rows.length
	{
		row:= (A_Index-1)
		loop % d.getElementById(id).rows[row].cells.length
		{
			col := (A_Index-1)
			tObj[row, col] := d.getElementById(id).rows[row].cells[col].innertext
		}
	}
	return tObj
}
Please tell me the way where we generate array inside chrome console and extract it as it is to AHK, having all the DOMparameters unlike above reading function, this is why I am not converting to chrome AHK coz I work a lot with tables

I wish for support for console.table() but it return type: unidentified

Code: Select all

js = table(document.querySelector('#ctl00_ContentPlaceHolder2_dgSubmitted'))
table := page.evaluate(js)
"When there is no gravity, there is absolute vacuum and light travel with no time" -Game changer theory
20170201225639
Posts: 144
Joined: 01 Feb 2017, 22:57

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by 20170201225639 » 18 Jan 2021, 05:21

I've been running Chrome in debug mode for almost a month now and I don't see any performance-related reasons why I shouldn't just run debug mode all the time, since it feels just as fast as non-debug mode, and the productivity gains from Chrome.ahk is simply too great.

If you'd like to run Chrome in debug mode always, in addition to making a custom shortcut, you can also change the registry, so that even when Chrome is launched by other apps or via double-clicking an .html file, it's still launched in debug mode.

Code: Select all

Value = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 -- "`%1"
RegWrite, REG_SZ, HKEY_CLASSES_ROOT, ChromeHTML\shell\open\command, , %Value%
Source: https://stackoverflow.com/questions/51563287/how-to-make-chrome-always-launch-with-remote-debugging-port-flag
20170201225639
Posts: 144
Joined: 01 Feb 2017, 22:57

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by 20170201225639 » 18 Jan 2021, 12:52

teadrinker wrote:
09 Dec 2020, 09:25
Class LightJson:

teadrinker, thanks for posting this version! If I already am using LightJson, and all I need to do is converting back and forth, is there any benefit of using one of the non-Light versions of your class e.g. posted at
https://www.autohotkey.com/boards/viewtopic.php?f=76&t=65631&start=80
?


(Also, I can't tell for sure in that thread which version is the current latest version of the non-Light version. Does your class have an official thread?)
teadrinker
Posts: 4309
Joined: 29 Mar 2015, 09:41
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by teadrinker » 18 Jan 2021, 16:02

20170201225639 wrote: is there any benefit of using one of the non-Light versions of your
Nope, Parse and Stringify methods do the same in both classes.
20170201225639 wrote: Does your class have an official thread?
Not yet. :)
20170201225639
Posts: 144
Joined: 01 Feb 2017, 22:57

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by 20170201225639 » 19 Jan 2021, 14:49

teadrinker wrote:
18 Jan 2021, 16:02
Nope, Parse and Stringify methods do the same in both classes.
Good to know, it's definitely noticeably faster on my machine than other methods available.
20170201225639
Posts: 144
Joined: 01 Feb 2017, 22:57

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by 20170201225639 » 19 Jan 2021, 15:03

Added this to my local copy of the Chrome class, so tabs can be cycled using a hotkey somewhat like cycling windows using ahk's GroupActivate.

For example, you can use this to activate all google docs tabs one by one

video: https://streamable.com/sohy5v

Code: Select all

Chrome.CycleToNextTabMatchingThisDescription("url", "https://docs.google.com", "startswith")
Super crude, but so far it's been working okay (as in, stress-testing it did not result in Chrome refusing connection).



The method:

Code: Select all


	CycleToNextTabMatchingThisDescription(Key, Value, MatchMode)
	{
		static ListOfDebuggerURLsForAllMatches
		static IndexOfNextTabToCycleTo

		if (A_PriorHotkey!=A_ThisHotkey)
		{
			IndexOfNextTabToCycleTo := 0
			ListOfDebuggerURLsForAllMatches := []
			for n, PageData in this.GetPageList()
			{
				if (PageData["type"]!="page")
				{
					OutputDebug, Skipping
					continue
				}
				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)))
				ListOfDebuggerURLsForAllMatches.Push(PageData.webSocketDebuggerUrl)
			}
		}
		
		len := ListOfDebuggerURLsForAllMatches.Length()
		if (len==0)
		{
			OutputDebug, Nada
		}
		else
		{
			IndexOfNextTabToCycleTo++
			if (IndexOfNextTabToCycleTo>len)
				IndexOfNextTabToCycleTo := 1
			
			PageInstance := new this.Page(ListOfDebuggerURLsForAllMatches[IndexOfNextTabToCycleTo])
			PageInstance.Call("Page.bringToFront")
			PageInstance.Disconnect()
		}
	}

AcidUK
Posts: 2
Joined: 03 Feb 2021, 11:11

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by AcidUK » 03 Feb 2021, 11:32

Edit: Solved - Chrome.ahk works fine with AutohotkeyU32 on my system, but not AutohotkeyU64.

Hello, I'm experiencing something a little odd, and I can't put a finger on what's going on. I wonder if someone might be able to offer some insight. I'm new to AHK, but not scripting in general.

I have a script which works fine in my IDE (using vscode, but pointing the interpreter at my AutoHotkeyU64.exe (which is installed in userspace rather than as admin). It works absolutely fine within this, but when I launch it in windows using the same Autohotkey executable, I'm getting the following error with the Chrome AHK library, even with the simplest of scripts.

Code: Select all

#NoEnv

#Include C:\Sean\AHK\Chrome\Chrome.ahk

page := Chrome.GetPageByURL("https www.bbc.co.uk /news",  Broken Link for safety "startswith")
The link is normally formed, the forum has adjusted it. This causes no errors in VSCode when executed (it doesn't produce any output, I've simplified it to the most basic script that will demonstrate the error). When I launch it in windows however, I get the following error, and google/these forums/the github issues aren't giving me any clues:

Code: Select all

---------------------------
debug.ahk
---------------------------
Error in #include file "C:\Sean\AHK\Chrome\Chrome.ahk":
     0x80072726 - An invalid argument was supplied.
Source:		WinHttp.WinHttpRequest
Description:	An invalid argument was supplied.


HelpFile:		(null)
HelpContext:	0

Specifically: send

	Line#
	102: }
	108: {
	109: Process,Close,this.PID
	110: }
	118: {
	119: http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
	120: http.open("GET", "http 127.0.0.1: "  Broken Link for safety this.DebugPort "/json")  
--->	121: http.send()  
	122: Return,this.Jxon_Load(http.responseText)
	123: }
	137: {
	138: Count := 0
	139: For n,PageData in this.GetPageList()
	140: {
	141: if (((MatchMode = "exact" && PageData[Key] = Value) || (MatchMode = "contains" && InStr(PageData[Key], Value)) || (MatchMode = "startswith" && InStr(PageData[Key], Value) == 1) || (MatchMode = "regex" && PageData[Key] ~= Value)) && ++Count == Index)  

Continue running the script?
---------------------------
Yes   No   
---------------------------
** Please note that the forum has adjusted the link in line 120, it reads fine otherwise.

Thanks for any insight!
Scar1ot
Posts: 3
Joined: 04 Feb 2021, 00:26

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by Scar1ot » 04 Feb 2021, 00:51

Hello!
First, I want to say a huge thank you to the creator of this plugin. It really helps a lot in automating tasks. Secondly, I write via google translator, there may be distortions of phrases :)
Recently, there was such a problem:
When executing a script that contains javascript functions, the error "$ is not a function" appears. Moreover, this error does not appear on all sites - on some the script is executed, on some not.
I searched the topics on the forum, but still did not solve the problem.
The script itself is simple:

Code: Select all

#NoEnv
SetBatchLines, -1
#Include Chrome.ahk
Page: = Chrome.GetPage ()
test: = Page.Evaluate ("$ ('# block-system-main'). attr ('class')") .value
msgbox,% test%
return
I can throw off examples of sites where it works and where it does not.
What could be the problem?
AutoHotKey version 1.1.32.00
Chrome.ahk version 1.2
gregster
Posts: 8921
Joined: 30 Sep 2013, 06:48

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by gregster » 04 Feb 2021, 00:59

Scar1ot wrote:
04 Feb 2021, 00:51
What could be the problem?
At least, you have a lot of spaces at places where they are not allowed, for example between function name and parentheses, between : and =, and so on.

Code: Select all

#NoEnv
SetBatchLines, -1
#Include Chrome.ahk
Page := Chrome.GetPage()
test := Page.Evaluate("$('# block-system-main').attr('class')").value
msgbox, %test%
return
(but I suppose this is not your complete code - you are not creating a Chrome instance; this probably won't work.
See the Chrome.ahk source code for how to connect to an already running instance or see the examples for how to create a new instance.
In doubt, try to build up your own code on the provided examples which can be found in the Chrome.ahk release zip-file, in this topic and across these forums.)

Generally, try to review AHK syntax again, especially functions, expressions and variables.
Scar1ot
Posts: 3
Joined: 04 Feb 2021, 00:26

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by Scar1ot » 04 Feb 2021, 04:03

gregster wrote:
04 Feb 2021, 00:59
Scar1ot wrote:
04 Feb 2021, 00:51
What could be the problem?
At least, you have a lot of spaces at places where they are not allowed, for example between function name and parentheses, between : and =, and so on.

Code: Select all

#NoEnv
SetBatchLines, -1
#Include Chrome.ahk
Page := Chrome.GetPage()
test := Page.Evaluate("$('# block-system-main').attr('class')").value
msgbox, %test%
return
(but I suppose this is not your complete code - you are not creating a Chrome instance; this probably won't work.
See the Chrome.ahk source code for how to connect to an already running instance or see the examples for how to create a new instance.
In doubt, try to build up your own code on the provided examples which can be found in the Chrome.ahk release zip-file, in this topic and across these forums.)

Generally, try to review AHK syntax again, especially functions, expressions and variables.
As I understand it, chrome.ahk still connects. Yes, I don't have a new chrome created, but debugmode is enabled --remote-debugging-port = 9222
In addition, this script works on some sites and does not work on others. On the working day it is impossible to define the function.
I removed the extra spaces - it did not help.
burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by burque505 » 04 Feb 2021, 09:23

@Scar1ot, this is just a guess, probably bad. Is it possible some sites you visit don't accept jQuery? I admit it seems unlikely. Assuming you are wanting "test" to hold the text value of the class of '#block-system-main', you might try this.

Code: Select all

test := Page.Evaluate("document.querySelector('#block-system-main').classList.value").value
This should also work if the above code works:

Code: Select all

test := Page.Evaluate("document.getElementById('block-system-main').classList.value").value
Edit:
This should work also.

Code: Select all

test := Page.Evaluate("document.getElementById('block-system-main').className").value
So should this.

Code: Select all

test := Page.Evaluate("document.getElementById('block-system-main').getAttribute(""class"")").value
Here's code you could use to check whether the site supports jQuery, and what version.

Code: Select all

version := Page.Evaluate("if (jQuery && typeof jQuery !== ""undefined"") {jQuery.fn.jquery}").value
msgbox %version%
I got that code from this blog and modified it. Try it on that page and see what happens ...
Regards,
burque505
Scar1ot
Posts: 3
Joined: 04 Feb 2021, 00:26

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by Scar1ot » 05 Feb 2021, 03:19

burque505 wrote:
04 Feb 2021, 09:23
Regards,
burque505
Thanks for the options!
Yesterday I found a working version myself (by the way, you suggested it too)
In my case, this option works:

Code: Select all

test:=Page.Evaluate("align = document.querySelector('.b-flat-right__with').getAttribute('class')").value
burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by burque505 » 05 Feb 2021, 09:42

@Scar1ot, glad to see you got it working.
User avatar
kagato
Posts: 10
Joined: 04 Dec 2015, 16:48
Location: 127.0.0.1

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by kagato » 18 Feb 2021, 12:05

teadrinker wrote:
09 Dec 2020, 09:25
Class LightJson:

Code: Select all

json = {"key": "value"}
Obj := LightJson.Parse(json)                     ; json to AHK object
MsgBox, % Obj.key

; set boolean value
Obj.bool := LightJson.true
MsgBox, % LightJson.Stringify(obj, "   ")        ; AHK object to json

unescapedStr = 
(
text%A_Tab%abc
new line
)
escapedStr := LightJson.Stringify(unescapedStr)  ; escape spec characters
MsgBox, % escapedStr

class LightJson
{
   static JS := LightJson.GetJS(), true := {}, false := {}, null := {}
   
   Parse(json, _rec := false) {
      if !_rec
         obj := this.Parse(this.JS.eval("(" . json . ")"), true)
      else if !IsObject(json)
         obj := json
      else if this.JS.Object.prototype.toString.call(json) == "[object Array]" {
         obj := []
         Loop % json.length
            obj.Push( this.Parse(json[A_Index - 1], true) )
      }
      else {
         obj := {}
         keys := this.JS.Object.keys(json)
         Loop % keys.length {
            k := keys[A_Index - 1]
            obj[k] := this.Parse(json[k], true)
         }
      }
      Return obj
   }
   
   Stringify(obj, indent := "") {
      if indent|1 {
         for k, v in ["true", "false", "null"]
            if (obj = this[v])
               Return v

         if IsObject( obj ) {
            isArray := true
            for key in obj {
               if IsObject(key)
                  throw Exception("Invalid key")
               if !( key = A_Index || isArray := false )
                  break
            }
            for k, v in obj
               str .= ( A_Index = 1 ? "" : "," ) . ( isArray ? "" : """" . k . """:" ) . this.Stringify(v, true)

            Return isArray ? "[" str "]" : "{" str "}"
         }
         else if !(obj*1 = "" || RegExMatch(obj, "^-?0|\s"))
            Return obj
         
         for k, v in [["\", "\\"], [A_Tab, "\t"], ["""", "\"""], ["/", "\/"], ["`n", "\n"], ["`r", "\r"], [Chr(12), "\f"], [Chr(8), "\b"]]
            obj := StrReplace( obj, v[1], v[2] )

         Return """" obj """"
      }
      sObj := this.Stringify(obj, true)
      Return this.JS.eval("JSON.stringify(" . sObj . ",'','" . indent . "')")
   }
   
   GetJS() {
      static Doc, JS
      if !Doc {
         Doc := ComObjCreate("htmlfile")
         Doc.write("<meta http-equiv=""X-UA-Compatible"" content=""IE=9"">")
         JS := Doc.parentWindow
         ( Doc.documentMode < 9 && JS.execScript() )
      }
      Return JS
   }
}
Using with class Chrome:

Code: Select all

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(), PID: Chromes[Chromes.MinIndex(), "PID"]}
      else
         ChromeInst := new Chrome(ProfilePath)
      ```
   */
   FindInstances()
   {
      Out := {}
      for Item in ComObjGet("winmgmts:").ExecQuery("SELECT * FROM Win32_Process WHERE Name = 'chrome.exe'")
         if RegExMatch(Item.CommandLine, "i)chrome.exe""?\s+--remote-debugging-port=(\d+)", Match)
            Out[Match1] := {cmd: Item.CommandLine, PID: Item.ProcessId}
      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 : "")
      . 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: "  Broken Link for safety this.DebugPort "/json")
      http.send()
      return LightJson.Parse(http.responseText)
   }
   
   /*
      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": LightJson.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(LightJson.Stringify({"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",, LightJson.Stringify(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)
      {
         response := this.Call("Runtime.evaluate",
         ( LTrim Join
         {
            "expression": JS,
            "objectGroup": "console",
            "includeCommandLineAPI": LightJson.true,
            "silent": LightJson.false,
            "returnByValue": LightJson.false,
            "userGesture": LightJson.true,
            "awaitPromise": LightJson.false
         }
         ))
         
         if (response.exceptionDetails)
            throw Exception(response.result.description,, LightJson.Stringify(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 := LightJson.Parse(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
            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
            }
         }
      }
   }
}

class LightJson
{
   static JS := LightJson.GetJS(), true := {}, false := {}, null := {}
   
   Parse(json, _rec := false) {
      if !_rec
         obj := this.Parse(this.JS.eval("(" . json . ")"), true)
      else if !IsObject(json)
         obj := json
      else if this.JS.Object.prototype.toString.call(json) == "[object Array]" {
         obj := []
         Loop % json.length
            obj.Push( this.Parse(json[A_Index - 1], true) )
      }
      else {
         obj := {}
         keys := this.JS.Object.keys(json)
         Loop % keys.length {
            k := keys[A_Index - 1]
            obj[k] := this.Parse(json[k], true)
         }
      }
      Return obj
   }
   
   Stringify(obj, indent := "") {
      if indent|1 {
         for k, v in ["true", "false", "null"]
            if (obj = this[v])
               Return v

         if IsObject( obj ) {
            isArray := true
            for key in obj {
               if IsObject(key)
                  throw Exception("Invalid key")
               if !( key = A_Index || isArray := false )
                  break
            }
            for k, v in obj
               str .= ( A_Index = 1 ? "" : "," ) . ( isArray ? "" : """" . k . """:" ) . this.Stringify(v, true)

            Return isArray ? "[" str "]" : "{" str "}"
         }
         else if !(obj*1 = "" || RegExMatch(obj, "^-?0|\s"))
            Return obj
         
         for k, v in [["\", "\\"], [A_Tab, "\t"], ["""", "\"""], ["/", "\/"], ["`n", "\n"], ["`r", "\r"], [Chr(12), "\f"], [Chr(8), "\b"]]
            obj := StrReplace( obj, v[1], v[2] )

         Return """" obj """"
      }
      sObj := this.Stringify(obj, true)
      Return this.JS.eval("JSON.stringify(" . sObj . ",'','" . indent . "')")
   }
   
   GetJS() {
      static Doc, JS
      if !Doc {
         Doc := ComObjCreate("htmlfile")
         Doc.write("<meta http-equiv=""X-UA-Compatible"" content=""IE=9"">")
         JS := Doc.parentWindow
         ( Doc.documentMode < 9 && JS.execScript() )
      }
      Return JS
   }
}
Thank you so much for this! It has completely resolved the issues I was having with huge delays when pulling large JSON blobs in from fetch responses. I am having however one issue in that when I attempt to perform a reload (pageHandle.Call("Page.reload")) I get the parsing error:

Code: Select all

{"code":-32602,"data":"Failed to deserialize params - CBOR: map start expected at position 6","message":"Invalid parameters"}
When using Coco's class, it works fine and the attached page reloads as expected. Other Page functions (Page.navigate and Page.bringToFront) work fine when called with LightJson. Is it expecting to find one of the optional parameters (ignoreCache and scriptToEvaluateOnLoad)?
Chuck Norris doesn't need garbage collection because he doesn't call .Dispose(), he calls .DropKick().
teadrinker
Posts: 4309
Joined: 29 Mar 2015, 09:41
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by teadrinker » 18 Feb 2021, 12:55

kagato wrote: when I attempt to perform a reload (pageHandle.Call("Page.reload")) I get the parsing error
Thanks for the response, fixed:

Code: Select all

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(), PID: Chromes[Chromes.MinIndex(), "PID"]}
		else
			ChromeInst := new Chrome(ProfilePath)
		```
	*/
	FindInstances()
	{
		Out := {}
		for Item in ComObjGet("winmgmts:").ExecQuery("SELECT * FROM Win32_Process WHERE Name = 'chrome.exe'")
			if RegExMatch(Item.CommandLine, "i)chrome.exe""?\s+--remote-debugging-port=(\d+)", Match)
				Out[Match1] := {cmd: Item.CommandLine, PID: Item.ProcessId}
		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 : "")
		. 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 LightJson.Parse(http.responseText)
	}
	
	/*
		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": LightJson.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(LightJson.Stringify({"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",, LightJson.Stringify(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)
		{
			response := this.Call("Runtime.evaluate",
			( LTrim Join
			{
				"expression": JS,
				"objectGroup": "console",
				"includeCommandLineAPI": LightJson.true,
				"silent": LightJson.false,
				"returnByValue": LightJson.false,
				"userGesture": LightJson.true,
				"awaitPromise": LightJson.false
			}
			))
			
			if (response.exceptionDetails)
				throw Exception(response.result.description,, LightJson.Stringify(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 := LightJson.Parse(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
				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
				}
			}
		}
	}
}

class LightJson
{
   static JS := LightJson.GetJS(), true := {}, false := {}, null := {}
   
   Parse(json, _rec := false) {
      if !_rec
         obj := this.Parse(this.JS.eval("(" . json . ")"), true)
      else if !IsObject(json)
         obj := json
      else if this.JS.Object.prototype.toString.call(json) == "[object Array]" {
         obj := []
         Loop % json.length
            obj.Push( this.Parse(json[A_Index - 1], true) )
      }
      else {
         obj := {}
         keys := this.JS.Object.keys(json)
         Loop % keys.length {
            k := keys[A_Index - 1]
            obj[k] := this.Parse(json[k], true)
         }
      }
      Return obj
   }
   
   Stringify(obj, indent := "") {
      if indent|1 {
         for k, v in ["true", "false", "null"]
            if (obj = this[v])
               Return v

         if IsObject( obj ) {
            isArray := true
            for key in obj {
               if IsObject(key)
                  throw Exception("Invalid key")
               if !( key = A_Index || isArray := false )
                  break
            }
            for k, v in obj
               str .= ( A_Index = 1 ? "" : "," ) . ( isArray ? "" : """" . k . """:" ) . this.Stringify(v, true)

            Return str = "" ? "{}" : isArray ? "[" . str . "]" : "{" . str . "}"
         }
         else if !(obj*1 = "" || RegExMatch(obj, "^-?0|\s"))
            Return obj
         
         for k, v in [["\", "\\"], [A_Tab, "\t"], ["""", "\"""], ["/", "\/"], ["`n", "\n"], ["`r", "\r"], [Chr(12), "\f"], [Chr(8), "\b"]]
            obj := StrReplace( obj, v[1], v[2] )

         Return """" obj """"
      }
      sObj := this.Stringify(obj, true)
      Return this.JS.eval("JSON.stringify(" . sObj . ",'','" . indent . "')")
   }
   
   GetJS() {
      static Doc, JS
      if !Doc {
         Doc := ComObjCreate("htmlfile")
         Doc.write("<meta http-equiv=""X-UA-Compatible"" content=""IE=9"">")
         JS := Doc.parentWindow
         ( Doc.documentMode < 9 && JS.execScript() )
      }
      Return JS
   }
}
User avatar
kagato
Posts: 10
Joined: 04 Dec 2015, 16:48
Location: 127.0.0.1

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by kagato » 18 Feb 2021, 13:38

teadrinker wrote:
18 Feb 2021, 12:55
kagato wrote: when I attempt to perform a reload (pageHandle.Call("Page.reload")) I get the parsing error
Thanks for the response, fixed:
Thank you kindly :dance:
Chuck Norris doesn't need garbage collection because he doesn't call .Dispose(), he calls .DropKick().
User avatar
Xeo786
Posts: 759
Joined: 09 Nov 2015, 02:43
Location: Karachi, Pakistan

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by Xeo786 » 19 Feb 2021, 02:44

I wanted to read table using chrome AHK and here are my findings

first of all HTML of table

I knew we can define anything in console as variable, and I came up with idea to do the calculation in chrome console and extract that array to AHK seems quit simple but It took me few hours to figure out how to do it, I read chrome API and console, and here is my code

Code: Select all

;#Include ../Chrome.ahk
#Include ChomeLJ.ahk
page := chrome.GetPageByURL( myurl,"contains")

; a function which will turn table into Jsonarray inside console 
stringtotable =
(
function tableToJson(table) {
    var data = [];
    // first row needs to be headers
    var headers = [];
    for (var i=0; i<table.rows[0].cells.length; i++) {
        headers[i] = table.rows[0].cells[i].innerHTML.toLowerCase().replace(/ /gi,'');
    }
    // go through cells
    for (var i=1; i<table.rows.length; i++) {
        var tableRow = table.rows[i];
        var rowData = {};
        for (var j=0; j<tableRow.cells.length; j++) {
            rowData[ headers[j] ] = tableRow.cells[j].innerHTML
        }
        data.push(rowData);
    }       
    return data;
}
)

page.evaluate(stringtotable) ; feed the table into the console so we gona use it next time

;another js code which using tabletoarrayfuntion on element having table using querySelector
js = tableToJson(document.querySelector('#ctl00_ContentPlaceHolder2_dgSubmitted'))

i := page.evaluate(js) ; it return with an object but it does not have any table idk where is that json table hidden or something 
x := i.objectid ; I just found objectid which was "objid = {"prototypeObjectId": "93" }"
; jxondump didn't worked either on i or i.value
msgbox, % chrome.Jxon_Dump(i.value) "`n" x 
simply Evaluate(JS) failed to retrieve Table to AHK so I create eval_obj similar to Evaluate(JS) with only change to set returnByValue set true

Code: Select all

Eval_obj(JS)
		{
			response := this.Call("Runtime.evaluate",
			( LTrim Join
			{
				"expression": JS,
				"objectGroup": "console",
				"includeCommandLineAPI": Chrome.Jxon_True(),
				"silent": Chrome.Jxon_False(),
				"returnByValue": Chrome.Jxon_True(),
				"userGesture": Chrome.Jxon_True(),
				"awaitPromise": Chrome.Jxon_False()
			}
			))
			
			if (response.exceptionDetails)
			{
				throw Exception(Chrome.Jxon_Dump(response.result.description)) ; Chrome.Jxon_Dump(response.exceptionDetails)
			}
			
			return response.result
		}
and it worked like a charm

Code: Select all

js = tableToJson(document.querySelector('#ctl00_ContentPlaceHolder2_dgSubmitted'))
table := page.Eval_obj(js).value
loop, % table.maxindex()
{
	msgbox, % "row: " a_index
	. "`nSB " table[a_index ,"gdno"]
	. "`nRef " table[a_index ,"blno"]
	. "`nStatus " table[a_index ,"status"]
	. "`nView " table[a_index ,"view"]
	. "`nprint " table[a_index ,"print"]
}
Hope my findings gonna help y'all and yah ..... if you change tableRow.cells[j].innerHTML to tableRow.cells[j].innerText in tableToJson you gonna get innertext instead but in my case I needed html :D
"When there is no gravity, there is absolute vacuum and light travel with no time" -Game changer theory
burque505
Posts: 1731
Joined: 22 Jan 2017, 19:37

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

Post by burque505 » 19 Feb 2021, 09:09

@Xeo786, thank you! Looking forward to trying it out.
Regards,
burque505
Post Reply

Return to “Scripts and Functions (v1)”