⏩ Изучаем Go на примерах, #1: Парсер и чекер прокси

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Источник: XSS.is
Автор BlameUself


JavaScript - очень классный язык программирования, но сегодня речь пойдет не о нем.
1920px-Go_Logo_Blue.svg.png


У меня давно возникали мысли об изучении Go, этот язык привлекал меня простотой работы с потоками, большими возможностями работы с сетью, высокой скоростью и лаконичным стилем. Я считаю, что выбор этого языка в качестве первого - весьма плохая идея, поскольку большинство обучающих ресурсов предполагают знакомство с программированием в целом, не фокусируясь на мелочах. Официальная документация-знакомство будет достаточно понятной, если вы ранее работали с C-подобными ЯП. Вы можете начать с нее, и пусть некоторые концепции будут реализованы иначе, чем в вашем языке, в целом все объяснено очень круто и все обучение понятно - Tour of Go - https://go.dev/tour/list. Также, я могу порекомендовать курс:
Отличный преподаватель, у него также есть простой курс по сетевым протоколам. Далее, вы можете посетить канал - https://www.youtube.com/@nikolay_tuzov, там очень много проектов на Go.
В этой статье я постараюсь описывать код с точки зрения новичков, но в то же время статья содержит код рабочего прокси-парсера/чекера, и если вас интересует проект, вы найдете исходники здесь.

Для того чтобы начать, скачиваем и устанавливаем с официального сайта Go - https://go.dev/doc/install, проверяем все ли корректно установилось командой go version в терминале. Работать мы будем в VS Code, перед началом добавляем расширение для комфортной работы - https://marketplace.visualstudio.com/items?itemName=golang.Go

Итак, наша задача состоит в том, чтобы получать данные с сайтов, которые содержат списки бесплатных прокси. Очевидно, что многие из них не будут работать в целом или для определенного ресурса. Так что далее нам необходимо проверять их способность к подключению к желаемому ресурсу.
Декомпозируем задачу. План работ следующий:
  1. Получаем список прокси с одного ресурса. На этом этапе выводим результат в терминал.
  2. Получаем списки с множества ресурсов, удаляем дубликаты. Список ресурсов берем из файла и сохраняем результат в файл.
  3. Учимся обращаться к удаленным ресурсам, используя полученные прокси, отбрасываем неудачные попытки.
Первым сайтом, с которого мы будем получать данные, будет https://free-proxy-list.net/. Данные, которые меня интересуют, это ipAddress:port.
Поскольку в сущности софт будет универсальный, и в него можно будет добавить дополнительные ссылки с сайтами, на которых находятся прокси, нам необходим универсальный способ получения информации, и конечно же, таким выступят регулярные выражения.
Переходим в IDE:
C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "fmt"
    "io"
    "net/http"
    "regexp"
)

