Google Search - Open the First 10 Links in Google in a New Tab

Nothing too special here but thought it might be useful to someone. I find myself constantly searching google and then command clicking on the first several results to open them in a new tab in Safari. This speeds up that process a little.

It works in Google Chrome and Firefox as well, I haven't tested any other browser. Google Search.zip (61.0 KB)

1 Like

Hey Skillet,

Tab is not a reliable method of navigating links in Safari on my 10.9.5 system.

This macro will scrape the links out of a Google search page and send them to TextWrangler (although you can change TextWrangler to BBEdit in the script to use BBEdit instead).

It produces a list with the link titles and links formatted in a table like so:

Google Links to TextWrangler.kmmacros (5.2 KB)

From there I have another script to load each individual link into the front Safari window, which I'll post after I clean it up a bit.

I don't like to load 10 links at a time, because my MacBook Pro is limited to 8GB of RAM — but it would be easy to modify the script to do that.

-Chris

Thanks Chris, you are always a rock star in your approach. I also noticed that with the tab it would often do every other link, which seems very bizarre.

I look forward to seeing your following AppleScript.

This macro will load links from the front TextWrangler window into the front Safari window.

However if there's a document open in TextWrangler called Temp_URLs.txt it will be used instead.

As URLs are loaded they are given a checkmark to indicate they've been viewed:

The macro tolerates whitespace and text that does NOT contain a URL.

The macro will beep when no more URLs are left to load.

Links { Load URL from TextWrangler Front Window }.kmmacros (4.2 KB)

I like to position my window in TextWrangler (BBEdit for me) so I can see the edge of it behind Safari. That lets me see the line numbers in TextWrangler to let me know how many more URLs I have to process.

On my system I have plugins for more sites than Google. I use a folder in the Finder to contain scripts named with the base-domain of the site to be searched. So it's relatively easy to add a new site to my link scraper.

-Chris

Thanks – a useful approach and good food for thought.

For reference, in case it's of any interest, here's one way of opening all the tabs from the shell (after gathering them with JXA (and in-browser) Javascript), and passing them on in Markdown format.

Harvest Google result links, opening each in own tab.kmmacros (2.9 KB)

osascript -l JavaScript <<JXA_END 2>/dev/null
function run() {
  "use strict";

  // Harvest elements from Safari by XPath pattern
  function pageXPathHarvest(strXPath) {
    var lstWins = appSafari.windows(),
      oWin = lstWins.length ? lstWins[0] : null;

    return (oWin) ? appSafari.doJavaScript(
      "(" + xpathHarvest.toString() + ")(\"" + strXPath + "\")", { in
          : oWin.currentTab
      }
    ) : "No Safari page open";
  }

  // Harvesting function to run in the browser context
  function xpathHarvest(strPath) {
    var r = document.evaluate(strPath, document, null, 0, null),
      lst = [],
      oNode;

    while (oNode = r.iterateNext()) {
      lst.push([oNode.text, oNode.href]);
    }
    return lst;
  }

  // MAIN
  var appSafari = Application("Safari"),
    app = Application.currentApplication();

  app.includeStandardAdditions = true;

  // Gather any results as [ text ]( href ) Markdown links
  var strGoogleResultPath = "//*[@class='r']/a",
    lstLinks = pageXPathHarvest(strGoogleResultPath),
    blnFound = lstLinks.length,
    strResult = blnFound ?
    lstLinks.reduce(function (strAccum, lstTextLink) {
      return strAccum + '[' + lstTextLink[0] + '](' + lstTextLink[1] + ')\n';
    }, '') :
    'No matching links matching "' + strGoogleResultPath + '" found',
    strCmd;

  //  open each link in a browser tab
  if (blnFound) {
    strCmd = lstLinks.reduce(
      function (strAccum, lstTextLink) {
        return strAccum + 'open "' + lstTextLink[1] + '"\n';
      },
      ''
    )
    app.doShellScript(strCmd);
  }

  // and pass labelled links on in Markdown format
  return strResult;
}
JXA_END
1 Like

Cool. I look forward to playing with that when I get updated to Yosemite.

-Chris

Thank you both a bunch, very nice work!

So I had been using this almost daily until I upgraded my Mac and it broke any ideas?
Keyboard Maestro 8.2.2 “Harvest Google result links, opening each in own tab” Macro

Harvest Google result links- opening each in own tab.kmmacros (30 KB)

Does the Safari on your new system have this setting enabled in the main menu:

