Euromind
  • Javascript
    • Javascript

      Dansk evighedskalender

      7. december, 2020

      Javascript

      API til Statistikbanken

      21. september, 2019

      Javascript

      IntersectionObserver

      9. august, 2019

      Javascript

      Navngivne RegEx-grupper i ECMAScript 2018

      29. juli, 2019

      Javascript

      RegEx: Unicode og Look Backward i ECMAScript 2018

      24. juli, 2019

  • CSS/SCSS
    • CSS/SCSS

      Dansk evighedskalender

      7. december, 2020

      CSS/SCSS

      Variable fonte med dansk tegnsæt i open source

      11. august, 2019

      CSS/SCSS

      Progressbar for dokumentposition

      31. juli, 2019

      CSS/SCSS

      Media Query i 2019

      18. juli, 2019

      CSS/SCSS

      Danske Adressers Web API

      17. juli, 2019

  • C#
    • C#

      Authentication for IOS og Android med Firebase i…

      4. oktober, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 3

      5. september, 2019

      C#

      Hurtig eksport til Excel

      4. september, 2019

      C#

      Andersen, Grundtvig, Kierkegaard og ML.NET – del 2

      2. september, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 1

      11. august, 2019

  • Javascript
    • Javascript

      Dansk evighedskalender

      7. december, 2020

      Javascript

      API til Statistikbanken

      21. september, 2019

      Javascript

      IntersectionObserver

      9. august, 2019

      Javascript

      Navngivne RegEx-grupper i ECMAScript 2018

      29. juli, 2019

      Javascript

      RegEx: Unicode og Look Backward i ECMAScript 2018

      24. juli, 2019

  • CSS/SCSS
    • CSS/SCSS

      Dansk evighedskalender

      7. december, 2020

      CSS/SCSS

      Variable fonte med dansk tegnsæt i open source

      11. august, 2019

      CSS/SCSS

      Progressbar for dokumentposition

      31. juli, 2019

      CSS/SCSS

      Media Query i 2019

      18. juli, 2019

      CSS/SCSS

      Danske Adressers Web API

      17. juli, 2019

  • C#
    • C#

      Authentication for IOS og Android med Firebase i…

      4. oktober, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 3

      5. september, 2019

      C#

      Hurtig eksport til Excel

      4. september, 2019

      C#

      Andersen, Grundtvig, Kierkegaard og ML.NET – del 2

      2. september, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 1

      11. august, 2019

Euromind
Webdesign

Headless browser på 10 minutter

af Per Lindsø Larsen 25. juli, 2020
skrevet af Per Lindsø Larsen 25. juli, 2020
Headless browser på 10 minutter

Browsere og høns har det til fælles, at de kan være ganske aktive selv om de får hovedet kappet af. En headless browser er en browser uden brugerflade – eller om man vil – en bil uden karosseri. Motoren kører glimrende, men der er ikke meget at se på. Med den hovedløse browser kan man i princippet alt hvad man kan via brugerfladen, det foregår blot via programmering. Det kan være nyttigt i automatisering af tests af webapplikationer m.v. Og det kan også – som jeg vil koncentrere mig om i dette indlæg – være en nyttig ting ved scraping af dynamiske websider.

Hvad er bedre til en konkret eksemplificering af problemstillingen end Statens Arkiver? Denne hæderkronede institution. Følgende url leder til en ganske tilfældig af de millionvis af digitaliserede sider fra kirkebøger, folketællinger m.v:

https://www.sa.dk/ao-soegesider/da/billedviser?epid=21618400#389236,74205804

Er jeg nu – af kryptiske grunde – i den situation, at jeg ønske ikke blot at downloade siden, men også få hentet noget meta-data om hvad den indeholder, så vil man måske af gammel refleks ty til HtmlAgilityPack og lave noget i retning af

var url ="https://www.sa.dk/ao-soegesider/da/billedviser?epid=21618400#389236,74205804";
var web = new HtmlWeb();
var doc = web.Load(url);
var htmlBody = doc.DocumentNode.SelectSingleNode("//body"); 
HtmlNodeCollection childNodes = htmlBody.ChildNodes; 
foreach (var node in childNodes) { 
   if (node.NodeType == HtmlNodeType.Element) 
       { 
          Console.WriteLine(node.OuterHtml); 
       } 
}

Det giver i dette tilfælde – og i mange andre tilfælde – en noget kedelig oplevelse, fordi den statiske side, som jeg henter. ikke indeholder blot antydningen af de data, som jeg skal bruge. De dannes dynamisk og er først tilgængelige, når javascript på siden er afviklet. Det er i sådan en situation, at man tænker: “Hvor er det godt at have en hovedløs browser”!