func main() {

    resp, err := http.Get("https://free-proxy-list.net/")
    if err != nil {
        fmt.Println("Error on page fetch:", err)
        return
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading the page content:", err)
        return
    }

    re := regexp.MustCompile(`\d+\.\d+\.\d+\.\d+:\d+`)

    proxies := re.FindAllString(string(body), -1)

    fmt.Println("Found Proxies:")
    for _, proxy := range proxies {
        fmt.Println(proxy)
    }
}
Импортируем пакеты. Нам понадобится fmt для вывода лога полученных данных в терминал. Пакет io предоставляет базовые интерфейсы для операций ввода-вывода. В данном случае он используется для чтения ответа от HTTP-запроса. Для выполнения HTTP-запросов и обработки ответов используем пакет net/http, а пакет regexp предоставляет функциональность для работы с регулярными выражениями.
Код довольно простой. Сперва мы выполняем HTTP GET-запрос по указанному URL, возвращая ответ и ошибку, если таковая возникла. Затем обрабатываем потенциальную ошибку, завершая работу программы в случае таковой. После успешного выполнения запроса используется defer, чтобы отложить выполнение операции закрытия тела ответа (resp.Body.Close()) до тех пор, пока функция main() не завершится, и оптимизировать ресурсы. defer - уникальное ключевое слово для языка Go. В этой статье можно узнать больше об этом - ссылка на статью. Затем мы читаем тело ответа (это тот HTML-код, который вы увидите, если нажмете Ctrl+U в Chrome) с помощью io.ReadAll(). Результат чтения записывается в переменную body, а любая ошибка, которая может возникнуть в процессе чтения, проверяется также, как и при GET-запросе. Далее создается регулярное выражение с помощью regexp.MustCompile(), которое будет использоваться для поиска IP-адресов и портов в тексте страницы. Я уже рекомендовал книгу "Изучаем регулярные выражения" - Бен Форта, она все также прекрасна. Логика в данном случае очень простая: IP-адрес представляет собой последовательность цифр, обычно ограниченную количеством цифр, но в целом это некоторое количество цифр, точка и так четыре раза, затем идет двоеточие и снова цифры. Символ "d+" обозначает одно или более вхождений десятичной цифры (0-9). Итак, регулярное выражение выглядит так: \\d+\\.\\d+\\.\\d+\\.\\d+:\\d+. Вызываем метод re.FindAllString(string(body), -1) для поиска всех соответствий регулярному выражению. Он возвращает срез строк, содержащих найденные прокси. string(body) - это преобразование содержимого тела ответа, которое изначально представлено в виде среза байтов, в строку. -1 - это параметр, который указывает методу FindAllString(), что нужно найти все соответствия регулярному выражению в строке. Итерируемся по всем элементам среза proxies с помощью цикла for и выводим их в консоль.
Untitled.png



Отлично, продолжаем. На данный момент мы загрузили ссылку на сайт. Для масштабирования приложения нам необходимо вынести ссылки в файл, из которого мы сможем их брать и, при необходимости, добавлять новые. Также нам необходимо сохранять результаты за пределами вывода в консоль, в данном случае мы будем делать это в текстовый файл.
Я создам файл links.txt в корне проекта и помещу туда на данный момент лишь одну ссылку на https://free-proxy-list.net/. Переходим в VS Code.
C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "bufio"
    "fmt"
    "io"
    "net/http"
    "os"
    "regexp"
)

func main() {
    file, err := os.Open("links.txt")
    if err != nil {
        fmt.Println("Error opening the file:", err)
        return
    }
    defer file.Close()

    outFile, err := os.Create("notChecks.txt")
    if err != nil {
        fmt.Println("Error creating the output file:", err)
        return
    }
    defer outFile.Close()

    writer := bufio.NewWriter(outFile)

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        url := scanner.Text()

        resp, err := http.Get(url)
        if err != nil {
            fmt.Println("Error on page fetch:", err)
            continue
        }
        defer resp.Body.Close()

        body, err := io.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("Error reading the page content:", err)
            continue
        }

        re := regexp.MustCompile(`\d+\.\d+\.\d+\.\d+:\d+`)
        proxies := re.FindAllString(string(body), -1)

        for _, proxy := range proxies {
            fmt.Fprintln(writer, proxy)
        }
    }

    if err := scanner.Err(); err != nil {
        fmt.Println("Error reading the file:", err)
    }

    writer.Flush()
}
Подключаем несколько дополнительных пакетов - bufio и os. bufio предоставляет функциональность для буферизованного чтения и записи данных, а os предоставляет функции для работы с операционной системой.
Сначала мы открываем файл "links.txt", в который заранее добавили ссылки, и как обычно обрабатываем ошибки. Поскольку это будут непроверенные прокси, я выбрал название для файла, куда они будут сохраняться, "notChecks.txt”. Следовательно, далее в коде мы создаем данный файл, и если такой уже существует, мы просто перезаписываем его содержимое. Используем bufio.NewWriter() для создания writer'а, который будет использоваться для записи в "notChecks.txt". В цикле for scanner.Scan() проходимся по каждой строке в файле "links.txt". Для каждой строки выполняем HTTP-запрос (http.Get(url)), также как и делали ранее. В данном случае логика обработки ошибок будет несколько иная, нам не следует прекращать работу программы, ибо если одна из десятков ссылок по каким-то причинам будет проблемной, мы все равно хотим получить результат из девяти других, поэтому используем continue и логируем ошибку. Снова итерируем по элементам среза proxies, но в этот раз используем fmt.Fprintln(writer, proxy) для записи в файл. После завершения чтения всех URL-адресов из файла, происходит проверка наличия ошибок и их вывод в консоль. Логирование достаточно понятное и помогает отбросить лишние ссылки. В целом, если мы хотим больше данных, следует их также сохранять в отдельный файл, добавив время проверки. Использование writer.Flush() гарантирует, что все данные будут записаны в файл перед завершением работы программы.
Проверяем работу программы - все отлично. Добавляю еще одну ссылку https://proxyspace.pro/socks4.txt, и получаю результат:
Untitled (1).png



