fix: use MediaWiki API for wiki scraping to bypass Cloudflare

Direct wiki page requests return 403 due to Cloudflare JS challenge.
Switch to MediaWiki parse API which returns rendered HTML without
blocking. Also match img[alt='Official'] in lane detection.
This commit is contained in:
2026-04-05 00:14:13 +07:00
parent 0cd5a0eb03
commit 5cff47f1ba
5 changed files with 572 additions and 212 deletions
+511 -171
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -1,5 +1,44 @@
package parser
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
)
// wikiAPIResponse represents the MediaWiki parse API response.
type wikiAPIResponse struct {
Parse struct {
Text struct {
Content string `json:"*"`
} `json:"text"`
} `json:"parse"`
}
// fetchWikiDoc fetches a Fandom wiki page via the MediaWiki parse API (bypasses Cloudflare)
// and returns a goquery document of the rendered HTML.
func fetchWikiDoc(pageName string) (*goquery.Document, error) {
url := fmt.Sprintf(
"https://leagueoflegends.fandom.com/api.php?action=parse&page=%s&prop=text&format=json",
pageName,
)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("fetching wiki API for %s: %w", pageName, err)
}
defer resp.Body.Close()
var apiResp wikiAPIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, fmt.Errorf("decoding wiki API response: %w", err)
}
return goquery.NewDocumentFromReader(strings.NewReader(apiResp.Parse.Text.Content))
}
// buildNameIndex creates a map from champion name to slice index for O(1) lookups.
func buildNameIndex(champions []ChampionResult) map[string]int {
idx := make(map[string]int, len(champions))
+5 -13
View File
@@ -2,14 +2,11 @@ package parser
import (
"fmt"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
)
const wikiDraftPositionURL = "https://leagueoflegends.fandom.com/wiki/List_of_champions_by_draft_position"
// columnLaneMap maps 1-indexed column positions to lane names.
var columnLaneMap = map[int]string{
1: "top",
@@ -19,18 +16,12 @@ var columnLaneMap = map[int]string{
5: "support",
}
// EnrichLanes scrapes the LoL Wiki draft position table to add lane data.
// EnrichLanes fetches the LoL Wiki draft position table to add lane data.
func EnrichLanes(champions []ChampionResult) ([]ChampionResult, error) {
resp, err := http.Get(wikiDraftPositionURL)
doc, err := fetchWikiDoc("List_of_champions_by_draft_position")
if err != nil {
return nil, fmt.Errorf("fetching wiki draft positions: %w", err)
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("parsing wiki HTML: %w", err)
}
nameIndex := buildNameIndex(champions)
@@ -42,8 +33,9 @@ func EnrichLanes(champions []ChampionResult) ([]ChampionResult, error) {
}
cols.Each(func(colIdx int, col *goquery.Selection) {
// Check if this column has a "Yes" indicator image.
if col.Find("img[alt='Yes']").Length() == 0 {
// Check if this column has a lane indicator image (alt="Yes" or alt="Official").
hasIndicator := col.Find("img[alt='Yes'], img[alt='Official']").Length() > 0
if !hasIndicator {
return
}
+15 -17
View File
@@ -20,14 +20,23 @@ func TestDdragonAPI(t *testing.T) {
}
}
func TestWikiDraftPosition(t *testing.T) {
resp, err := http.Get(wikiDraftPositionURL)
func TestWikiAPIChampionList(t *testing.T) {
doc, err := fetchWikiDoc("List_of_champions")
if err != nil {
t.Fatalf("request failed: %v", err)
t.Fatalf("fetch failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
if doc.Find("tbody").Length() == 0 {
t.Fatal("expected at least one tbody in champion list")
}
}
func TestWikiAPIDraftPosition(t *testing.T) {
doc, err := fetchWikiDoc("List_of_champions_by_draft_position")
if err != nil {
t.Fatalf("fetch failed: %v", err)
}
if doc.Find("tbody").Length() < 2 {
t.Fatal("expected at least two tbody in draft position page")
}
}
@@ -46,14 +55,3 @@ func TestRegionAPI(t *testing.T) {
t.Fatalf("expected application/json, got %s", ct)
}
}
func TestWikiChampionList(t *testing.T) {
resp, err := http.Get(wikiChampionListURL)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
+2 -11
View File
@@ -2,27 +2,18 @@ package parser
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/PuerkitoBio/goquery"
)
const wikiChampionListURL = "https://leagueoflegends.fandom.com/wiki/List_of_champions"
// EnrichReleaseDates scrapes the LoL Wiki to add release year to each champion.
// EnrichReleaseDates fetches the LoL Wiki to add release year to each champion.
func EnrichReleaseDates(champions []ChampionResult) ([]ChampionResult, error) {
resp, err := http.Get(wikiChampionListURL)
doc, err := fetchWikiDoc("List_of_champions")
if err != nil {
return nil, fmt.Errorf("fetching wiki champion list: %w", err)
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("parsing wiki HTML: %w", err)
}
// Build a name-to-index map for O(1) lookups.
nameIndex := buildNameIndex(champions)