Powershell Verkaufspreis auslesen aus Shop?

3 Antworten

Geh in deinem Browser in die Entwicklerconsole [F12]...

Expantiere den Body-Tag und fahre über die Zeilen, bis Du die gewünschte Information findest. In der Seitenanzeige wird diese markiert...

Bild zum Beitrag

jetzt kennst Du die Kriterien nach den der Seiteninhalt (.Content) im Script zerlegt und gefiltert werden muss.

Da die uns wichigen Informationen in span-Tags hinterlegt sind splitten wir den ganzen Content an den Worten span. Da dann immer noch jede Menge Müllzeilen enthalten sind, filtern wir nur die Zeilen Raus, welche mit " class=" beginnen

Jetzt ist es einfach das Ergebnis feiner zu filtern ("header-name" und "product-price__price" ) und das ganze wie gehabt per RegEx einem object zuzuweisen.

$Response = Invoke-WebRequest -URI https://www.douglas.de/de/p/5010115043
$HtmlSpans = $Response.Content -split 'span'|   ?{$_ -like ' class=*'}  #schon mal grob alle Spannzeilen rausfiltern. 
 #auch gleich mal anschauen wa wir da haben...   
$HtmlSpans 
 #jetzt  bauen  wir die  Zeilen welche  die gewünschten Informationen enthalten in ein Object ein ("header-name" und "product-price__price" )
$MeinArtikel = [PSCustomObject]@{
            Name = $HtmlSpans|
                ?{ $_ -like '*header-name*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
            Price = $HtmlSpans|
                ?{ $_ -like '*product-price__price*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\s*?.\<)')).Value}
}
$MeinArtikel|ft
pause

Ja ...das vorgehen ist auf dieser Seite etwas anders als bei den Übersichtsseiten, wo die Details in den .Links zu finden waren

Ich hatte ha bereits erwähnt:

Webscrapping funktioniert nur für die entsprechende Webseite 

...und mit Glück auf Ähnlich gestalteten Seiten...🥵

Die Herangehensweise ist jedoch immer die gleiche...

  • Webseite Analysieren (zb in der Entwicklerconsole des Browsers)
  • Möglichst allgemeingültige Strings finden um den Quellcode zu zerlegen und das gewünschte zu filtern
  • beten, dass das Muster auch bei ähnlich gestalteten Seiten passt.

Den Webscrapper der alles kann wird es nicht geben...

Woher ich das weiß:eigene Erfahrung – Ich mach das seit 30 Jahren
 - (Computer, programmieren, IT)

Lukmon22 
Beitragsersteller
 08.08.2022, 11:50

Danke, also bei Douglas klappt das ganze auch super. Probiere ich das selbe bei einem anderen abieter, z.B. Parfumdreams (Seren Filler Anti-Falten Serum von L’Oréal Paris | parfumdreams) geht es nicht. Habe dir mal mein Script angehängt. Weist du hier, was das problem ist ?

$Response = Invoke-WebRequest -URI Seren Filler Anti-Falten Serum von L’Oréal Paris | parfumdreams
$HtmlSpans = $Response.Content -split 'span'|   ?{$_ -like '* itemprop=*' -or '* class=*'}  #schon mal grob alle Spannzeilen rausfiltern.    
$HtmlSpans 
 #Zeilen welche  die gewünschten Informationen enthalten in ein Object ("header-name" und "product-price__price" )
$MeinArtikel = [PSCustomObject]@{
            Name = $HtmlSpans|
                ?{ $_ -like '*name*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
            Price = $HtmlSpans|
                ?{ $_ -like '*premium-subscription-inactive black*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\s*?.\<)')).Value}
}
$MeinArtikel|ft
Erzesel  09.08.2022, 10:55
@Lukmon22
  #korrigiert (eine valide Uri  musst du schon angeben)