Safari > Develop > Allow Javascript from Apple Events ?

Harvest Google result links (from JXA action) opening each in own tab.kmmacros (21.1 KB)

This variant (running the code from an Execute JavaScript for Automation action – rather than the shell) seems to be working here.

Any good where you are ?

1 Like

And here is an updated JXA script, which seems to be working on Sierra, with KM 8.2.2 and Safari 11.1.2 (Develop > Allow JavaScript from Apple Events enabled)

(() => {
    'use strict';

    const main = () => {
        const
            strXPath = "//*[@class='r']/a",
            saf = Application("Safari"),
            ws = saf.windows,
            lrHarvest = bindLR(
                0 < ws.length ? (
                    Right(ws.at(0))
                ) : Left('No window open in Safari'),
                w => {
                    const
                        xs = pageXPathHarvest(
                            saf, w, strXPath
                        );
                    return 0 < xs.length ? (
                        // Safari effect
                        tabsOpened(saf, w, xs),
                        // Keyboard Maestro value
                        Right(
                            xs.reduce(
                                (a, link) =>
                                `${a}[${link[0]}](${link[1]})\n`,
                                ''
                            )
                        )
                    ) : Left(
                        'Perhaps not a Google search page ?\n' +
                        '(No links matching "' + strXPath + '")'

                    );
                }
            );
        return lrHarvest.Left || lrHarvest.Right;
    };

    // SAFARI ---------------------------------------------

    // Harvest elements from Safari by XPath pattern
    const pageXPathHarvest = (browser, oWin, strXPath) =>
        browser.doJavaScript(
            `(${xpathHarvest})("${strXPath}")`, { in
                : oWin.currentTab
            }
        );

    // tabsOpened :: Application -> Window -> (String, String) -> IO()
    const tabsOpened = (safari, oWin, links) => {
        const winTabs = oWin.tabs;
        links.map(link => winTabs.push(safari.Tab({
            url: link[1]
        })))
    }

    // Harvesting function to run in the browser context
    const xpathHarvest = strPath => {
        const
            r = document.evaluate(strPath, document, null, 0, null),
            xs = [];
        var oNode;
        while (oNode = r.iterateNext()) {
            xs.push([oNode.text, oNode.href]);
        }
        return xs;
    };

    // GENERIC FUNCTIONS ----------------------------------

    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // MAIN
    return main();
})();

1 Like

Thank you I forgot about checking that and thought it was the upgrade that I did.

Very appreciated.

1 Like

Hey @ComplexPoint

I tried to use this script to open first Google result from the keyboard and it fails on me.

I was running it like so:

I did activate: Develop > Allow JavaScript from Apple Events enabled

Perhaps I am missing something and this is not what the script does. It's just the AppleScript I was using made by @ccstone is failing in Sierra and above now.

Here is the error I get when I run it:

/var/folders/5l/5s7qw5ld1xxglm0094_hyh_c0000gn/T/Keyboard-Maestro-Script-81ECA7A6-72E3-4B9E-B603-D65552A765FE:5:6: script error: Expected expression but found “>”. (-2741)

Something to do with the triggering ? (this version is Safari-only, of course).

The following variant is working here with:

  • Safari Version 11.1.2 (13605.3.8)
  • MacOS 10.13.6

(Opens tabs, copies links as MD)

Harvest Google result links as Markdown.kmmacros (23.7 KB)

sample

JS Source

(() => {
    'use strict';

    const main = () => {
        const
            strXPath = "//*[@class='r']/a",
            saf = Application("Safari"),
            ws = saf.windows;
        return bindLR(
            0 < ws.length ? (
                Right(ws.at(0))
            ) : Left('No window open in Safari'),
            w => {
                const
                    xs = pageXPathHarvest(
                        saf, w, strXPath
                    );
                return 0 < xs.length ? (
                    // Safari effect
                    tabsOpened(saf, w, xs),
                    // Keyboard Maestro value
                    Right(
                        xs.reduce(
                            (a, link) =>
                            `${a}[${link[0]}](${link[1]})\n`,
                            ''
                        )
                    )
                ) : Left(
                    'Perhaps not a Google search page ?\n' +
                    '(No links matching "' + strXPath + '")'
                );
            }
        );
    };

    // SAFARI ---------------------------------------------

    // Harvest elements from Safari by XPath pattern
    const pageXPathHarvest = (browser, oWin, strXPath) =>
        browser.doJavaScript(
            `(${xpathHarvest})("${strXPath}")`, { in
                : oWin.currentTab
            }
        );

    // tabsOpened :: Application -> Window -> (String, String) -> IO()
    const tabsOpened = (safari, oWin, links) => {
        const winTabs = oWin.tabs;
        links.map(link => winTabs.push(safari.Tab({
            url: link[1]
        })))
    }

    // Harvesting function to run in the browser context
    const xpathHarvest = strPath => {
        const
            r = document.evaluate(strPath, document, null, 0, null),
            xs = [];
        var oNode;
        while (oNode = r.iterateNext()) {
            xs.push([oNode.text, oNode.href]);
        }
        return xs;
    };

    // GENERIC FUNCTIONS ----------------------------------

    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // MAIN
    return main();
})();