Поскольку IP-адреса вероятно будут пересекаться между ресурсами, нам необходимо устранять дубликаты. Я сразу вспомнил о такой структуре данных, как хеш-таблица. Поскольку мой первый язык программирования был JavaScript, я сразу подумал о структуре данных set, но таковой в Go не предусмотрено. Однако мы можем создать set из map.
C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "bufio"
    "fmt"
    "io"
    "net/http"
    "os"
    "regexp"
)

func main() {
    file, err := os.Open("links.txt")
    if err != nil {
        fmt.Println("Error opening the file:", err)
        return
    }
    defer file.Close()

    outFile, err := os.Create("notChecks.txt")
    if err != nil {
        fmt.Println("Error creating the output file:", err)
        return
    }
    defer outFile.Close()

    writer := bufio.NewWriter(outFile)

    scanner := bufio.NewScanner(file)

    uniqueProxies := make(map[string]bool)

    for scanner.Scan() {
        url := scanner.Text()

        resp, err := http.Get(url)
        if err != nil {
            fmt.Println("Error on page fetch:", err)
            continue
        }
        defer resp.Body.Close()

        body, err := io.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("Error reading the page content:", err)
            continue
        }

        re := regexp.MustCompile(`\d+\.\d+\.\d+\.\d+:\d+`)
        proxies := re.FindAllString(string(body), -1)

        for _, proxy := range proxies {
            if _, exists := uniqueProxies[proxy]; !exists {
                uniqueProxies[proxy] = true
                fmt.Fprintln(writer, proxy)
            }
        }
    }

    if err := scanner.Err(); err != nil {
        fmt.Println("Error reading the file:", err)
    }

    writer.Flush()
}
Итак, создаем мапу uniqueProxies := make(map[string]bool). Напомню, что карты (map) в Go представляют собой набор пар ключ-значение. В данном случае ключами будут строки (прокси адреса), а значениями будут булевы значения. В цикле for _, proxy := range proxies мы проходимся по адресам и проверяем, существует ли такой в мапе uniqueProxies. if _, exists := uniqueProxies[proxy]; !exists - обращаемся к значению карты по ключу proxy. Если прокси-адрес отсутствует в карте, переменная exists будет равна false, и условие !exists будет истинным. uniqueProxies[proxy] = true - если прокси-адрес не существует в карте, мы добавляем его туда, присваивая булево значение true по ключу proxy, чтобы отметить его как присутствующий.
Проверяю работу кода, вставив подряд две одинаковые ссылки, и убеждаюсь в успешном результате 😀

Поскольку не все прокси, которые мы получили, будут рабочими, нам необходимо проверить их способность к подключению. Переходим к созданию чекера в IDE. Создаем новый файл на Go.
C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "bufio"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "strings"
    "sync"
    "time"
)