$Response = Invoke-WebRequest -URI https://www.parfumdreams.de/LOreal-Paris/Gesichtspflege/Seren/Filler-Anti-Falten-Serum/index_99348.aspx
  #korrigiert: im warum auch immer, in der Preisangabe scheint ein Zeilenvorschub  zu sein ...entfernt... 
  #kein Stern vor ' itemprop=*' und ' class=*' (sonst wird  de Mist, welchen wir wegbekommen wollen wieder mit einbezogen)
  #wir vergleichen ob $_ das  eine enthält oder ob $_ das andere enthält...
  #!!!   $_ -like ' itemprop=*' -or ' class=*' hingegen  vergleicht ob $_ ' itemprop=*' enthält oder ob der string ' class=*' nicht leer '' ist... (da er nicht '' ist  Dein ganzer Where-Ausdruck immer $True)
$HtmlSpans = $Response.Content -split 'span' -replace [Environment]::NewLine,''|?{$_ -like ' itemprop=*' -or $_ -like ' class=*'}
$HtmlSpans 
  #korrigiert... für  price:   nach dem € steht auch noch ein * und danach noch Leerzeichen bis < kommt, also muss der Lookahead geändert werden: (?=\s*?..\s*?\<)
  # da man im Lookbehind nur  Literale (keine Ausdrücke) verwenden kann  muss ich die Leerzeichen vor dem Peis mit -relace killen
$MeinArtikel = [PSCustomObject]@{
            Name = $HtmlSpans|
                ?{ $_ -like '*"name"*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
            Price = $HtmlSpans|
                 ?{ $_ -like '*premium-subscription-inactive black*'}|
                 %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\s*?..\s*?\<)')).Value -replace '\s+',$Null}
}
$MeinArtikel|ft
pause

...aber noch mal zum -or im Where -Ausdruck :

?{$_ -like ' itemprop=*' -or ' class=*'}

Das -or prüft ob zwei verschiedenen Ausdrücke zutreffen.

' itemprop=blah und blubb' -like ' itemprop=*'
# ist True
' schnetteredeng' -like ' itemprop=*' -or 'irgendwas'
# ist  auch True
# denn :
[boolean]'irgendeinString'
# ... ist  natürlich  True
[boolean]''
#nur ein LeerString ist False

...also musst Du komplette Ausdrücke mit -or vergleichen :

' itemprop=blah und blubb' -like ' itemprop=*'  #ist true
#...oder:
' class=blah und blubb' -like ' class=*'  #ist  true 
#          Ausdruck 1                          oder       Ausdruck 2 
' itemprop=blah und blubb' -like ' itemprop=*' -or  ' class=blah und blubb' -like ' class=*'  #ist  true 
Lukmon22 
Beitragsersteller
 12.08.2022, 11:50
@Erzesel

Vielen Dank für die erklärung mit dem -or ausdruck, sehr hilfreich!! Zu deinem korrigierten Script; Dort wird mir auch nicht der Preis angezeigt, nachdem ich es ausgeführt habe. Könntest du da nochmal rüberschauen ?

Erzesel  12.08.2022, 13:11
@Lukmon22

Ich zitiere mich nochmal selber:

Webscrapping funktioniert nur für die entsprechende Webseite und auch nur so lange bis der Eigentümer den Code ändert.

...und genau das ist in der Zwischenzeit geschehen...

 class="icon s premium-subscription-logo-active top-navigation-sub" /></li>                <li class="premium-subscription-inactive"><div class="bubble grid-aligned">    <div class="bubble-pointer"></div>    <div class="inner-bubble clearfix">        <div id="PremiumSubscriptionBubble">    <div class="mbs1 clearfix">        <
 class="tag tag-premium mts05 premium-subscription-active hidden">PREMIUM</
 class="premium-subscription-inactive">        GP: 473,67 €* / 1000 ml      </
 class="premium-subscription-active hidden">        GP: 426,33 €* / 1000 ml      </
 class="show-when-no-js">30 ml</
 class="availability-dot availability-Available"></
!!!hier!!!: class="premium-subscription-inactive">    14,21 €*  </
 class="premium-subscription-active font-premium hidden">    12,79 €*  </
 class="font-headline-sub no-wrap" >UVP 18,95 €*</

