EST. 2026 — Notes from real server work SYSADMIN · SECURITY · AI · DIRECTADMIN Issue 01
THE SYSADMIN ALMANAC
Home / Linux / Government ICT Computer Price Criteria + DIY Builds with Scripts
Linux

Government ICT Computer Price Criteria + DIY Builds with Scripts

by Ton-Or ·22 Jul 2026·9 min read
Government ICT Computer Price Criteria + DIY Builds with Scripts

The reference price document lists all the minimum specifications, but it does not say what the actual retail prices are. So I wrote a script to pull Advice prices and compare each component, then found that a DIY machine meeting both sets of criteria costs only tens of baht more than the budget for Item 5.

A colleague in the industry asked me how strict the latest government computer reference price criteria (the December 2568 edition) are about specifications, and whether building a machine would offer better value than buying a prebuilt one within the budget. So I sat down and read the actual document item by item, then wrote a Python script to pull prices from Advice and put together builds for a comparison based on numbers, rather than guesses. There are several hidden traps here that are easy to miss when skimming the criteria, so I wanted to walk through both the requirements and the code.

The filenames and paths in this article are relative examples, while the script structure and price figures are real, from the latest crawl.

Two ICT Items That Need a Close Read

The document has two items that desktop buyers need to look at: Item 4 for office computers and Item 5 for Type 1 processing computers. The main differences are the CPU and graphics requirements; everything else (RAM DDR5 16GB, SSD 500GB, a 23-inch monitor, keyboard+mouse) is exactly the same.

Item 4 — Office Work

Budget: 20,000 baht

CPU ≥4 core / 8 thread, Turbo ≥4.4 GHz, Cache at the same level ≥12 MB, RAM DDR5 ≥16 GB, SSD ≥500 GB, monitor ≥23 inches -> No graphics card required

Item 5 — Processing Work

Budget: 24,000 baht

CPU ≥8 core / 8 thread, Turbo ≥4.6 GHz, Cache at the same level ≥16 MB, plus GPU memory ≥2 GB; everything else matches Item 4

What people miss when skimming is the phrase "a total of at least 8 cores" in Item 5. It means actual physical cores, not threads, so many budget CPUs with 6 core 12 thread easily pass Item 4 but immediately fail Item 5. This needs a script to check it, not a quick glance.

An iGPU Can Meet the GPU Requirement

Item 5 requires GPU memory ≥2 GB, but the criteria allow an iGPU that "uses ≥2 GB of main memory for graphics" to qualify -> choosing a CPU with an iGPU means I can leave out the discrete graphics card entirely, saving several thousand baht

The Problem Is That the Criteria Do Not Give Actual Prices

The document only gives minimum specifications and price ceilings. It does not say what components that actually meet those specifications cost at retail right now, or whether they are in stock. So I pulled prices from Advice as a reference, because its website lists both stock by branch and member prices.

The first problem I ran into was that Advice product pages use Nuxt SSR. Prices are not directly in the HTML; they are embedded in __NUXT_DATA__ as an "index graph," where values reference one another by index number rather than containing the actual values. I had to write a resolver to follow those chains myself.

import json, re
from pathlib import Path

# หน้า Advice เป็น Nuxt SSR — ข้อมูลจริงฝังใน __NUXT_DATA__ เป็น index graph
NUXT_RE = re.compile(r'__NUXT_DATA__"[^>]*>(\[.*?\])</script>', re.S)

def load_graph(html_path):
    text = Path(html_path).read_text(encoding="utf-8", errors="ignore")
    m = NUXT_RE.search(text)
    return json.loads(m.group(1)) if m else None

# ค่าทุกตัวใน graph อ้างกันด้วย index ไม่ใช่ค่าตรงๆ ต้องไล่ resolve เอง
def resolve(graph, idx, depth=0, seen=frozenset()):
    if depth > 25 or idx in seen or not isinstance(idx, int) or not (0 <= idx < len(graph)):
        return idx
    seen = seen | {idx}
    val = graph[idx]
    if isinstance(val, dict):
        return {k: resolve(graph, v, depth + 1, seen) for k, v in val.items()}
    if isinstance(val, list):
        # unwrap reactive wrapper ของ Nuxt ก่อนไล่ต่อ
        if len(val) == 2 and val[0] in ("ShallowReactive", "Reactive", "Ref"):
            return resolve(graph, val[1], depth + 1, seen)
        return [resolve(graph, v, depth + 1, seen) for v in val]
    return val

