Search for and download building records from the Hong Kong Buildings
Department BRAVO (Building Records Access and Viewing On-line) system.
Works for any project — not tied to a specific building or lot.
playwright-cli for browser automation.
agent-browser is NOT suitable for BRAVO (element interaction issues).
BRAVO has a multi-step login. These steps are identical for all users:
https://bravo.bd.gov.hk/login?request_locale=zh_HK
textbox "登入名稱" with username
textbox "密碼" with password
button "登入"
/notice):
checkbox "本人確認已閱讀..."
button "接受"
/noticeProcess.action):
checkbox "選項 A" (building construction matters)
button "遞交"
/checkMulitpleSignOn):
button "是"
/home)
Always take a fresh snapshot after each page transition to get current
element refs. Refs change between sessions.
BRAVO uses jQuery UI autocomplete for address search.
textbox "位置" on the home page, click to focus
playwright-cli type "" — type character-by-character
.ui-menu-item-wrapper element in the dropdown
button "搜尋" or press Enter
button "只顯示搜尋結果"
fill command — no autocomplete trigger
goto to internal pages — kills the session
Results appear in a table with columns:
To open a record's plan list:
3/4003/72)
button "圖則" in that row
viewPlanList?recId=
tab-select
Each plan in the list has:
button "Preview" — thumbnail view (low res, full plan)
button "Inspect Full Image" — Daeja ViewONE viewer
The Daeja ViewONE viewer often shows "Lost connection to the server"
while the actual tile API remains functional. This is the most reliable
method for full-resolution plan extraction.
Navigate to the inspection page for a plan and extract both the base
dimensions and auth tokens from the tile request:
var entries = performance.getEntriesByType('resource');
for (var e of entries) {
if (e.name.includes('op=tile') && e.name.includes('tw=')) {
var url = e.name;
var getP = function(n) {
var m = url.match(new RegExp('[&?]' + n + '=([^&]*)'));
return m ? m[1] : '';
};
var baseW = parseInt(getP('tw')); // e.g., 794
var baseH = parseInt(getP('th')); // e.g., 554
var tokens = {
fch: getP('fch'), xCCH: getP('X-CCH'), xCDH: getP('X-CDH'),
xCId: getP('X-CId'), xLCId: getP('X-LCId'),
version: getP('v') || '45571'
};
// Use these for the HD request
}
}
The tw and th values tell you the base resolution at 96 DPI. Multiply
by the desired scale factor to get target HD dimensions.
Construct a tile URL with increased DPI. The scale factor determines output
quality — 6x gives ~4764px for a typical 794px base, suitable for 300 DPI
printing at A3 size.
https://bravo.bd.gov.hk/v1files/?v=<version>&op=tile
&tw=<baseW * scale>&th=<baseH * scale>&x=0&y=0&page=1
&DPI=<96 * scale>&rxr=<96 * scale>&ryr=<96 * scale>
&w=<baseW * scale>&h=<baseH * scale>&enh=1&iv=0
&fch=<fch>&err=0
&X-CCH=<xCCH>&X-CDH=<xCDH>&X-CId=<xCId>&X-LCId=<xLCId>
&X-Client-Version=<version>
This returns the FULL plan as a single PNG tile. Unlike zoom+canvas
extraction, this captures the entire drawing at once.
Large HD images require chunked base64 encoding inside page.evaluate()
to avoid "Maximum call stack size exceeded":
const arr = new Uint8Array(await blob.arrayBuffer());
let b64 = '';
for (let i = 0; i < arr.length; i += 8192) {
b64 += String.fromCharCode.apply(null, arr.subarray(i, i + 8192));
}
return { b64: btoa(b64), size: arr.length };
Then decode in Python and save via Pillow:
data = base64.b64decode(result["b64"])
with open(output_path, "wb") as f:
f.write(data)
Use Pillow to combine all downloaded PNGs:
from PIL import Image
images = [Image.open(f).convert("RGB") for f in png_files]
images[0].save("output.pdf", "PDF", save_all=True,
append_images=images[1:], resolution=300)
for each plan
tw×th)
getInspectionPlanImage URL requires Daeja auth headers
eval on #systemMsgModal button
history.back() or tab switching,
never goto to internal URLs
API still works — proceed with HD tile download method
共 1 个版本