mirror of
https://github.com/tiennm99/loldle.git
synced 2026-10-11 03:13:42 +00:00
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:
1 parent
090082a51d
commit
23be5513e4
5 files changed
+572
-212
No files matched your search
+511
-171
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,44 @@
|
|||||||
package parser
|
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.
|
// buildNameIndex creates a map from champion name to slice index for O(1) lookups.
|
||||||
func buildNameIndex(champions []ChampionResult) map[string]int {
|
func buildNameIndex(champions []ChampionResult) map[string]int {
|
||||||
idx := make(map[string]int, len(champions))
|
idx := make(map[string]int, len(champions))
|
||||||
|
|||||||
+5
-13
@@ -2,14 +2,11 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"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.
|
// columnLaneMap maps 1-indexed column positions to lane names.
|
||||||
var columnLaneMap = map[int]string{
|
var columnLaneMap = map[int]string{
|
||||||
1: "top",
|
1: "top",
|
||||||
@@ -19,18 +16,12 @@ var columnLaneMap = map[int]string{
|
|||||||
5: "support",
|
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) {
|
func EnrichLanes(champions []ChampionResult) ([]ChampionResult, error) {
|
||||||
resp, err := http.Get(wikiDraftPositionURL)
|
doc, err := fetchWikiDoc("List_of_champions_by_draft_position")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetching wiki draft positions: %w", err)
|
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)
|
nameIndex := buildNameIndex(champions)
|
||||||
|
|
||||||
@@ -42,8 +33,9 @@ func EnrichLanes(champions []ChampionResult) ([]ChampionResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cols.Each(func(colIdx int, col *goquery.Selection) {
|
cols.Each(func(colIdx int, col *goquery.Selection) {
|
||||||
// Check if this column has a "Yes" indicator image.
|
// Check if this column has a lane indicator image (alt="Yes" or alt="Official").
|
||||||
if col.Find("img[alt='Yes']").Length() == 0 {
|
hasIndicator := col.Find("img[alt='Yes'], img[alt='Official']").Length() > 0
|
||||||
|
if !hasIndicator {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-17
@@ -20,14 +20,23 @@ func TestDdragonAPI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWikiDraftPosition(t *testing.T) {
|
func TestWikiAPIChampionList(t *testing.T) {
|
||||||
resp, err := http.Get(wikiDraftPositionURL)
|
doc, err := fetchWikiDoc("List_of_champions")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("request failed: %v", err)
|
t.Fatalf("fetch failed: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
if doc.Find("tbody").Length() == 0 {
|
||||||
if resp.StatusCode != http.StatusOK {
|
t.Fatal("expected at least one tbody in champion list")
|
||||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
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,27 +2,18 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
)
|
)
|
||||||
|
|
||||||
const wikiChampionListURL = "https://leagueoflegends.fandom.com/wiki/List_of_champions"
|
// EnrichReleaseDates fetches the LoL Wiki to add release year to each champion.
|
||||||
|
|
||||||
// EnrichReleaseDates scrapes the LoL Wiki to add release year to each champion.
|
|
||||||
func EnrichReleaseDates(champions []ChampionResult) ([]ChampionResult, error) {
|
func EnrichReleaseDates(champions []ChampionResult) ([]ChampionResult, error) {
|
||||||
resp, err := http.Get(wikiChampionListURL)
|
doc, err := fetchWikiDoc("List_of_champions")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetching wiki champion list: %w", err)
|
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.
|
// Build a name-to-index map for O(1) lookups.
|
||||||
nameIndex := buildNameIndex(champions)
|
nameIndex := buildNameIndex(champions)
|
||||||
|
|||||||
Reference in new issue
Block a user