Once the graph is resolved back into a normal dict, the product data sits under a seed named pdp-hero-seed:. From there, I can drill into product_get and extract prices and specifications. The important point is that member prices (SALEPRICE6 in item_master) are often much lower than the website's sale prices, so I need to keep both.

def extract(graph):
    tree = resolve(graph, 0)
    pdp  = next(v for k, v in tree["data"].items()
                if str(k).startswith("pdp-hero-seed:"))
    prod = find_first(pdp, lambda x: isinstance(x, dict) and "product_get" in x)
    pg   = prod["product_get"]
    im   = pg.get("item_master") or {}
    specs = [strip_html(s["spec_value"]) for s in (pg.get("spec_dtls") or [])
             if s.get("spec_value")]
    return {
        "sku":          pg.get("code"),
        "name":         pg.get("Product") or pg.get("header"),
        "group":        pg.get("Grp"),                        # CPU / MB / RAM / ...
        "price_sale":   to_int(prod.get("price_sale")),       # ราคาขายหน้าเว็บ
        "price_member": to_int(im.get("SALEPRICE6")),         # ราคาสมาชิก (ถูกกว่าเยอะ)
        "spec_full":    " | ".join(specs),                    # สเปกดิบ คั่นด้วย |
        "stock_type":   (pg.get("stock") or {}).get("type"),  # instock / outstock
    }

find_first, strip_html, and to_int are small helpers in the same file—they walk through dicts/lists to find the required key, strip HTML tags, and convert prices containing commas into integers. Then a single run processes the entire cache folder.