zuvor:

class="premium-subscription-inactive black">    14,21 

Damit trifft natürlich der Zeilenlfilter nicht mehr. Schlimmer noch, da sich dergleiche Filterstring ("premium-subscription-inactive") nun auf mehrere Zeilen anwenden lässt und das Regex innerhab diese Zeilen 10mal matched wird aus dem erwarteten String für den Preis ein StringArray mit mehreren Einträgen...

</l

<divclass="bubblegrid-aligned

</di




GP:473,67€*/1000
14,21

Da das letzte Match zufällig der erwartete Preis ist, genügt außer der Änderung des Zeilenfilter ein ...|select -last 1:

$MeinArtikel = [PSCustomObject]@{
            Name = $HtmlSpans|
                ?{ $_ -like '*"name"*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
            Price = $HtmlSpans|
                 ?{ $_ -like '*premium-subscription-inactive*'}|
                 %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\s*?..\s*?\<)')).Value -replace '\s+',$Null}|select -last 1
}

Solche Änderungen können täglich oder gar Stündlich in Erscheinung treten. Natürlich interessieren sich die Programmierer solcher Shopseiten herzlich wenig darum, ob ihre Seite "scraperfreundlich" ist. Im Gegenteil, unser kleines Script wird nicht das einzige sein, welches ohne Nachfrage einfach automatisiert Daten abgreift. In ähnlicher weise arbeiten auch Preisvergleichbots. ...und die sind nicht in jedem fall erwünscht...

Ich weiß nicht, was Du letztendlich geplant hast. Eine universelle Lösung gibt es nicht ...

...ich kann auch nicht bei jeder kleinen Änderung an den beobachteten Seiten "hinterherhecheln".

Das ist Dein Projekt. Ich habe Dir gezeigt wie man ungefähr vorgehen muss und worauf man achten muss. Wenn ich den kleinen "Hokuspokus" auch noch supporten soll, geht das dann doch zu weit.

Lukmon22 
Beitragsersteller
 13.09.2022, 10:12
@Erzesel

Vielen Dank, sehr große hilfe gewesen! Ich schätze die mühe sehr und es hat mir extrem geholfen. Ich habe selber nochmal recherchiert und habe Regex an sich auch verstanden. Nur den teil:

Matches($_, '(?<=\>)(.*?)(?=\s*?..\s*?\<)'))

verstehe ich nicht so ganz. Matches ist klar aber was genau haben all die Klammern für eine bedeutung bzw für eine Aufgabe ? Ich kenne es nur so, dass dort z.B. \d\d\d für eine gewisse Zeichenabfolge steht.

Erzesel  13.09.2022, 16:08
@Lukmon22

Klammern kenzeichnen Gruppen.

bei dem Ausdruck handelt es sich um einen Lookarround.