func main() {
    file, err := os.Open("notChecks.txt")
    if err != nil {
        fmt.Println("Error opening the file:", err)
        return
    }
    defer file.Close()
    scanner := bufio.NewScanner(file)
    var ips []string
    var ports []string
    for scanner.Scan() {
        proxyStr := scanner.Text()
        parts := strings.Split(proxyStr, ":")
        if len(parts) != 2 {
            fmt.Println("Proxy format not valid:", proxyStr)
            continue
        }
        ips = append(ips, parts[0])
        ports = append(ports, parts[1])
    }
    var wg sync.WaitGroup
    concurrentRequests := 10
    semaphore := make(chan struct{}, concurrentRequests)
    validFile, err := os.Create("validProxy.txt")
    if err != nil {
        fmt.Println("Error creating the output file:", err)
        return
    }
    defer validFile.Close()
    for i := range ips {
        wg.Add(1)
        go func(ip, port string) {
            defer wg.Done()
            semaphore <- struct{}{}
            valid := checkProxy(ip, port)
            if valid {
                fmt.Printf("Valid Proxy %s:%s\n", ip, port)
                validFile.WriteString(ip + ":" + port + "\n")
            } else {
                fmt.Printf("Invalid Proxy %s:%s\n", ip, port)
            }
            <-semaphore
        }(ips[i], ports[i])
    }
    wg.Wait()
    fmt.Println("Check completed. Valid proxies have been saved to the file validProxy.txt.")
}

func checkProxy(ip, port string) bool {
    client := http.Client{
        Timeout: 5 * time.Second,
    }
    proxyURL := fmt.Sprintf("http://%s:%s", ip, port)
    parsed, err := url.Parse(proxyURL)
    if err != nil {
        fmt.Println("Error while parsing URL:", err)
        return false
    }
    transport := &http.Transport{
        Proxy: http.ProxyURL(parsed),
    }
    client.Transport = transport
    req, err := http.NewRequest("GET", "http://google.com", nil)
    if err != nil {
        fmt.Println("Error while creating a request:", err)
        return false
    }
    resp, err := client.Do(req)
    if err != nil {
        return false
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        return false
    }
    return true
}
Присупим к разбору кода. Мы добавляем несколько новых пакетов:
net/url - для работы с URL-адресами и создания прокси в формате "http://ip:порт”
strings - для работы со строками
sync - будет использоваться для синхронизации горутин и ожидания их завершения
time - для работы с временем и установки таймаута при выполнении HTTP-запросов через прокси.
Начнем разбор логики работы функции checkProxy. Эта функция отвественна непосредственно за проверку прокси, она принимает на вход IP адрес и порт, и возвращает булевое значение, определяющее валидность.
Валидация происходит путем запроса к ресурсу и проверки статуса кода ответа, это хороший способ универсальной проверки. В данном случае ресурсом выступит - “http://google.com”, но его можно заменить на любой, с которым вы планируете работать. Альтернативным способом проверки может быть сервис типа - https://httpbin.org/ip, который возвращает IP, с которого вы обратились. Валидным будет все, что вернуло статус код 200 OK.
Для обращения через прокси, мы создаем HTTP-клиента с таймаутом в 5 секунд, следовательно, любой запрос, который будет выполняться через этот клиент и не завершится в течение 5 секунд, будет прерван и будет считаться невалидным. Далее мы формируем URL прокси-сервера в формате "http://ip:порт", используя переданные в функцию ip и port. Создаем Transport, который определяет параметры соединения, в данном случае это прокси, далее объявляем клиент с использованием транспорта. Создается HTTP Get запрос к сайту google, в случае ошибки при создании отдаем false и выводим лог. Далее отправляем запрос через клиента, в случае ошибки (например, из-за таймаута), возвращаем false. После в функции происходит проверка кода состояния. Если код не является 200 OK, снова таки отдаем false. Во всех остальных случаях возвращаем true.
Разберем функцию main. Открываем файл 'notChecks.txt'. Создаем сканер для построчного чтения файла. Инициализируем два массива ips и ports, в которых будут храниться IP-адреса и порты прокси-серверов соответственно. Далее мы, с помощью цикла, разбиваем все на эти два массива, чтобы в дальнейшем передать их в функцию проверки. Мне кажется, это далеко не самое оптимальное решение, и возможно стоит подумать над его оптимизацией. В цикле также происходит проверка наличия двоеточия, и если строка не содержит его, она считается невалидной. После создается группа ожидания sync.WaitGroup, которая будет использоваться для ожидания завершения всех горутин. Также создается канал semaphore для управления количеством одновременно выполняемых горутин. В данном коде их десять. Создадим файл для записи валидных прокси “validProxy.txt”. Происходит итерация по списку прокси. Для каждого IP-адреса и порта в списке прокси создается новая горутина. Горутина добавляется в группу ожидания. Внутри горутины выполняется функция checkProxy, которая проверяет прокси и записывает результат в файл "validProxy.txt". Перед выполнением запроса происходит ожидание освобождения места в канале semaphore, чтобы не было выполнено более 10 запросов одновременно. wg.Wait() - ожидает завершения всех горутин. После завершения проверки выводим сообщение в консоль.
Untitled (2).png



