saleIds) {
int foundCount = 0;
- // Simple regex-based parsing for auction links
- java.util.regex.Pattern linkPattern = java.util.regex.Pattern.compile(
- "href=\"(/a/[^\"]+A[17]-(\\d+)[^\"]*)\"");
- java.util.regex.Matcher linkMatcher = linkPattern.matcher(html);
+ try {
+ org.jsoup.nodes.Document doc = org.jsoup.Jsoup.parse(html);
- while (linkMatcher.find()) {
- String href = linkMatcher.group(1);
- int auctionId = Integer.parseInt(linkMatcher.group(2));
+ // Find all auction links (format: /a/title-A1-12345 or /a/title-A7-12345)
+ org.jsoup.select.Elements auctionLinks = doc.select("a[href^='/a/']");
- // Avoid duplicates
- if (saleIds.contains(auctionId)) {
- continue;
- }
-
- // Check if this auction is Dutch (location contains ", NL")
- if (isDutchAuction(html, href)) {
- saleIds.add(auctionId);
- foundCount++;
- System.out.println(" Found Dutch auction: " + auctionId + " - " + href);
+ for (org.jsoup.nodes.Element link : auctionLinks) {
+ String href = link.attr("href");
+
+ // Extract auction ID from URL
+ java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("/a/.*?-A([17])-(\\d+)");
+ java.util.regex.Matcher matcher = pattern.matcher(href);
+
+ if (!matcher.find()) {
+ continue;
+ }
+
+ String typeNum = matcher.group(1);
+ int auctionId = Integer.parseInt(matcher.group(2));
+
+ // Skip duplicates
+ if (saleIds.contains(auctionId)) {
+ continue;
+ }
+
+ // Extract auction info using JSoup
+ AuctionInfo auction = extractAuctionInfo(link, href, auctionId, "A" + typeNum);
+
+ // Only keep Dutch auctions
+ if (auction != null && "NL".equals(auction.country)) {
+ saleIds.add(auctionId);
+ foundCount++;
+
+ // Save to database
+ try {
+ db.upsertAuction(auction);
+ System.out.println(" Found Dutch auction: " + auctionId + " - " + auction.title + " (" + auction.location + ")");
+ } catch (SQLException e) {
+ System.err.println(" Failed to save auction: " + e.getMessage());
+ }
+ }
}
+ } catch (Exception e) {
+ System.err.println(" Error parsing HTML: " + e.getMessage());
}
return foundCount;
}
/**
- * Checks if an auction is located in the Netherlands
+ * Extracts auction information from a link element using JSoup
+ * This method intelligently parses the HTML structure to extract:
+ * - Title
+ * - Location (city and country)
+ * - Lot count (if available)
*/
- private boolean isDutchAuction(String html, String href) {
- int hrefPos = html.indexOf(href);
- if (hrefPos == -1) return false;
+ private AuctionInfo extractAuctionInfo(org.jsoup.nodes.Element link, String href, int auctionId, String type) {
+ AuctionInfo auction = new AuctionInfo();
+ auction.auctionId = auctionId;
+ auction.type = type;
+ auction.url = "https://www.troostwijkauctions.com" + href;
- // Look at 1000 characters before and after the href for location info
- int startPos = Math.max(hrefPos - 500, 0);
- int endPos = Math.min(hrefPos + 1000, html.length());
- String context = html.substring(startPos, endPos);
+ // Extract title from href (convert kebab-case to title)
+ java.util.regex.Pattern titlePattern = java.util.regex.Pattern.compile("/a/(.+?)-A[17]-");
+ java.util.regex.Matcher titleMatcher = titlePattern.matcher(href);
+ if (titleMatcher.find()) {
+ String slug = titleMatcher.group(1);
+ auction.title = slug.replace("-", " ");
+ // Capitalize first letter
+ if (!auction.title.isEmpty()) {
+ auction.title = auction.title.substring(0, 1).toUpperCase() + auction.title.substring(1);
+ }
+ } else {
+ auction.title = "Unknown Auction";
+ }
- // Look for ", NL" pattern
- return context.contains(", NL");
+ // Try to find title in link text (more accurate)
+ String linkText = link.text();
+ if (!linkText.isEmpty() && !linkText.matches(".*\\d+.*")) {
+ // If link text doesn't contain numbers, it's likely the title
+ String[] parts = linkText.split(",|\\d+");
+ if (parts.length > 0 && parts[0].trim().length() > 5) {
+ auction.title = parts[0].trim();
+ }
+ }
+
+ // Extract location using JSoup selectors
+ // Look for tags that contain location info
+ org.jsoup.select.Elements locationElements = link.select("p");
+ for (org.jsoup.nodes.Element p : locationElements) {
+ String text = p.text();
+
+ // Pattern: "City, Country" or "City, Region, Country"
+ if (text.matches(".*[A-Z]{2}$")) {
+ // Ends with 2-letter country code
+ String countryCode = text.substring(text.length() - 2);
+ String cityPart = text.substring(0, text.length() - 2).trim();
+
+ // Remove trailing comma or whitespace
+ cityPart = cityPart.replaceAll("[,\\s]+$", "");
+
+ auction.country = countryCode;
+ auction.city = cityPart;
+ auction.location = cityPart + ", " + countryCode;
+ break;
+ }
+ }
+
+ // Fallback: check HTML content directly
+ if (auction.country == null) {
+ String html = link.html();
+ java.util.regex.Pattern locPattern = java.util.regex.Pattern.compile(
+ "([A-Za-z][A-Za-z\\s,\\-']+?)\\s*(?:)?\\s*\\s*([A-Z]{2})(?![A-Za-z])");
+ java.util.regex.Matcher locMatcher = locPattern.matcher(html);
+
+ if (locMatcher.find()) {
+ String city = locMatcher.group(1).trim().replaceAll(",$", "");
+ String country = locMatcher.group(2);
+ auction.city = city;
+ auction.country = country;
+ auction.location = city + ", " + country;
+ }
+ }
+
+ // Extract lot count if available (kavels/lots)
+ org.jsoup.select.Elements textElements = link.select("*");
+ for (org.jsoup.nodes.Element elem : textElements) {
+ String text = elem.ownText();
+ if (text.matches("\\d+\\s+(?:kavel|lot|item)s?.*")) {
+ java.util.regex.Pattern countPattern = java.util.regex.Pattern.compile("(\\d+)");
+ java.util.regex.Matcher countMatcher = countPattern.matcher(text);
+ if (countMatcher.find()) {
+ auction.lotCount = Integer.parseInt(countMatcher.group(1));
+ break;
+ }
+ }
+ }
+
+ return auction;
}
/**
@@ -664,13 +782,34 @@ public class TroostwijkScraper {
// Domain classes and services
// ----------------------------------------------------------------------
+ /**
+ * Represents auction metadata (veiling informatie)
+ */
+ public static class AuctionInfo {
+ public int auctionId; // Unique auction ID (from URL)
+ public String title; // Auction title
+ public String location; // Location (e.g., "Amsterdam, NL")
+ public String city; // City name
+ public String country; // Country code (e.g., "NL")
+ public String url; // Full auction URL
+ public String type; // Auction type (A1 or A7)
+ public int lotCount; // Number of lots/kavels
+ public LocalDateTime closingTime; // Closing time if available
+
+ @Override
+ public String toString() {
+ return String.format("Auction{id=%d, type=%s, title='%s', location='%s', lots=%d, url='%s'}",
+ auctionId, type, title, location, lotCount, url);
+ }
+ }
+
/**
* Simple POJO representing a lot (kavel) in an auction. It keeps track
* of the sale it belongs to, current bid and closing time. The method
* minutesUntilClose computes how many minutes remain until the lot closes.
*/
static class Lot {
-
+
int saleId;
int lotId;
String title;
@@ -684,7 +823,7 @@ public class TroostwijkScraper {
String url;
LocalDateTime closingTime; // null if unknown
boolean closingNotified;
-
+
long minutesUntilClose() {
if (closingTime == null) return Long.MAX_VALUE;
return java.time.Duration.between(LocalDateTime.now(), closingTime).toMinutes();
@@ -704,18 +843,33 @@ public class TroostwijkScraper {
}
/**
* Creates tables if they do not already exist. The schema includes
- * tables for sales, lots, images, and object labels. This method is
+ * tables for auctions, lots, images, and object labels. This method is
* idempotent; it can be called multiple times.
*/
void ensureSchema() throws SQLException {
try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) {
- // Sales table
+ // Auctions table (veilingen)
+ stmt.execute("CREATE TABLE IF NOT EXISTS auctions ("
+ + "auction_id INTEGER PRIMARY KEY,"
+ + "title TEXT NOT NULL,"
+ + "location TEXT,"
+ + "city TEXT,"
+ + "country TEXT,"
+ + "url TEXT NOT NULL,"
+ + "type TEXT,"
+ + "lot_count INTEGER DEFAULT 0,"
+ + "closing_time TEXT,"
+ + "discovered_at INTEGER" // Unix timestamp
+ + ")");
+
+ // Sales table (legacy - keep for compatibility)
stmt.execute("CREATE TABLE IF NOT EXISTS sales ("
+ "sale_id INTEGER PRIMARY KEY,"
+ "title TEXT,"
+ "location TEXT,"
+ "closing_time TEXT"
+ ")");
+
// Lots table
stmt.execute("CREATE TABLE IF NOT EXISTS lots ("
+ "lot_id INTEGER PRIMARY KEY,"
@@ -731,8 +885,9 @@ public class TroostwijkScraper {
+ "url TEXT,"
+ "closing_time TEXT,"
+ "closing_notified INTEGER DEFAULT 0,"
- + "FOREIGN KEY (sale_id) REFERENCES sales(sale_id)"
+ + "FOREIGN KEY (sale_id) REFERENCES auctions(auction_id)"
+ ")");
+
// Images table
stmt.execute("CREATE TABLE IF NOT EXISTS images ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
@@ -742,8 +897,98 @@ public class TroostwijkScraper {
+ "labels TEXT,"
+ "FOREIGN KEY (lot_id) REFERENCES lots(lot_id)"
+ ")");
+
+ // Create indexes for better query performance
+ stmt.execute("CREATE INDEX IF NOT EXISTS idx_auctions_country ON auctions(country)");
+ stmt.execute("CREATE INDEX IF NOT EXISTS idx_lots_sale_id ON lots(sale_id)");
}
}
+
+ /**
+ * Inserts or updates an auction record
+ */
+ synchronized void upsertAuction(AuctionInfo auction) throws SQLException {
+ String sql = "INSERT INTO auctions (auction_id, title, location, city, country, url, type, lot_count, closing_time, discovered_at)"
+ + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ + " ON CONFLICT(auction_id) DO UPDATE SET "
+ + "title = excluded.title, location = excluded.location, city = excluded.city, "
+ + "country = excluded.country, url = excluded.url, type = excluded.type, "
+ + "lot_count = excluded.lot_count, closing_time = excluded.closing_time";
+
+ try (Connection conn = DriverManager.getConnection(url); PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setInt(1, auction.auctionId);
+ ps.setString(2, auction.title);
+ ps.setString(3, auction.location);
+ ps.setString(4, auction.city);
+ ps.setString(5, auction.country);
+ ps.setString(6, auction.url);
+ ps.setString(7, auction.type);
+ ps.setInt(8, auction.lotCount);
+ ps.setString(9, auction.closingTime != null ? auction.closingTime.toString() : null);
+ ps.setLong(10, Instant.now().getEpochSecond());
+ ps.executeUpdate();
+ }
+ }
+
+ /**
+ * Retrieves all auctions from the database
+ */
+ synchronized List getAllAuctions() throws SQLException {
+ List auctions = new ArrayList<>();
+ String sql = "SELECT auction_id, title, location, city, country, url, type, lot_count, closing_time FROM auctions";
+
+ try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) {
+ ResultSet rs = stmt.executeQuery(sql);
+ while (rs.next()) {
+ AuctionInfo auction = new AuctionInfo();
+ auction.auctionId = rs.getInt("auction_id");
+ auction.title = rs.getString("title");
+ auction.location = rs.getString("location");
+ auction.city = rs.getString("city");
+ auction.country = rs.getString("country");
+ auction.url = rs.getString("url");
+ auction.type = rs.getString("type");
+ auction.lotCount = rs.getInt("lot_count");
+ String closing = rs.getString("closing_time");
+ if (closing != null) {
+ auction.closingTime = LocalDateTime.parse(closing);
+ }
+ auctions.add(auction);
+ }
+ }
+ return auctions;
+ }
+
+ /**
+ * Retrieves auctions by country code
+ */
+ synchronized List getAuctionsByCountry(String countryCode) throws SQLException {
+ List auctions = new ArrayList<>();
+ String sql = "SELECT auction_id, title, location, city, country, url, type, lot_count, closing_time "
+ + "FROM auctions WHERE country = ?";
+
+ try (Connection conn = DriverManager.getConnection(url); PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, countryCode);
+ ResultSet rs = ps.executeQuery();
+ while (rs.next()) {
+ AuctionInfo auction = new AuctionInfo();
+ auction.auctionId = rs.getInt("auction_id");
+ auction.title = rs.getString("title");
+ auction.location = rs.getString("location");
+ auction.city = rs.getString("city");
+ auction.country = rs.getString("country");
+ auction.url = rs.getString("url");
+ auction.type = rs.getString("type");
+ auction.lotCount = rs.getInt("lot_count");
+ String closing = rs.getString("closing_time");
+ if (closing != null) {
+ auction.closingTime = LocalDateTime.parse(closing);
+ }
+ auctions.add(auction);
+ }
+ }
+ return auctions;
+ }
/**
* Inserts or updates a lot record. Uses INSERT OR REPLACE to
diff --git a/src/main/resources/test.html b/src/test/resources/test.html
similarity index 100%
rename from src/main/resources/test.html
rename to src/test/resources/test.html
diff --git a/src/test/resources/test_auctions.html b/src/test/resources/test_auctions.html
new file mode 100644
index 0000000..78af290
--- /dev/null
+++ b/src/test/resources/test_auctions.html
@@ -0,0 +1,205 @@
+Alle veilingen | Troostwijk Auctions
Helpcentrum NL | EURTaal
Nederlands
Nederlands English français polski Deutsch italiano românăValuta weergeven
EUR - Euro
EUR - Euro BGN - Bulgaarse lev CHF - Zwitserse frank CZK - Tsjechische kroon DKK - Deense kroon GBP - Britse pond HUF - Hongaarse forint NOK - Noorse kroon PLN - Poolse zloty RON - Roemeense leu SEK - Zweedse kroon Wisselkoersen worden dagelijks bijgewerkt, niet in realtime.
Alle veilingen 522 resultaten Vandaag 28 nov 25Sluiting van een metaalbewerkingsfabriek – CNC-bewerkingscentra, draadvonkmachine, gereedschapsmachines en meer
Faillissement Carpetright Alkmaar
Faillissement Carpetright Heerhugowaard
Dakleer, OSB, tuinhuizen, tuinplanken, machines en bouwmaterialen
Catering & Horeca Uitverkoop - Hongarije
Liquidatie van de volledige fabriek: buisbewerkingslijnen, diverse werkplaatsartikelen, bakken van verschillende types en nog veel meer!
France Médical Enchères – 100+ lots gemengde medische apparatuur
Saint-Germain-de-la-Grange, FR
Online veiling: Industriële pompen, motoren en hydraulische eenheden - Polen
British Medical Auctions: Zeiss Operating Microscopes
Faillissement Carpetright Zaandam
Exclusieve diamanten en juwelen
Complete inventaris en machines van boekbinderij in verband met bedrijfsbeëindiging
Meerdere locaties (2), NL
Online veiling: optische sorteerder en LDPE-pelletiseermachine - Bulgarije
Oldtimer veiling te Vorden
verzorgings- en massage tafels, kapperstoelen en winkelmeubilair wegens faillissement I-Learning
Britse medische veilingen: 20+ Lots UK-gebaseerde Lifepak 15 en Corpuls Defibrillators
Metaalbewerkingsmachines, bouwmachines en gereedschappen
Meerdere locaties (2), NL
Magazijnveiling
Meerdere locaties (59), BE
Interieur, witgoed en huishoudelijke apparatuur
Keramische- en natuursteen tegels
Buitenspa’s en badkamerproducten
MAN MARC 6 Stoomturbine Incl. Generator en installaties
Morgen 29 nov 25
Voor de best mogelijke ervaring op ons platform gebruiken wij functionele en analytische cookies, en technieken die daarop lijken. Met onze uitgebreide selectie laten we je graag kavels zien die voor jou relevant zijn. Je kunt kiezen voor een gepersonaliseerde ervaring met aanbevelingen en advertenties die beter passen bij jouw interesses. Dit geldt ook voor nieuwsbrieven en notificaties als je die ontvangt. Daarnaast kun je kiezen voor persoonlijke advertenties buiten ons platform.
+
+
+
+
+ We bepalen je interesses door gegevens van je biedingen, favoriete kavels, klantprofiel en informatie van derden te combineren, uiteraard alleen met jouw toestemming. Met cookies en vergelijkbare technieken verzamelen we ook je surfgedrag op onze website. Dit doen we natuurlijk niet als je tracking of cookies hebt uitgeschakeld op je apparaat of in je browser.
+
+
+
+
+ Persoonlijke advertenties buiten ons platform worden getoond door onze partners doordat we versleutelde gegevens delen, cookies en vergelijkbare technieken gebruiken. Zie ook ons
Privacyverklaring en
Cookie FAQ . Wil je profiteren van deze persoonlijke ervaringen binnen en buiten onze site, kies dan voor ‘Alle cookies accepteren’. Als je dit niet wilt, selecteer dan ‘Alleen noodzakelijke cookies accepteren’. Je kunt ook je voorkeuren zelf instellen en later altijd aanpassen in de cookie instellingen.
Voorkeurenmenu Wanneer u een website bezoekt, kan er informatie in uw browser worden opgeslagen of eruit worden opgehaald, voornamelijk in de vorm van cookies. Deze informatie kan over u, uw voorkeuren of uw apparaat zijn en wordt voornamelijk gebruikt om de website correct te laten werken. De informatie identificeert u normaal gesproken niet direct, maar kan u een beter op uw voorkeuren toegesneden surfervaring geven. Omdat we uw recht op privacy respecteren, kunt u er voor kiezen sommige soorten cookies te blokkeren. Klik op de namen voor de verschillende categorieën voor meer informatie en om onze standaardinstellingen te wijzigen. Weest u zich er echter wel van bewust dat het blokkeren van sommige soorten cookies uw ervaring van de website en de door ons aangeboden diensten nadelig kan beïnvloeden.
+
Meer informatie Alle toestaan Cookievoorkeuren beheren Deze cookies stellen de website in staat om extra functies en persoonlijke instellingen aan te bieden. Ze kunnen door ons worden ingesteld of door externe aanbieders van diensten die we op onze pagina’s hebben geplaatst. Als u deze cookies niet toestaat kunnen deze of sommige van deze diensten wellicht niet correct werken.
Cookiedetails
Deze cookies kunnen door onze adverteerders op onze website worden ingesteld. Ze worden wellicht door die bedrijven gebruikt om een profiel van uw interesses samen te stellen en u relevante advertenties op andere websites te tonen. Ze slaan geen directe persoonlijke informatie op, maar ze zijn gebaseerd op unieke identificatoren van uw browser en internetapparaat. Als u deze cookies niet toestaat, zult u minder op u gerichte advertenties zien.
Cookiedetails
Deze cookies stellen ons in staat bezoekers en hun herkomst te tellen zodat we de prestatie van onze website kunnen analyseren en verbeteren. Ze helpen ons te begrijpen welke pagina’s het meest en minst populair zijn en hoe bezoekers zich door de gehele site bewegen. Alle informatie die deze cookies verzamelen wordt geaggregeerd en is daarom anoniem. Als u deze cookies niet toestaat, weten wij niet wanneer u onze site heeft bezocht.
Cookiedetails
Deze cookies kunnen worden gebruikt door de verschillende sociale media die we op onze website hebben geplaatst om u in staat te stellen onze inhoud met uw vrienden en netwerken te delen. Ze kunnen uw browser op andere websites volgen en bouwen een profiel van uw interesses op. Dit kan van invloed zijn op de inhoud en berichten die u op andere websites ziet. Als u deze cookies niet toestaat kunt u de sociale mediaknoppen wellicht niet zien of gebruiken.
Cookiedetails
Deze cookies zijn nodig anders werkt de website niet. Deze cookies kunnen niet worden uitgeschakeld. In de meeste gevallen worden deze cookies alleen gebruikt naar aanleiding van een handeling van u waarmee u in wezen een dienst aanvraagt, bijvoorbeeld uw privacyinstellingen registreren, in de website inloggen of een formulier invullen. U kunt uw browser instellen om deze cookies te blokkeren of om u voor deze cookies te waarschuwen, maar sommige delen van de website zullen dan niet werken. Deze cookies slaan geen persoonlijk identificeerbare informatie op.
Cookiedetails
\ No newline at end of file