Puppeteer Sharp

Puppeteer Sharp er en .Net port til den officielle Node JS Puppeteer API og vil give alt, hvad der er brug for. Den downloader sin egen lokale, skrabede version af Chrome, så jeg ikke ender op med at skidtet ikke længere virker, for jeg har opdateret til en ny version af Chrome browseren på min computer. Der er også andre veje til en hovedløs browser, f.eks. Selenium med ChromeDriver eller FirefoxDriver, men til mange opgaver er Puppeteer hurtigere og kan køre async ud-af-boksen.

Installer Nuget Package PuppeteerSharp:

Eller direkte:

PM> Install-Package PuppeteerSharp -Version 2.0.4

Vi kan nu med enkle midler hente siden efter at javescvript er afvikler, og altså gerne skulle indeholde noget mere data. Det kan komme an på en hurtig prøve:

    public static async Task<string> GetContent(string url)
    {
            await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
            var browser = await Puppeteer.LaunchAsync(new LaunchOptions() { Headless = true });
            var page = await browser.NewPageAsync();
            await page.GoToAsync(url);
            return await page.GetContentAsync();
     }
      
     private static void Main(string[] args)
     {
       string url = "https://www.sa.dk/ao-soegesider/da/billedviser?epid=21618400#389236,74205804";
       Console.WriteLine($"Længde på statisk html: {new WebClient().DownloadString(url).Length} bytes");
       Console.WriteLine($"Længde på dynamisk html: {GetContent(url).GetAwaiter().GetResult().Length} bytes");
    }

     // OUTPUT:
     // Længde statisk html: 7561 bytes
     // Længde dynamisk html: 38564 bytes

Dokumentlængden er nu 5 gange så stor, så mon ikke der er bid? Puppeteer har nogle indbyggede funktioner til at trevle dokumentet igennem:

var node = page.WaitForXPathAsync(xPath);

Funktionen tager som parameter en XPath. Synes man den syntaks er træls at arbejde med, så er der hjælp at hente i Developer Tools. Højreklik på den ønskede node i dokumentet og vælg Copy -> Copy Xpath:

Når vi har fået den ønskede node, kan vi trække de elementer ud af den, som vi er interesseret i:

node.EvaluateFunctionAsync(string script);

hvor script f.eks. kan være “e => e.innerText”, hvis det er innerText der skal bruges. Eller “e => e.getAttribute(‘href’)” hvis det er værdien af en attribut der ønskes.

Det kan pakkes sammen i en nifty og nem FindNodeAsync-funktion:

        public static async Task<string> FindAsync(Page p, string xPath, string script, int timeOut = 1000)
        {
            try
            {
                var _node =  p.WaitForXPathAsync(xPath, new WaitForSelectorOptions { Timeout = timeOut });
                return await _node.EvaluateFunctionAsync<string>(script);
            }
            catch (Exception e)
            {
                // Cheer up - der er andet i livet end dokument nodes
                return "";
            }
        }

Det hele kan nu sættes sammen så metadata for en række arkivalie-urls hentes nemt, hurtigt og asykront:

        public static Stopwatch stopwatch = new Stopwatch();
        public class AOInfo
        {
            public string headcontent = "";
            public string content = "";
            public string creator = "";
            public string opslag = "";
            public string resultstr = "";
        }

     public static async Task<AOInfo> Extract(Browser browser, string url)
     {
       var page = await browser.NewPageAsync();
       await page.GoToAsync(url);
       var info = new AOInfo();
       info.opslag = await FindAsync(page, "//*[contains(@class, 'fancytree-active')]", "e => e.innerText");
       info.creator = await FindAsync(page, "//*[contains(@id, 'creator')]", "e => e.innerText");
       info.headcontent = await FindAsync(page, "//*[contains(@id, 'title')]", "e => e.innerText");
       info.content = await FindAsync(page, "//*[contains(@class, 'fancytree-title')]", "e => e.innerText");
       info.resultstr = info.creator == "Danmarks Statistik"
                ? $"{info.headcontent}: {info.content}, opslag {info.opslag}"
                : $"{info.headcontent} ({info.creator}): {info.content}, opslag {info.opslag}";
       Console.WriteLine(info.resultstr);
       await page.CloseAsync();
       return info;
    }

        public static async Task<string> FindAsync(Page p, string xPath, string script, int timeOut = 1000)
        {
            try
            {
                var _node = p.WaitForXPathAsync(xPath, new WaitForSelectorOptions { Timeout = timeOut });
                return await _node.EvaluateFunctionAsync<string>(script);
            }
            catch (Exception e)
            {
                // Cheer up
                return "";
            }
        }


        public static async Task Go(string[] urls)
        {
            Console.WriteLine("Starter browseren op...");
            stopwatch.Start();
            int totalUrls = urls.Length;
            var tasks = new Task[totalUrls];
            var browser = await LaunchBrowser(new LaunchOptions() {Headless = true});
            Console.WriteLine($"Browser klar efter {stopwatch.ElapsedMilliseconds} ms.");
            Console.WriteLine();
            for (var i = 0; i < totalUrls; i++)
            {
                  tasks[i]= Extract(browser, urls[i]);
            }
            await Task.WhenAll(tasks);
            await browser.CloseAsync();
            Console.WriteLine();
            Console.WriteLine($"Færdig efter {stopwatch.ElapsedMilliseconds} ms.");
        }

        private static void Main(string[] args)
        {
            var urls = new string[]
            {
                "https://www.sa.dk/ao-soegesider/da/billedviser?epid=21618433#389529,74223144",
                "https://www.sa.dk/ao-soegesider/da/billedviser?bsid=119630#119630,17430260",
                "https://www.sa.dk/ao-soegesider/da/billedviser?bsid=8406#8406,207690",
                "https://www.sa.dk/ao-soegesider/da/billedviser?epid=17072242#142200,34752377"
            };
            Go(urls).GetAwaiter().GetResult();
        }