Вероятно, вы уже могли заметить ошибку, а именно то, что я совсем забыл учитывать тип прокси. Обращаться к протоколу SOCKS через HTTP - неправильное решение. Я заметил эту ошибку только на этом этапе и начал думать, как исправить ее уже после написания кода. На большинстве сайтов с раздачами прокси будет указан тип. Проблема в том, что написать регулярное выражение, которое будет собирать эту информацию универсально, достаточно проблематично. Следовательно, мы рассуждаем с позиции того, что мы хотим универсальное решение, и мы не знаем, какой тип мы получаем на входе. В общем, я принял решение добавить проверку на протокол SOCKS, и в случае, если мы получаем невалидный результат, прокидывать ее вниз на проверку по HTTP. Это, очевидно, не самое оптимальное решение, тем не менее, оно работает. Проверка на SOCKS будет происходить по TCP. Я изучил спецификацию и пришел к выводу, что проверка по TCP должна стать универсальным решением как для SOCKS5, так и для SOCKS4, поскольку подключение с использованием TCP поддерживается всеми версиями протокола.
Переходим к коду:
C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "bufio"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "strings"
    "sync"
    "time"

    "golang.org/x/net/proxy"
)

func main() {
    file, err := os.Open("notChecks.txt")
    if err != nil {
        fmt.Println("Error opening the file:", err)
        return
    }
    defer file.Close()
    scanner := bufio.NewScanner(file)
    var ips []string
    var ports []string
    for scanner.Scan() {
        proxyStr := scanner.Text()
        parts := strings.Split(proxyStr, ":")
        if len(parts) != 2 {
            fmt.Println("Proxy format not valid:", proxyStr)
            continue
        }
        ips = append(ips, parts[0])
        ports = append(ports, parts[1])
    }
    var wg sync.WaitGroup
    concurrentRequests := 10
    semaphore := make(chan struct{}, concurrentRequests)
    validFile, err := os.Create("validProxy.txt")
    if err != nil {
        fmt.Println("Error creating the output file:", err)
        return
    }
    defer validFile.Close()
    for i := range ips {
        wg.Add(1)
        go func(ip, port string) {
            defer wg.Done()
            semaphore <- struct{}{}
            valid, proxyType := checkProxyWithSocks(ip, port)
            if !valid {
                valid, proxyType = checkProxy(ip, port)
            }
            if valid {
                fmt.Printf("✅ Valid %s Proxy %s:%s\n", proxyType, ip, port)
                validFile.WriteString(ip + ":" + port + "\n")
            } else {
                fmt.Printf("❌ Invalid Proxy %s:%s\n", ip, port)
            }
            <-semaphore
        }(ips[i], ports[i])
    }
    wg.Wait()
    fmt.Println("Check completed. Valid proxies have been saved to the file validProxy.txt.")
}

