Files
thptqg/crawler/internal/sources/source_2017.go
T
tiennm99 c359a0b444 refactor: one directory per pipeline stage, and an assembler to drive them
The repository now reads as the pipeline it is: crawler fetches, parser
converts, assembler verifies and publishes, with data/ and web/ as the stores
they hand work through. go-parser is renamed parser now that there is no other.

The assembler replaces build-db.js and assemble-site.js. It compiles the
parser, builds and verifies each database, compresses it, runs the Vite build
and assembles _site — one command, and the only place that knows the order.

It also closes a real hole: nothing previously asserted that a database reached
the site. An empty staging directory assembled happily, so every page rendered,
every query 404d and CI stayed green. The row-count and size guards could not
catch that, since they only run when a database was built at all.

Removing Node from the root forced the dataset list out of web/src/datasets.js,
which the assembler cannot import. datasets.json is now the registry both sides
read — JSON because Go and the browser both parse it without a dependency —
while presentation stays in the web app, keyed by id and cross-checked against
the registry so a half-added dataset fails instead of half-working.

Guards verified by making each one fail: a missing database, and an expected
row count one higher than the truth.
2026-08-13 22:50:05 +07:00

71 lines
2.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sources
import (
"fmt"
"regexp"
"strings"
"golang.org/x/text/unicode/norm"
"github.com/tiennm99/thptqg/crawler/internal/article"
)
// browserUA and the Referer are both required: the CDN 403s an unrecognised
// User-Agent, and rejects requests that do not carry the article as Referer.
const browserUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
const article2017 = "https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm"
// source2017 fetches the 2017 dataset: one .xls per province, still served from
// the CDN that originally published it.
//
// Dest cannot use the CDN's filenames the way 2016 does, because they are
// inconsistent — Angiang.xls, 1BaRiaVungTau.xls, 23HaiPhong.xls, Gia-Lai.xls —
// carrying upload-order prefixes and arbitrary capitalisation. The province name
// in the link text is the stable identifier, so the local name is derived from
// that instead, which is what produced the files in data/2017/.
var source2017 = Source{
ID: "2017",
Summary: "63 province .xls files from the baotintuc.vn CDN",
Article: article2017,
Exts: []string{".xls"},
WantFiles: 63,
Headers: map[string]string{
"User-Agent": browserUA,
"Referer": article2017,
},
Dest: func(l article.Link) (string, error) {
s := slug(l.Text)
if s == "" {
return "", fmt.Errorf("link %s has no usable label to name it by", l.URL)
}
return s + ".xls", nil
},
}
var nonAlphanumeric = regexp.MustCompile(`[^a-z0-9]+`)
// slug turns a province name into a filename stem: transliterated to ASCII,
// lowercased, with every run of other characters collapsed to one hyphen.
//
// "Bà Rịa -Vũng Tàu" -> "ba-ria-vung-tau"
// "Bắc Kạn" -> "bac-kan"
//
// The article writes the names with diacritics, so this has to strip them.
// Combining marks are removed by the literal range U+0300–U+036F rather than by
// the unicode.Mn category, matching what parser does to build ho_ten_ascii.
func slug(name string) string {
var b strings.Builder
for _, r := range norm.NFD.String(name) {
switch {
case r >= 0x0300 && r <= 0x036F:
// combining mark: drop
case r == 'đ' || r == 'Đ':
b.WriteByte('d')
default:
b.WriteRune(r)
}
}
return strings.Trim(nonAlphanumeric.ReplaceAllString(strings.ToLower(b.String()), "-"), "-")
}