Det giver de ønskede data:

Download som PDF eller billedfil

Der er andre muligheder med Puppeteer. Skulle livet en dag blive så afsporet, at den eneste tilbageværende interesse er en frisk daglig pdf-kopi af Digitaliseringsstyrelsens hjememside – intet menneskeligt bør være os fremmed – så er der også hjælp at hente med Puppeteer.

        public static Stopwatch s = new Stopwatch();


        public static async Task CreatePdf(Browser browser, string url)
        {
            Console.WriteLine($"Starter download af {url}");
            var pdfFilename = $"{new Uri(url).Host}.pdf";
            var page = await browser.NewPageAsync();
            await page.GoToAsync(url);
            Console.WriteLine($"Hentet {url} efter {s.ElapsedMilliseconds} ms.");
            Console.WriteLine($"Starter Konvertering af PDF for {url}");
            await page.PdfAsync(pdfFilename);
            await page.CloseAsync();
            Console.WriteLine($"Dannet PDF: {pdfFilename} efter {s.ElapsedMilliseconds} ms.");
        }
         

        public static async Task SaveAsPdf(string[] urls)
        {
            Console.WriteLine("Starter browseren op...");
            s.Start();
            int totalUrls = urls.Length;
            var tasks = new Task[totalUrls];
            await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
            var browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true, 
                DefaultViewport = null
            });
            Console.WriteLine($"Browser klar efter {s.ElapsedMilliseconds} ms.");
            for (var i = 0; i < totalUrls; i++)
            {
                tasks[i]= CreatePdf(browser, urls[i]);
            }
            await Task.WhenAll(tasks);
            await browser.CloseAsync();
            Console.WriteLine($"Færdig efter {s.ElapsedMilliseconds} ms.");
        }

        private static void Main(string[] args)
        {
            var urls = new string[]
                {"https://google.com", "http://microsoft.com", "http://ft.dk", "http://digst.dk", "http://eb.dk"};

            Task.Run(() => SaveAsPdf(urls));
        }

Vi får nu siderne hentet ned i fuld størrelse – og altså ikke bare et screendump af, hvad der er synligt i browserviduet.

Et lille hack er indsat for at sikre, at pdf-kopien ikke er hakket op på 3-4 forskellige sider:

                      var height = await page.EvaluateExpressionAsync<int>("document.body.scrollHeight");
            var width = await page.EvaluateExpressionAsync<int>("document.body.scrollWidth");

            await page.PdfAsync(pdfFilename, new PdfOptions()
                { PrintBackground = true, Height = height + "px", Width = width + "px", PageRanges = "1" });
            

Alt er tilsyneladende som det skal være – men ikke helt. GDPR-helvedet bryder løs:

Sådan en popup kan ødelægge enhver glæde ved den daglige kopi. Der er flere løsninger på problemet, men her kan vi bruge Puppeteers SetCookieAsync funktion. Et kig i Developer Tools viser, at der skal sættes en cookie med det mundrette navn GoBasic_CookieAcceptanceState_digst.dk

Den kan vi så sætte på følgende måde inden siden kaldes:

            await page.SetCookieAsync(new CookieParam()
                {Name = "GoBasic_CookieAcceptanceState_digst.dk", Value = "Accepted", Domain = "digst.dk"});