# แกะทุกไฟล์ HTML ใน cache ออกมาเป็น JSON ก้อนเดียว
python tools/extract_advice.py data/advice_cache/*.html > data/advice_products.json

Filter for Available Stock, Then Let the Script Check Specifications

The important point here is that a low price means nothing if the item is unavailable. So I first filtered for stock_type = instock (online), then parsed each CPU's spec_full against the criteria for Item 4 and Item 5. The raw CPU specifications are ordered as brand | model | socket | core | thread | base | boost | L1 | L2 | L3 | iGPU.

import json

rows = json.load(open("data/advice_products.json", encoding="utf-8"))

def passes(cpu, core_min, thread_min, ghz_min, l3_min):
    # spec_full: brand | model | socket | core | thread | base | boost | L1 | L2 | L3 | iGPU
    f = [x.strip() for x in cpu["spec_full"].split("|")]
    socket        = f[2]
    core, thread  = int(f[3]), int(f[4])
    boost         = float(f[6].replace("GHz", "").strip())
    l3_mb         = int(f[9].replace("MB", "").strip())
    ddr5          = socket != "AM4"        # AM4 = DDR4 -> ตกเกณฑ์ "DDR5 หรือดีกว่า"
    return (core >= core_min and thread >= thread_min
            and boost >= ghz_min and l3_mb >= l3_min and ddr5)

cpus = [r for r in rows if r["group"] == "CPU" and r["stock_type"] == "instock"]
for c in cpus:
    ok4 = passes(c, 4, 8, 4.4, 12)   # ข้อ 4 งานสำนักงาน
    ok5 = passes(c, 8, 8, 4.6, 16)   # ข้อ 5 งานประมวลผล
    print(f'{c["name"]:<34} ข้อ4={ok4}  ข้อ5={ok5}  {c["price_sale"]}฿')

Running this against the latest data revealed three problems that reading the criteria alone would never show.

The DDR5 Trap

The criteria require DDR5 or better RAM -> AMD AM4 CPUs such as the R5 5600 / 5600G may easily meet the core count, but the platform uses DDR4 and immediately fails the criteria. The script therefore needs to check the socket too, not just cores and speed

Second, almost all CPUs with 8 or more cores that met Item 5 were out of stock online on the crawl date (i5-14400/14500, i7-13700, and R7 5700X all showed outstock and had to be ordered into a branch). Third, the i3-14100 with an iGPU was also outstock, leaving only the F model without integrated graphics, which means a build for Item 4 using this CPU needs a discrete graphics card added. Without collecting stock status from the start, I would have ended up with builds that look good on paper but cannot actually be bought.

Compare the Criteria with Real Components, Then Produce a Table

The final step is to assemble complete builds and calculate the full cost. The price to compare against the government budget is not just the component price; it must include VAT 7% and the vendor's after-sales service fee. I wrote a calculator to reproduce the figures in the table exactly.

# ราคาชิ้นส่วน Build ประหยัดสุด (ราคา sale ที่ดึงมาได้จาก Advice)
parts = {
    "CPU  Core Ultra 5 225 (10C/10T 4.9GHz 20MB +iGPU)": 5410,
    "MB   H810M DDR5 LGA1851":                            2290,
    "RAM  16GB DDR5-6000":                                6520,
    "SSD  512GB NVMe PCIe":                               2845,
    "PSU  600W":                                           770,
    "Case mATX":                                           820,
    "Monitor 23.8\" IPS":                                 2420,
    "KB+M":                                                359,
}
subtotal = sum(parts.values())          # 21,434
after_vat = round(subtotal * 1.07)      # + VAT 7%
grand     = round(after_vat * 1.05)     # + ค่าบริการหลังการขาย 5%
print(subtotal, after_vat, grand)       # 21434 22934 24081
Total Price CalculationComponent Price21,434→×1.07VAT 7%→×1.05After-Sales Service Fee→Grand Total24,081

For the final report, I used matplotlib to draw a PNG table comparing all the builds on one page (a Thai font must also be installed, or the characters appear as squares). The full pipeline follows these steps.

  1. Download Advice product pages and cache them as HTML (to avoid rate limits and allow repeat crawls without requesting the pages again)
  2. Run extract_advice.py to unpack __NUXT_DATA__ into JSON containing sale/member prices + specifications + stock status
  3. Filter for stock_type = instock, then parse spec_full against the criteria for Item 4 / Item 5
  4. Assemble complete builds and calculate the total including VAT + after-sales service fees
  5. Run render_build_table.py to produce an image table comparing all builds on one page
# ออกเป็นตารางภาพเทียบทุกชุด -> docs/build_comparison.png
python tools/render_build_table.py

The Resulting Figures

The results were more interesting than I expected. The cheapest build, using a Core Ultra 5 225 CPU (10C/10T 4.9GHz 20MB with an iGPU), had a component price of 21,434 baht. Adding tax and service fees brought the grand total to 24,081 baht. This single machine meets both Item 4 and Item 5.

20,000Item 4 office budget
24,000Item 5 compute budget
24,081DIY with tax+service
+81Difference from Item 5
Buy a Prebuilt Machine to the Criteria

Item 5 Budget: 24,000 baht

A machine that "just meets" the criteria -> 8C/8T 4.6GHz 16MB as specified in the document, with no headroom. The vendor chooses the cheapest components that still qualify

DIY Using the Script

Core Ultra 5 225 Total: 24,081 baht

The total price differs by just +81 baht, but buys 10C/10T 4.9GHz 20MB -> exceeding both Item 4 and Item 5 in one machine. Getting the member price makes it cheaper still

My conclusion is that if I need a machine that actually "meets the ICT criteria," I can build one using currently available stock for about the same price as a prebuilt machine. The difference is tens of baht, but the specifications are well above the requirements. The key point is not how many baht I save, but having a script that automatically pulls actual prices and compares them against the criteria, exposing the DDR5 trap, stock problems, and the iGPU option that reading the document alone would never reveal. The next time prices change, I can run another crawl and rerun the same script, and the entire table updates automatically.

Anyone adapting this structure to another year's criteria or another store only needs to change the regex that unpacks the SSR data and the spec_full indices to match that website. The rest of the criteria comparison logic can be reused as is. If I have left anything out or made a typo, I will come back and correct it later.

Hit something like this? Stuck somewhere?

Come trade notes on the LinuxThai board

→ Continue on the board

Comments 0

No comments yet — be the first