270 lines
5.5 KiB
Go
270 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
gq "github.com/PuerkitoBio/goquery"
|
|
)
|
|
|
|
const (
|
|
URL = "https://winworldpc.com"
|
|
csvFileName = "results.csv"
|
|
csvFullResultsFileName = "results_full.csv"
|
|
logFileName = "output.log"
|
|
UserAgent = "Mozilla/5.0 (compatible; IPFS.ScraperBot/v1.0; +https://github.com/Neo-Desktop/winworldpc-ipfs-scraper)"
|
|
)
|
|
|
|
type File struct {
|
|
Name string
|
|
Version string
|
|
Language string
|
|
GUID string
|
|
Size string
|
|
Hash string
|
|
Architecture string
|
|
IPFSLink string
|
|
MirrorLinks []string
|
|
}
|
|
|
|
func (f File) MarshalCSV() []string {
|
|
out := []string{
|
|
f.Name,
|
|
f.Version,
|
|
f.Language,
|
|
f.GUID,
|
|
f.Size,
|
|
f.Hash,
|
|
f.Architecture,
|
|
f.IPFSLink,
|
|
}
|
|
|
|
out = append(out, f.MirrorLinks...)
|
|
return out
|
|
}
|
|
|
|
type Article struct {
|
|
Title string
|
|
Version string
|
|
WWPCLink string
|
|
Files []File
|
|
}
|
|
|
|
func scrapeSearchPage(page int) []Article {
|
|
log.Printf("=============================== PAGE %2d ===============================", page)
|
|
urlA, err := url.Parse(URL + "/search")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
queryParameters := urlA.Query()
|
|
queryParameters.Add("page", fmt.Sprintf("%d", page))
|
|
urlA.RawQuery = queryParameters.Encode()
|
|
|
|
res, err := fetch(urlA.String())
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
|
|
}
|
|
|
|
doc, err := gq.NewDocumentFromReader(res.Body)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
articles := make([]Article, 0)
|
|
|
|
doc.Find(".media>.media-body").Each(func(i int, s *gq.Selection) {
|
|
title := strings.TrimSpace(s.Find(".mt-0 a").First().Text())
|
|
|
|
s.Find(".nav>.nav-link>a").Each(func(j int, s1 *gq.Selection) {
|
|
url, ok := s1.Attr("href")
|
|
if !ok {
|
|
log.Printf("anchor does not have a href")
|
|
return
|
|
}
|
|
|
|
version := strings.TrimSpace(s1.Text())
|
|
|
|
if len(articles) > 6 {
|
|
return
|
|
}
|
|
articles = append(articles, scrapeArticlePage(Article{
|
|
Title: title,
|
|
Version: version,
|
|
WWPCLink: url,
|
|
}))
|
|
})
|
|
})
|
|
|
|
return articles
|
|
}
|
|
|
|
func scrapeArticlePage(article Article) Article {
|
|
res, err := fetch(URL + article.WWPCLink)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return article
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
log.Printf("status code error: %d %s", res.StatusCode, res.Status)
|
|
return article
|
|
}
|
|
|
|
doc, err := gq.NewDocumentFromReader(res.Body)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return article
|
|
}
|
|
|
|
doc.Find("#downloadsTable tbody tr").Each(func(i int, tr *gq.Selection) {
|
|
file := File{}
|
|
|
|
tr.Find("td").Each(func(j int, td *gq.Selection) {
|
|
switch j {
|
|
|
|
case 0: // download name / link / guid
|
|
link, ok := td.Find("a").Attr("href")
|
|
if ok {
|
|
file.GUID = strings.Replace(link, "/download/", "", -1)
|
|
}
|
|
file.Name = strings.TrimSpace(td.Text())
|
|
|
|
case 1: // version
|
|
file.Version = strings.TrimSpace(td.Text())
|
|
|
|
case 2: // language
|
|
file.Language = strings.TrimSpace(td.Text())
|
|
|
|
case 3: // architecture
|
|
title, ok := td.Find("img").Attr("title")
|
|
if ok {
|
|
file.Architecture = title
|
|
}
|
|
|
|
case 4: // file size / hash
|
|
file.Size = strings.TrimSpace(td.Text())
|
|
hash, ok := td.Find("span").Attr("title")
|
|
if ok {
|
|
file.Hash = strings.TrimSpace(hash)
|
|
}
|
|
|
|
//case 5: // download counter
|
|
}
|
|
})
|
|
|
|
article.Files = append(article.Files, scrapeDownloadPage(file))
|
|
})
|
|
|
|
return article
|
|
}
|
|
|
|
func scrapeDownloadPage(file File) File {
|
|
res, err := fetch(URL + "/download/" + file.GUID)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return file
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
log.Printf("status code error: %d %s", res.StatusCode, res.Status)
|
|
return file
|
|
}
|
|
|
|
doc, err := gq.NewDocumentFromReader(res.Body)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return file
|
|
}
|
|
|
|
link, ok := doc.Find("#localClientLink a").Attr("href")
|
|
if ok {
|
|
file.IPFSLink = link
|
|
}
|
|
|
|
doc.Find("#mirrorsList a").Each(func(i int, s *gq.Selection) {
|
|
link, ok := s.Attr("href")
|
|
if ok {
|
|
file.MirrorLinks = append(file.MirrorLinks, link)
|
|
}
|
|
})
|
|
|
|
return file
|
|
}
|
|
|
|
func fetch(urlA string) (*http.Response, error) {
|
|
client := &http.Client{}
|
|
|
|
log.Printf("sleeping 3 seconds before requesting %s", urlA)
|
|
time.Sleep(3 * time.Second)
|
|
|
|
req, err := http.NewRequest(http.MethodGet, urlA, nil)
|
|
if err != nil {
|
|
return req.Response, err
|
|
}
|
|
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
return client.Do(req)
|
|
}
|
|
|
|
func writeCSV(articles []Article, filename string) {
|
|
csvFile, err := os.OpenFile(filename, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
|
|
defer csvFile.Close()
|
|
|
|
csvWriter := csv.NewWriter(csvFile)
|
|
defer csvWriter.Flush()
|
|
|
|
for _, article := range articles {
|
|
for _, file := range article.Files {
|
|
_ = csvWriter.Write(file.MarshalCSV())
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
logHandle, err := os.OpenFile(logFileName, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
|
|
if err != nil {
|
|
log.Fatalf("error opening file: %v", err)
|
|
}
|
|
|
|
defer logHandle.Close()
|
|
|
|
logWriter := io.MultiWriter(os.Stdout, logHandle)
|
|
log.SetOutput(logWriter)
|
|
|
|
log.Printf("WinWorld IPFS Scraper Started")
|
|
startTime := time.Now()
|
|
|
|
articles := make([]Article, 0)
|
|
|
|
for i := 1; i < 63; i++ {
|
|
results := scrapeSearchPage(i)
|
|
articles = append(articles, results...)
|
|
go writeCSV(results, csvFileName)
|
|
}
|
|
|
|
writeCSV(articles, csvFullResultsFileName)
|
|
|
|
log.Printf("WinWorld IPFS Scraper completed")
|
|
log.Printf("Total time: %s", time.Now().Sub(startTime).String())
|
|
}
|