centralhighlands.rb 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. # Central Highlands Council — Development Applications (site page)
  2. require "nokogiri"
  3. require "cgi"
  4. require_relative "../lib/http"
  5. require_relative "../lib/util"
  6. require_relative "../lib/scraper_helpers"
  7. TABLE = ENV.fetch("TABLE_NAME") # run_all.sh -> da_centralhighlands
  8. URL = "https://centralhighlands.tas.gov.au/development-applications/"
  9. DB.ensure_table!(TABLE)
  10. # DA 2025/42, DA2025/00042, etc.
  11. REF_RX = %r{\bDA\s*(20\d{2})\s*/\s*([A-Za-z0-9\-_.]+)}i
  12. def extract_ref(str)
  13. s = CGI.unescape(str.to_s)
  14. if (m = s.match(%r{\bDA\s*(20\d{2})\s*/\s*([A-Za-z0-9\-_.]+)}i))
  15. return "DA #{m[1]} / #{m[2]}"
  16. end
  17. if (m = s.match(%r{\bDA(20\d{2})\s*[-\/]?\s*([0-9]{3,})\b}i))
  18. return "DA #{m[1]} / #{m[2]}"
  19. end
  20. nil
  21. end
  22. def extract_addr(text)
  23. if (m = text.match(/Location:\s*(.+?)(?:\s*\/|\s{2,}|$)/i))
  24. m[1].strip
  25. else
  26. ""
  27. end
  28. end
  29. def extract_proposal(text)
  30. if (m = text.match(/Proposal:\s*(.+?)(?:\s{2,}|$)/i))
  31. m[1].strip
  32. else
  33. ""
  34. end
  35. end
  36. def extract_close_raw(text)
  37. s = text.gsub(/\s+/, " ")
  38. # "… until 20 August 2025"
  39. if (m = s.match(/\buntil\s+([0-9]{1,2}\s+[A-Za-z]{3,}\s+[0-9]{4})\b/i))
  40. return m[1]
  41. end
  42. # fallback date tokens
  43. return $1 if s =~ /(\b[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}\b)/
  44. return $1 if s =~ /(\b[0-9]{1,2}\s+[A-Za-z]{3,}\s+[0-9]{4}\b)/
  45. ""
  46. end
  47. # Central Highlands Council's site has been unreachable (connection timeout).
  48. # DAs for this council are also published on PlanBuild (council code CEH),
  49. # so planbuild.rb covers this council independently.
  50. html = begin
  51. Http.get(URL)
  52. rescue StandardError => e
  53. Log.warn "centralhighlands", "Failed to fetch #{URL}: #{e.class} #{e.message}. DAs are available via planbuild.rb (council code CEH)."
  54. puts "Done #{TABLE}. Saved 0 item(s) — site unreachable."
  55. exit 0
  56. end
  57. if html.include?("Just a moment") || html.include?("Enable JavaScript and cookies")
  58. Log.warn "centralhighlands", "Site is returning a Cloudflare challenge page — cannot scrape without browser-level JS execution. DAs for this council are available via planbuild.rb (council code CEH)."
  59. puts "Done #{TABLE}. Saved 0 item(s) — site blocked by Cloudflare."
  60. exit 0
  61. end
  62. doc = Nokogiri::HTML(html)
  63. container = doc.at_css("main, .entry-content, article") || doc
  64. # Grab anchors that look like the advertised docs
  65. links = container.css("a").select { |a|
  66. a.text =~ /click here to view application/i || a["href"].to_s.downcase.end_with?(".pdf")
  67. }
  68. puts "Found #{links.length} candidate link(s) for #{TABLE}"
  69. saved = 0
  70. links.each_with_index do |a, idx|
  71. pdf = abs_url(URL, a["href"])
  72. # Walk up to a nearby block and use its text to find fields
  73. host = a.ancestors("p, li, div").first || a.parent
  74. text = host ? host.text.strip : ""
  75. text = text.gsub(/\s+/, " ")
  76. address = extract_addr(text)
  77. description = extract_proposal(text)
  78. on_notice_raw = extract_close_raw(text)
  79. on_notice = Util.parse_aus_date(on_notice_raw)
  80. # Reference: proposal first, then file name, then surrounding text
  81. ref = extract_ref(description) || extract_ref(File.basename(pdf)) || extract_ref(text)
  82. # If we still have no address, fall back to a slice of the text
  83. address = text[0, 140] if address.empty?
  84. next if address.empty? || ref.nil?
  85. upsert_and_enrich!(
  86. table: TABLE,
  87. row: {
  88. description: description.empty? ? "Development Application" : description,
  89. date_received: on_notice,
  90. date_received_raw: on_notice_raw,
  91. address: address,
  92. council_reference: ref,
  93. applicant: "",
  94. owner: ""
  95. },
  96. extras: { document_url: pdf }
  97. )
  98. saved += 1
  99. end
  100. puts "Done #{TABLE}. Saved #{saved} item(s)."