func checkProxyWithSocks(ip, port string) (bool, string) {
    dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", ip, port), nil, proxy.Direct)
    if err != nil {
        fmt.Println("Error while creating SOCKS5 proxy:", err)
        return false, ""
    }

    httpTransport := &http.Transport{
        Dial: dialer.Dial,
    }

    client := &http.Client{
        Transport: httpTransport,
        Timeout:   5 * time.Second,
    }

    req, err := http.NewRequest("GET", "http://google.com", nil)
    if err != nil {
        fmt.Println("Error while creating a request:", err)
        return false, ""
    }

    resp, err := client.Do(req)
    if err != nil {
        return false, "SOCKS5"
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        return false, "SOCKS5"
    }
    return true, "SOCKS5"
}

func checkProxy(ip, port string) (bool, string) {
    client := http.Client{
        Timeout: 5 * time.Second,
    }
    proxyURL := fmt.Sprintf("http://%s:%s", ip, port)
    parsed, err := url.Parse(proxyURL)
    if err != nil {
        fmt.Println("Error while parsing URL:", err)
        return false, ""
    }
    transport := &http.Transport{
        Proxy: http.ProxyURL(parsed),
    }
    client.Transport = transport
    req, err := http.NewRequest("GET", "http://google.com", nil)
    if err != nil {
        fmt.Println("Error while creating a request:", err)
        return false, ""
    }
    resp, err := client.Do(req)
    if err != nil {
        return false, "HTTP"
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        return false, "HTTP"
    }
    return true, "HTTP"
}
Для работы с протоколом SOCKS я добавил пакет "golang.org/x/net/proxy". В функции main сначала происходит проверка с помощью функции checkProxyWithSocks. Если она возвращает false, мы переходим к checkProxy. Если мы получаем true, то выводим результат в консоль. В этой версии я немного изменил лог, добавив эмодзи для визуального разделения, а также указания типа прокси в случае валида. Функция checkProxy не претерпела изменений и все также проверяет валидность по HTTP. Давайте разберем функцию checkProxyWithSocks:
C-подобный: Скопировать в буфер обмена
dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", ip, port), nil, proxy.Direct)
Сперва в ней происходит создание SOCKS5 прокси-клиента с помощью функции proxy.SOCKS5. Первым аргументом указываем протокол TCP, вторым - адрес прокси-сервера. nil указывает на то, что не требуется аутентификация, а proxy.Direct указывает на то, что нет необходимости использовать цепочку прокси. Далее все происходит также, как и при проверке HTTP: создаем транспорт, клиент, формируем запрос, отправляем его и получаем ответ, обрабатываем ошибки и проверяем статус ответа, закрываем тело ответа и возвращаем результат.
Untitled (3).png



Подводим итоги. Проектирование и декомпозиция очень важны, а разработка на Go увлекательна и несложна. 🙂
На самом деле, здесь еще есть, над чем работать. Однако этот проект выполняет свою функцию. Я столкнулся с проблемой получения прокси с нескольких сайтов и в данный момент работаю над тем, чтобы понять, почему это происходит (все ссылки из links.txt рабочие). Также я бы добавил больше логов в консоль о процессе.
Я объединил все в одну функцию и внёс несколько изменений по сравнению с тем, что описано в статье:
  • Сохранение валидных прокси происходит в двух файлах в зависимости от типа.
  • Создал переменные для файлов, ссылки, на которой проверяем подключение, одновременных запросов и таймаута подключения.
Вы можете Скачать
 
Сверху Снизу