For feinschmeckere er der også mulighed for at dowloade siden som billedfil. Udskift PDfAsync:

                options = new ScreenshotOptions() {Quality = 50, FullPage = true};
                await page.ScreenshotAsync(imageFilename,options);

Der er et væld af andre muligheder i Puppeteer. Læs mere på bl.a. https://github.com/hardkoded/puppeteer-sharp

0 Kommentarer
4
FacebookTwitterPinterestEmail
forrige post
Authentication for IOS og Android med Firebase i Xamarin Forms
næste post
Automatisk køreafstand i Excel

Relaterede indlæg

Dansk evighedskalender

7. december, 2020

API til Statistikbanken

21. september, 2019

Variable fonte med dansk tegnsæt i open source

11. august, 2019

Navngivne RegEx-grupper i ECMAScript 2018

29. juli, 2019

Media Query i 2019

18. juli, 2019

Danske Adressers Web API

17. juli, 2019

Barebone AMP i Codepen

9. juli, 2019

Sund javascript

7. juli, 2019

Registrer Gmail API til brug i javascript

27. juni, 2019

Send email fra Javascript med Gmail API

21. juni, 2019

Efterlad en kommentar Afbryd svar

Gem mit navn, email, og website i denne browser til senere kommentarer.

Seneste indlæg

  • Dansk evighedskalender

    7. december, 2020
  • Automatisk køreafstand i Excel

    17. august, 2020
  • Headless browser på 10 minutter

    25. juli, 2020

Kategorier

  • C#
  • CSS/SCSS
  • Excel
  • HTML
  • Javascript
  • Mobile
  • Webdesign
  • Xamarin

Om mig

Om mig

Per Lindsø Larsen

Freelance fullstack developer bo9sat i Aarhus.

Du kan hyre mig til korterevarende projekter eller konkrete opgaveløsninger.

Pæn rabat til non-profit organisationer og foreninger.

Når jeg ikke koder, deltager jeg løbende i diverse spændende forskningsprojekter om alt andet end kodning.

Keep in touch

Facebook Twitter Email Github

Tags

Adresser AMP AMP Story Android API Billedformater Billedoptimering Brand C# Codepen Cordova CPR Crome DevTools CSS Debug Ecmascript Excel Fonte Gmail Gulp HTML Ikoner IOS Javascript JsFiddle Machine Learning Mail Mediaquery ML.NET Mobile RegEx SCSS SMTP Stylometri Visual Studio Webdesign Xamarin

Nyhedsbrev

Timeld nyhedsbrev for info om nye blog-indlæg, tips m.v.

  • Facebook
  • Twitter
  • Email
  • Github

@2019 - Euromind.com - Code-To-Go. All Right Reserved.
lindsoe@gmail.com - mobil: 42797273


Tilbage til top
Euromind
  • Javascript
    • Javascript

      Dansk evighedskalender

      7. december, 2020

      Javascript

      API til Statistikbanken

      21. september, 2019

      Javascript

      IntersectionObserver

      9. august, 2019

      Javascript

      Navngivne RegEx-grupper i ECMAScript 2018

      29. juli, 2019

      Javascript

      RegEx: Unicode og Look Backward i ECMAScript 2018

      24. juli, 2019

  • CSS/SCSS
    • CSS/SCSS

      Dansk evighedskalender

      7. december, 2020

      CSS/SCSS

      Variable fonte med dansk tegnsæt i open source

      11. august, 2019

      CSS/SCSS

      Progressbar for dokumentposition

      31. juli, 2019

      CSS/SCSS

      Media Query i 2019

      18. juli, 2019

      CSS/SCSS

      Danske Adressers Web API

      17. juli, 2019

  • C#
    • C#

      Authentication for IOS og Android med Firebase i…

      4. oktober, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 3

      5. september, 2019

      C#

      Hurtig eksport til Excel

      4. september, 2019

      C#

      Andersen, Grundtvig, Kierkegaard og ML.NET – del 2

      2. september, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 1

      11. august, 2019

Populære indlæg

  • 1

    Stylometri i C# – del 2

    7. juni, 2019
  • 2

    Send email fra Javascript med Gmail API

    21. juni, 2019
  • 3

    Andersen, Grundvig, Kierkegaard og ML.NET – del 1

    11. august, 2019
  • 4

    Gmail, Yahoo og Outlook som SMTP-server

    18. april, 2019
  • 5

    Registrer Gmail API til brug i javascript

    27. juni, 2019
@2019 - Euromind.com - Code-To-Go. All Right Reserved.
lindsoe@gmail.com - mobil: 42797273

Læs ogsåx

GULP møder AMP – del 2

7. juni, 2019

Dansk evighedskalender

7. december, 2020

GULP møder AMP – del 1

5. juni, 2019