This one works in that it opens all the links in new tabs. I just wanted a script that will open first link only in current tab.

And a way to specify which link to open in the script so I can have 4 different hotkeys for opening either 1st or 2nd or 3rd or 4th link with a macro for each one.

Got it – so what you need, in place of the tabsOpened function, is something like:

// nthLinkOpened :: Application -> (String, String) -> Int -> IO()
const nthLinkOpened = (safari, links, i) => {
    const
        ds = safari.documents,
        d = ( // Ensuring that there is an open document
            ds.length < 1 && ds.push(safari.Document()),
            ds.at(0)
        );
    // i is a zero-based index into the link list.
    return d.url = links[i][1];
};

or more directly:

// nthLinkOpened :: Application -> (String, String) -> Int -> IO()
const nthLinkOpened = (safari, links, i) =>
    (ds =>
        (
            ds.length < 1 && ds.push(safari.Document()),
            ds.at(0)
        )
        .url = links[i][1]
    )(safari.documents);

Where i is a zero-based index to the link that interests you.

Does that give you enough ?

(Let me know if you'd like me to flesh it out a bit more)

What do I pass in as links to nthLinkOpened?

You just need the same xs (derived by pageXPathHarvest) that were being passed to tabsOpened.

So, for example, if you are opening the first link (linkIndex = 0)

Something like:

(() => {
    'use strict';

    const main = () => {
        const
            linkIndex = 0,

            strXPath = "//*[@class='r']/a",
            saf = Application("Safari"),
            ws = saf.windows;
        return bindLR(
            0 < ws.length ? (
                Right(ws.at(0))
            ) : Left('No window open in Safari'),
            w => {
                const
                    xs = pageXPathHarvest(
                        saf, w, strXPath
                    );
                return 0 < xs.length ? (
                    // Safari effect

                    //tabsOpened(saf, w, xs),
                    nthLinkOpened(saf, xs, linkIndex),

                    // Keyboard Maestro value
                    Right(
                        xs.reduce(
                            (a, link) =>
                            `${a}[${link[0]}](${link[1]})\n`,
                            ''
                        )
                    )
                ) : Left(
                    'Perhaps not a Google search page ?\n' +
                    '(No links matching "' + strXPath + '")'
                );
            }
        );
    };

    // SAFARI ---------------------------------------------

    // Harvest elements from Safari by XPath pattern
    const pageXPathHarvest = (browser, oWin, strXPath) =>
        browser.doJavaScript(
            `(${xpathHarvest})("${strXPath}")`, { in
                : oWin.currentTab
            }
        );

    // tabsOpened :: Application -> Window -> (String, String) -> IO()
    const tabsOpened = (safari, oWin, links) => {
        const winTabs = oWin.tabs;
        links.map(link => winTabs.push(safari.Tab({
            url: link[1]
        })))
    };

    // nthLinkOpened :: Application -> (String, String) -> Int -> IO()
    const nthLinkOpened = (safari, links, i) =>
        (ds =>
            (
                ds.length < 1 && ds.push(safari.Document()),
                ds.at(0)
            )
            .url = links[i][1]
        )(safari.documents);

    // Harvesting function to run in the browser context
    const xpathHarvest = strPath => {
        const
            r = document.evaluate(strPath, document, null, 0, null),
            xs = [];
        let oNode;
        while (oNode = r.iterateNext()) {
            xs.push([oNode.text, oNode.href]);
        }
        return xs;
    };

    // GENERIC FUNCTIONS ----------------------------------

    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // MAIN
    return main();
})();

1 Like

Works wonderfully. Thank you.

1 Like

Does anyone know by chance if it's possible to make @ComplexPoint JXA script work for Google Chrome?

Although this extension works quite nicely too, to solve this. I just wanted to reduce amount of extensions I use in my browsers.