#!/usr/bin/env ruby # encoding: utf-8 require "brand2csv/version" require 'mechanize' require 'prettyprint' require 'optparse' require 'csv' module Brand2csv class Marke < Struct.new(:name, :markennummer, :inhaber, :land, :hinterlegungsdatum, :zeile_1, :zeile_2, :zeile_3, :zeile_4, :zeile_5, :plz, :ort) end class Swissreg # Weitere gesehene Fehler BekannteFehler = ['Das Datum ist ung', # ültig' 'Erweiterte Suche', 'Vereinfachte Trefferliste anzeigen', 'Es wurden keine Daten gefunden.', 'Die Suchkriterien sind teilweise unzul', # ässig', 'Geben Sie mindestens ein Suchkriterium ein', 'Die Suche wurde abgebrochen, da die maximale Suchzeit von 60 Sekunden', ] Base_uri = 'https://www.swissreg.ch' Start_uri = "#{Base_uri}/srclient/faces/jsp/start.jsp" AddressRegexp = /^(\d\d\d\d)\W*(.*)/ LineSplit = ', ' DefaultCountry = 'Schweiz' # Angezeigte Spalten "id_swissreg:mainContent:id_ckbTMChoice" TMChoiceFields = [ "tm_lbl_tm_text", # Marke # "tm_lbl_state"], # Status # "tm_lbl_nizza_class"], # Nizza Klassifikation Nr. # "tm_lbl_no"], # disabled="disabled"], # Nummer "tm_lbl_applicant", # Inhaber/in "tm_lbl_country", # Land (Inhaber/in) # "tm_lbl_agent", # Vertreter/in # "tm_lbl_licensee"], # Lizenznehmer/in "tm_lbl_app_date", # Hinterlegungsdatum ] attr_accessor :marke def initialize(timespan) @timespan = timespan @agent = Mechanize.new { |agent| # agent.user_agent_alias = 'Mac Safari' agent.user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0' # agent.redirection_limit = 5 agent.verify_mode = OpenSSL::SSL::VERIFY_NONE } @results = [] @errors = Hash.new @lastResponse = nil @lastDetail =nil @counterDetails = 0 @marke = 'zzzyyzzzzyzzyz*' # => Fehlermeldung: Es wurden keine Daten gefunden # asp* => 138 records werden geholt # a* => Es wurden 25,490 Treffer gefunden. Davon werden 10000 zufällig ausgewählte Schutztitel angezeigt. Bitte schränken Sie Ihre Suche weiter ein. # Ab 501 Treffer wird eine vereinfachte Trefferliste angezeigt. # asp* => 138 records werden geholt @marke = nil # => Fehlermeldung: Geben Sie mindestens ein Suchkriterium ein @marke = 'asp*' @number = '500000' @number = nil # @marke = "*WEIH*" @hitsPerPage = 100 end def writeResponse(filename, body) if defined?(RSpec) ausgabe = File.open(filename, 'w+') ausgabe.puts body ausgabe.close else puts "Skipping writing #{filename}" if $VERBOSE end end def view_state(response) if match = /javax.faces.ViewState.*?value="([^"]+)"/u.match(response.force_encoding('utf-8')) match[1] else "" end end def checkErrors(body) BekannteFehler.each { |errMsg| if body.to_s.index(errMsg) puts "Tut mir leid. Suche wurde mit Fehlermeldung <#{errMsg}> abgebrochen." exit 2 end } end def parse_swissreg(timespan = @timespan, # sollte 377 Treffer ergeben, für 01.06.2007-10.06.2007, 559271 wurde in diesem Zeitraum registriert marke = @marke, nummer =@number) # nummer = "559271" ergibt genau einen treffer @agent.get Start_uri # get a cookie for the session content = @agent.get_file Start_uri FileUtils.makedirs 'mechanize' writeResponse('mechanize/main.html', content) @state = view_state(content) data = [ ["autoScroll", "0,0"], ["id_swissreg:_link_hidden_", ""], ["id_swissreg_SUBMIT", "1"], ["id_swissreg:_idcl", "id_swissreg_sub_nav_ipiNavigation_item0"], ["javax.faces.ViewState", @state], ] content = @agent.post(Start_uri, data) writeResponse('mechanize/einfache_suche.html', content.body) data = [ ["autoScroll", "0,0"], ["id_swissreg:_link_hidden_", ""], ["id_swissreg_SUBMIT", "1"], ["id_swissreg:_idcl", "id_swissreg_sub_nav_ipiNavigation_item0_item3"], ["javax.faces.ViewState", @state], ] # sr1 ist die einfache suche, sr3 die erweiterte Suche @path = "/srclient/faces/jsp/trademark/sr3.jsp" response = @agent.post(Base_uri + @path, data) writeResponse('mechanize/erweiterte_suche.html', response.body) # Bis hier alles okay @criteria = [ ["autoScroll", "0,829"], ["id_swissreg:_link_hidden_", ""], ["id_swissreg:mainContent:id_ckbTMState", "1"], # "Hängige Gesuche 1 # ["id_swissreg:mainContent:id_ckbTMState", "2"], # "Gelöschte Gesuche 2 ["id_swissreg:mainContent:id_ckbTMState", "3"], # aktive Marken 3 # ["id_swissreg:mainContent:id_ckbTMState", "4"], # gelöschte Marken 4 ["id_swissreg:mainContent:id_cbxCountry", "_ALL"], # Auswahl Länder _ALL # ["id_swissreg:mainContent:id_txf_tm_no", ""], # Marken Nr ["id_swissreg:mainContent:id_txf_tm_no", nummer],# Marken Nr ["id_swissreg:mainContent:id_txf_app_no", ""], # Gesuch Nr. ["id_swissreg:mainContent:id_txf_tm_text", marke], ["id_swissreg:mainContent:id_txf_applicant", ""], # Inhaber/in ["id_swissreg:mainContent:id_txf_agent", ""], # Vertreter/in ["id_swissreg:mainContent:id_txf_licensee", ""], # Lizenznehmer ["id_swissreg:mainContent:id_txf_nizza_class", ""], # Nizza Klassifikation Nr. # ["id_swissreg:mainContent:id_txf_appDate", timespan], # Hinterlegungsdatum ["id_swissreg:mainContent:id_txf_appDate", timespan] , ["id_swissreg:mainContent:id_txf_expiryDate", ""], # Ablauf Schutzfrist # Markenart: Individualmarke 1 Kollektivmarke 2 Garantiemarke 3 ["id_swissreg:mainContent:id_cbxTMTypeGrp", "_ALL"], # Markenart ["id_swissreg:mainContent:id_cbxTMForm", "_ALL"], # Markentyp ["id_swissreg:mainContent:id_cbxTMColorClaim", "_ALL"], # Farbanspruch ["id_swissreg:mainContent:id_txf_pub_date", ""], # Publikationsdatum # info zu Publikationsgrund id_swissreg:mainContent:id_ckbTMPubReason ["id_swissreg:mainContent:id_ckbTMPubReason", "1"], #Neueintragungen ["id_swissreg:mainContent:id_ckbTMPubReason", "2"], #Berichtigungen ["id_swissreg:mainContent:id_ckbTMPubReason", "3"], #Verlängerungen ["id_swissreg:mainContent:id_ckbTMPubReason", "4"], #Löschungen ["id_swissreg:mainContent:id_ckbTMPubReason", "5"], #Inhaberänderungen ["id_swissreg:mainContent:id_ckbTMPubReason", "6"], #Vertreteränderungen ["id_swissreg:mainContent:id_ckbTMPubReason", "7"], #Lizenzänderungen ["id_swissreg:mainContent:id_ckbTMPubReason", "8"], #Weitere Registeränderungen # ["id_swissreg:mainContent:id_ckbTMEmptyHits", "0"], # Leere Trefferliste anzeigen # "id_swissreg:mainContent:id_cbxFormatChoice" 2 = Publikationsansicht 1 = Registeransicht ["id_swissreg:mainContent:id_cbxFormatChoice", "1"], ["id_swissreg:mainContent:id_cbxHitsPerPage", @hitsPerPage], # Treffer pro Seite ] TMChoiceFields.each{ | field2display| @criteria << ["id_swissreg:mainContent:id_ckbTMChoice", field2display] } # id_swissreg:mainContent:id_ckbTMChoice tm_lbl_tm_text puts "Marke ist #{marke}" if marke # Wortlaut der Marke puts "Hinterlegungsdatum ist #{timespan}" if $VERBOSE and timespan puts "nummer ist #{timespan}" if nummer @criteria << ["id_swissreg:mainContent:sub_fieldset:id_submit", "suchen"] @criteria << ["id_swissreg_SUBMIT", "1"] @criteria << ["id_swissreg:_idcl", ""] @criteria << ["id_swissreg:_link_hidden_", ""] @criteria << ["javax.faces.ViewState", @state] @path = "/srclient/faces/jsp/trademark/sr3.jsp" response = @agent.post(Base_uri + @path, @criteria) writeResponse('mechanize/resultate_1.html', response.body) checkErrors(response.body) @lastResponse = response end def parseAddress(nummer, inhaber) zeile_1, zeile_2, zeile_3, zeile_4, zeile_5 = inhaber.split(LineSplit) ort = nil plz = nil if m = AddressRegexp.match(zeile_2) zeile_2 = nil plz = m[1]; ort = m[2] elsif m = AddressRegexp.match(zeile_3) zeile_3 = nil plz = m[1]; ort = m[2] elsif m = AddressRegexp.match(zeile_4) zeile_4 = nil plz = m[1]; ort = m[2] elsif m = AddressRegexp.match(zeile_5) zeile_5 = nil plz = m[1]; ort = m[2] else puts "Achtung! Konnte Marke #{nummer} mit Inhaber #{inhaber} nicht parsen" if $VERBOSE return nil, nil, nil, nil, nil, nil, nil, nil end return zeile_1, zeile_2, zeile_3, zeile_4, zeile_5, plz, ort end def fetchDetails(nummer) # takes a long time! @counterDetails += 1 filename = "mechanize/detail_#{nummer}.html" if File.exists?(filename) doc = Nokogiri::Slop(File.open(filename)) else url = "https://www.swissreg.ch/srclient/faces/jsp/trademark/sr300.jsp?language=de§ion=tm&id=#{nummer}" pp "Opening #{url}" if $VERBOSE content = @agent.get_file url writeResponse("mechanize/detail_#{nummer}.html", content) doc = Nokogiri::Slop(content) end puts "Bitte um Geduld. Hole Adressdetails für Marke #{nummer}. (#{@counterDetails} von #{@errors.size})" path_name = "//html/body/form/div/div/fieldset/div/table/tbody/tr/td" counter = 0 doc.xpath(path_name).each{ |td| pp "#{counter}: #{td.text}" if $VERBOSE counter += 1 next unless /^inhaber/i.match(td.text) zeilen = [] doc.xpath(path_name)[counter].children.each{ |child| zeilen << child.text.gsub(LineSplit,'. ') unless child.text.length == 0 } # avoid adding <br> if info = @errors[nummer] info.inhaber = zeilen.join(LineSplit) info.zeile_1, info.zeile_2, info.zeile_3, info.zeile_4, zeile_5, info.plz, info.ort = parseAddress(nummer, info.inhaber) @results << info else bezeichnung = doc.xpath(path_name)[15] inhaber = zeilen.join(LineSplit) zeile_1, zeile_2, zeile_3, zeile_4, zeile_5, plz, ort = parseAddress(nummer, inhaber) hinterlegungsdatum = doc.xpath(path_name)[7] marke = Marke.new(bezeichnung, nummer, inhaber, DefaultCountry, hinterlegungsdatum, zeile_1, zeile_2, zeile_3, zeile_4, zeile_5, plz, ort ) @results << marke end } end def fetchresult(filename = nil, counter = 1) if filename doc = Nokogiri::Slop(File.open(filename)) else doc = Nokogiri::Slop(@lastResponse.body) end nrFailures = 0 counter += 1 puts "fetchresult. Counter #{counter} already #{@results.size} Datensätze für die Zeitspanne '#{@timespan}'" path_name = "//html/body/form/div/div/fieldset/table/tbody/tr/td/table/tr/td" hasNext = false doc.xpath(path_name).each{ |elem| if /scroll_1idx#{counter}/.match(elem.to_s) hasNext = true break end } path_name = "//html/body/form/div/div/fieldset/table/tbody/tr/td/table/tbody/tr" doc.xpath(path_name).each{ |elem| bezeichnung = elem.elements[1].text land = elem.elements[4].text next unless /#{DefaultCountry}/i.match(land) inhaber = elem.elements[3].text nummer = elem.elements[2].text if bezeichnung.length == 0 bezeichnung = elem.children[1].children[0].children[0].children[0].attribute('src').to_s end zeile_1, zeile_2, zeile_3, zeile_4, zeile_5, plz, ort = parseAddress(nummer, inhaber) if zeile_1 @results << Marke.new(bezeichnung, elem.elements[2].text, elem.elements[3].text, land, elem.elements[5].text, zeile_1, zeile_2, zeile_3, zeile_4, zeile_5, plz, ort ) else nrFailures += 1 @errors[nummer] = Marke.new(bezeichnung, elem.elements[2].text, elem.elements[3].text, land, elem.elements[5].text, zeile_1, zeile_2, zeile_3, zeile_4, zeile_5, plz, ort ) end } if doc.xpath(path_name) if hasNext @path = "/srclient/faces/jsp/trademark/sr30.jsp" puts "Calling sub #{counter} with #{@path}" if $VERBOSE data = [ ["autoScroll", "0,0"], ["id_swissreg:mainContent:id_sub_options_result:sub_fieldset:id_cbxHitsPerPage", @hitsPerPage], # ["id_swissreg:mainContent:vivian", "TRADEMARK REGISTER SEARCH TIMES: QUERY=[20] SELECT=[823] SERVER=[846] DELEGATE=[861] (HITS=[96])"], ["id_swissreg_SUBMIT", "1"], ["id_swissreg:_idcl", "id_swissreg:mainContent:scroll_1idx#{counter}"], ["id_swissreg:mainContent:scroll_1", "idx#{counter}"], ["tmMainId", ""], ["id_swissreg:_link_hidden_ "], ["javax.faces.ViewState", @state], ] TMChoiceFields.each{ | field2display| data << ["id_swissreg:mainContent:id_sub_options_result:id_ckbTMChoice", field2display] } response = @agent.post(Base_uri + @path, data) writeResponse("mechanize/resultate_#{counter}.html", response.body) checkErrors(response.body) @lastResponse = response fetchresult(nil, counter) else puts "Es gab #{nrFailures} Fehler beim Lesen von #{filename}" if $VERBOSE puts "Fand #{@results.size} Datensätze für die Zeitspanne '#{@timespan}'. Von #{@errors.size} muss die Adresse noch geholt werden." end end def emitCsv(filename='ausgabe.csv') return if @results.size == 0 CSV.open(filename, 'w', {:headers=>@results[0].members, :write_headers => true}) do |csv| @results.each{ |x| csv << x } end puts "Speicherte #{@results.size} gefunden Datensätze für die Zeitspanne '#{@timespan}' in #{filename}" end def fetchMissingDetails @errors.each{ |markennummer, info| fetchDetails(markennummer) } end end # class Swissreg def Brand2csv::run(timespan) session = Swissreg.new(timespan) session.parse_swissreg session.fetchresult session.fetchMissingDetails session.emitCsv end end # module Brand2csv