der Ausdruck (?<=davor\s)(.*?)(?=\sdanach) mit einfachereren Ausdrücken erklärt erklärt:

  • (.*?) ist das "Herz" die Zeichengruppe, welche zurückgegeben werden soll. (hier beliebig keine oder mehr Zeichen, so wenig wie möglich )
  • (?<=davor\s) davor ist der Lookbehind (Blick zurück), wenn das "Herz" auch ohne das drumherum matched. (hier: das Wort "davor" gefolgt von einem Leerzeichen.
  • (?=\sdanach) ist der lookahead (Blick voraus) , dito (ein Leerzeichen gefolgt vom Wort "danach"

sowohl Lookbehind als auch Lookahead gehen nicht in den zurückgegebenen String ein, sondern sind lediglich Bewertungskriterien.

'davor das Herz danach
nö Herz passt nö
davor ein andrer Text danach
blub und blahdavor das wird zurückgegeben danach muh und maehhhh'|
%{ ([Regex]::Matches($_, '(?<=davor\s)(.*?)(?=\sdanach)')).Value}

das Folgende funktioniert mit Powershell 5.1 (jedoch nicht mit PSHL 6+)

Im einfachste Fall ein Einzeiler:

((Invoke-WebRequest -URI https://www.douglas.de/de/c/parfum/01).Links|?{$_.class -like 'link link--no-decoration product-tile__main-link'}).innerText.trim().replace("`n",", ").replace("`r",", ")

So einfach sieht es nur aus, wenn man Powershell und HTML fest im griff hat.

In Wirklichkeit braucht es jede Menge Erfahrung um erstmal den QuellText einer Werbseite per Augenschein zu Analysieren und akzeptable Filterparameter herauszupicken um alles was man nicht haben möchte loszuwerden.

Obiger Einzeiler als "SchulbuchScript":

$Response = Invoke-WebRequest -URI https://www.douglas.de/de/c/parfum/01
$Response.links|
  Where-Object  {$_.class -like 'link link--no-decoration product-tile__main-link'}| #filtere Links der angegebenen Klasse (der manuellen Webseitenanalyse entnommen)
  Foreach-Object {
    $Product = $_.innerText #der InnerText liefert bei Douglas die Produktangaben
    $Product = $Product.trim() #Leerzeilen entfernen
    $Product.replace("`n",", ").replace("`r",", ") #Zeilenvorschübe entfernen und an die Pipe ausgeben
  }
pause

Das sieht so leicht aus... Eine Douglas-Seite hat eine Contentlänge von ca. 1,5 Mio Zeichen.. da muss man schon etwas suchen...

Webscrapping funktioniert nur für die entsprechende Webseite und auch nur so lange bis der Eigentümer den Code ändert. Das heist, alles was in diesem Post an Markern verwendet wird klappt nicht für andere Webseiten.

Obiges eigenet sich nur schwerlich zum Trennen der einzelnen Produktteile/-Preise.

Die Duglasseite hat im innerHTML der Links jedoch einen ganz passable Spartenaufteiling.

Mit etwas String- und RegEx-Akrobatik lassen sich schon ansehnliche Objekte erzeugen, mit denen man auch arbeiten kann

(Einige Produkte haben eine abweichende Klassenkennzeichnung. Diese Berücksichtige ich der Einfachheit halber nicht, so das nicht alle Angaben perfekt passen)

Das soll nur zeigen wie man's macht und nicht eine Komplettlösung sein.

$Response = Invoke-WebRequest -URI https://www.douglas.de/de/c/parfum/01
$Artikel = $Response.links|
    Where-Object   {$_.class -like 'link link--no-decoration product-tile__main-link'}| #filtere Links der angegebenen Klasse (der manuellen Webseitenanalyse entnommen)
    Foreach-Object {
        $ProductHtml = $_.innerHTML.Split([Environment]::NewLine)
            #das innerHtml filtern und zelegen (ein wenig RegexAkrobatik)
        [PSCustomObject]@{
            Brand = $ProductHtml|
                ?{ $_ -like '*product-tile__text product-tile__top-brand*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
            Linie = $ProductHtml|
                ?{ $_ -like '*product-tile__text product-tile__brand-line*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
            Name = $ProductHtml|
                ?{ $_ -like '*product-tile__text product-tile__name*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
            Category = $ProductHtml|
                ?{ $_ -like '*product-tile__text product-tile__category*'}|
                %{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
            BasePrice = $ProductHtml|
                ?{ $_ -like '*price-row__price--original-price*'}|
                %{ ([Regex]::Matches($_, '(?<=__price\>)(.*?)(?=\&nbsp)')).Value}
            DiscountPrice = $ProductHtml|
                ?{ $_ -like '*product-price__discount product-price__discount*'}|
                %{ ([Regex]::Matches($_, '(?<=__price\>)(.*?)(?=\&nbsp)')).Value}
        }
        #Nicht alle  Produkte   habe Preisangaben innerhal der vorgegebenen Stringmuster
    }
 #mal Anzeigen
$Artikel
 # nach csv-datei
$Artikel| Export-Csv -Path 'Artikel.csv' -NoTypeInformation
pause

Wie Du siehst ist ein WebScrapper ein Produkt einer manuellen Quellcodeanalyse und nicht mal innerhalb einer Seite perfekt zu realisieren.

Woher ich das weiß:eigene Erfahrung – Ich mach das seit 30 Jahren

Lukmon22 
Beitragsersteller
 27.07.2022, 11:16

Danke für deine mühe. Einfacher ist es doch, wenn man den Preis für nur ein bestimmtes Produkt möchte oder ?

Erzesel  27.07.2022, 12:02
@Lukmon22

Einfache geht's fast nicht.

Im obigen Beispiel werden stumpfsinnig alle Link-Einträge im HTMLcode herausgefiltert, welche sich auf Produkte der Seite beziehen. Das ist am Einfachsten .Jede Verfeinerung erfordert mehr Filteraufwand.

zB. (Code wie Oben)

 #Filtere Properties  .Brand auf den (Teil)String 'Armani' und  die Produktlinie auf Code
$Artikel|
    ?{($_.Brand -like '*Armani*') -and ($_.Linie -like '*Code*')}

...usw.

Ganz einfache Sache: Je feiner die Selektion, um so komplexer der Code...

Lukmon22 
Beitragsersteller
 01.08.2022, 08:58
@Erzesel

Eine Frage habe ich noch; '(?<=\>)(.*?)(?=\<)'. Wie kommst du auf diese Zeichenfolge bzw. wo liest du das aus ? Kannst du das mit einem Screenshot einmal auf der Douglas Seite zeigen ?

Erzesel  01.08.2022, 12:01
@Lukmon22

den String '(?<=\>)(.*?)(?=\<)' wirst du auch nicht im Quelltext des HTML finden.

Der Code [Regex]::Matches($StringVar,Pattern).Value besagt ja schon das ich einen String nach einem bestimmten Zeichenmuster (Pattern) auf Treffer (Matches) durchsuche ....

Ich verwende die .Net-Schreibweise weil diese mir am besten liegt (Ansichtssache).

Nun zum verwendeten RegEx-Pattern... Schritt für Schritt aufgelöst:

  • Das "Herz" (?<=\>)(.*?)(?=\<) ist der Ausdruck, welcher als Treffer zurückgegeben werden soll . .*? bedeutet jedes beliebige Zeichen ,kein oder mehrmals, so wenig wie möglich.
  • (?<=\>)(.*?)(?=\<) der erste Ausdruck ist ein "positive Lockbehind" wenn das "Herz" einen Treffer landet (tut es in unserm fall immer), schaut sich sich die Zeichen davor an ob diese mit dem Lookbehind-Wert übereinstimmen hier nur das Zeichen > .
  • (?<=\>)(.*?)(?=\<) ... passen die ersten beiden Voraussetzungen ">beliebig", wird mit eine Lookahead nachgeschaut ob dem "Herz" de im Lookahead definierte Ausdruck folgt (hier nur das Zeichen < .

Da es sich bei den Zeichen <> um Steuerzeichen handelt, werden diese durch einen Backslash\ als normaler Text maskiert (escaped).

Unser Patter sucht also '>irgendwas<', gibt jedoch nur 'irgendwas' als Wert(.Value) zurück.

demo.ps1

   #Liste der zu durchsuchenden Strings
$MyStringsList=@(
   '<DIV class="product-tile__text product-tile__top-brand">Narciso Rodriguez</DIV>',
   '<DIV class="product-tile__text product-tile__brand-line">for her</DIV>',
   'wumpe>Rumpelheinz<Mehlpampe',
   'der passt nicht!blah und blubb<rattazong',
   'und der scho garnicht'
)
$MyStringsList|%{ ([Regex]::Matches($_, '(?<=\>)(.*?)(?=\<)')).Value}
pause

Ich könnte da einen guten Regexgenerator empfehlen 🤣🤣🤣: https://images.gutefrage.net/media/fragen-antworten/bilder/376989534/0_full.webp?v=1607041881000