17 Commits
v0.3 ... v0.4.1

Author SHA1 Message Date
Rustem Kamalov
d8c3700252 Stealth browser as option. Update default config 2025-08-18 22:49:45 +03:00
Rustem Kamalov
7f0f32ac4e Handle failed navigation 2025-07-03 02:29:12 +03:00
Rustem Kamalov
ba94c69dd0 Merge pull request #10 from PACHAKUTlQ/main
Support http and socks5 proxy and fix failure due to updates of DOM on Google SERP
2025-07-03 02:09:18 +03:00
Pachakutiq
c387268d20 chore: Exclude .aider* in .gitignore 2025-05-27 15:05:53 +08:00
Pachakutiq
2f8ee2164d fix: Fix serve mode not supporting raw mode 2025-04-10 18:07:57 +08:00
Pachakutiq
6691ebaadd fix: Remove duplicated results for all search engines and both modes 2025-04-10 16:59:31 +08:00
Pachakutiq
8099dae3ce fix: Remove duplicated results in google/search.go 2025-04-10 15:35:54 +08:00
Pachakutiq
2f77dd6fe5 doc: Add proxy guide in README.md 2025-04-09 02:55:59 +08:00
Pachakutiq
90f27c2974 feat: Support short flag -x for proxy 2025-04-09 02:54:42 +08:00
Pachakutiq
ec03439b2f style: Format README.md using prettier 2025-04-09 02:11:22 +08:00
Pachakutiq
5ad89b2ea8 feat: Support proxy for serve 2025-04-09 02:08:01 +08:00
Pachakutiq
b2ffcf8477 feat: Support http and socks5 proxy (support authentication) for chrome driver mode 2025-04-08 23:51:44 +08:00
Pachakutiq
21f12c5996 fix: Google updated DOM of search result page 2025-04-08 18:48:07 +08:00
Pachakutiq
84e2d46c4b feat: Support http and socks5 proxy with auth for raw search 2025-04-08 16:06:41 +08:00
Pachakutiq
205f722ddc fix: Fix search crash (slice bounds out of range [4:1]) 2024-12-01 23:35:11 +08:00
Rustem Kamalov
aeb0616249 Fix google images selector + baidu images ads tag 2024-05-12 02:57:39 +03:00
Rustem Kamalov
fc49deba67 Fix yandex image search 2024-05-12 01:41:53 +03:00
17 changed files with 528 additions and 225 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ logrus.txt
logs.txt logs.txt
.release .release
core/test/ core/test/
.aider*

100
README.md
View File

@@ -1,18 +1,23 @@
# OpenSERP (Search Engine Results Page) # OpenSERP (Search Engine Results Page)
![OpenSERP](/logo.png) ![OpenSERP](/logo.png)
[![Go Report Card](https://goreportcard.com/badge/github.com/karust/openserp)](https://goreportcard.com/report/github.com/karust/openserp) [![Go Report Card](https://goreportcard.com/badge/github.com/karust/openserp)](https://goreportcard.com/report/github.com/karust/openserp)
[![Go Reference](https://pkg.go.dev/badge/github.com/karust/openserp.svg)](https://pkg.go.dev/github.com/karust/openserp) [![Go Reference](https://pkg.go.dev/badge/github.com/karust/openserp.svg)](https://pkg.go.dev/github.com/karust/openserp)
[![release](https://img.shields.io/github/release-pre/karust/openserp.svg)](https://github.com/karust/openserp/releases) [![release](https://img.shields.io/github/release-pre/karust/openserp.svg)](https://github.com/karust/openserp/releases)
<!-- ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/karust/openserp/latest) --> <!-- ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/karust/openserp/latest) -->
API access for search engines results if available isn't free. API access for search engines results if available isn't free.
Using OpenSERP, you can get search results from **Google**, **Yandex**, **Baidu** via API or CLI! Using OpenSERP, you can get search results from **Google**, **Yandex**, **Baidu** via API or CLI!
See [Docker](#docker) and [CLI](#cli) usage examples below ([search](#search), [images](#images)). See [Docker](#docker) and [CLI](#cli) usage examples below ([search](#search), [images](#images)).
## Docker usage <a name="docker"></a> 🐳 ## Docker usage <a name="docker"></a> 🐳
* Run API server:
- Run API server:
```bash ```bash
# Use prebuilt image # Use prebuilt image
docker run -p 127.0.0.1:7000:7000 -it karust/openserp serve -a 0.0.0.0 -p 7000 docker run -p 127.0.0.1:7000:7000 -it karust/openserp serve -a 0.0.0.0 -p 7000
@@ -22,26 +27,32 @@ docker-compose up --build
``` ```
### Request parameters ### Request parameters
| Param | Description |
|-------|--------------------------------------------------------------| | Param | Description |
| text | Text to search | | ------- | ---------------------------------------------------------------------- |
| lang | Search pages in selected language (`EN`, `DE`, `RU`...) | | text | Text to search |
| date | Date in `YYYYMMDD..YYYYMMDD` format (e.g. 20181010..20231010) | | lang | Search pages in selected language (`EN`, `DE`, `RU`...) |
| file | File extension to search (e.g. `PDF`, `DOC`) | | date | Date in `YYYYMMDD..YYYYMMDD` format (e.g. 20181010..20231010) |
| site | Search within a specific website | | file | File extension to search (e.g. `PDF`, `DOC`) |
| limit | Limit the number of results | site | Search within a specific website |
| answers | Include google answers as negative rank indexes (e.g. `true`, `false`) | limit | Limit the number of results |
| answers | Include google answers as negative rank indexes (e.g. `true`, `false`) |
### **Search** ### **Search**
### *Example request*
### _Example request_
Get 20 **Google** results for `hello world`, only in English: Get 20 **Google** results for `hello world`, only in English:
``` ```
GET http:/127.0.0.1:7000/google/search?lang=EN&limit=20&text=hello world GET http:/127.0.0.1:7000/google/search?lang=EN&limit=20&text=hello world
``` ```
You can replace `google` to `yandex` or `baidu` in query to change search engine.
|
### *Example response* You can replace `google` to `yandex` or `baidu` in query to change search engine.
|
### _Example response_
```JSON ```JSON
[ [
{ {
@@ -53,39 +64,66 @@ You can replace `google` to `yandex` or `baidu` in query to change search engine
}, },
] ]
``` ```
### **Images** **[WIP]** ### **Images** **[WIP]**
### *Example request*
### _Example request_
Get 100 **Google** results for `golden puppy`: Get 100 **Google** results for `golden puppy`:
``` ```
GET http://127.0.0.1:7000/google/image?text=golden puppy&limit=100 GET http://127.0.0.1:7000/google/image?text=golden puppy&limit=100
``` ```
## CLI <a name="cli"></a> ⌨️ ## CLI <a name="cli"></a> ⌨️
* Use `-h` flag to see commands.
* You can use `serve` command to serve API: - Use `-h` flag to see all commands.
- You can use `serve` command to serve API:
```bash ```bash
openserp serve openserp serve
``` ```
* Or print results in CLI using `search` command:
- Or print results in CLI using `search` command:
```bash ```bash
openserp search google "how to get banned from google fast" # Change `google` to `yandex` or `baidu` openserp search google "how to get banned from google fast" # Change `google` to `yandex` or `baidu`
``` ```
As a result you should get JSON output containting search results: As a result you should get JSON output containting search results:
```json ```json
[ [
{ {
"rank": 1, "rank": 1,
"url": "https://www.cyberoptik.net/blog/6-sure-fire-ways-to-get-banned-from-google/", "url": "https://www.cyberoptik.net/blog/6-sure-fire-ways-to-get-banned-from-google/",
"title": "11 Sure-Fire Ways to Get Banned From Google | CyberOptik", "title": "11 Sure-Fire Ways to Get Banned From Google | CyberOptik",
"description": "How To Get Banned From Google · 1. Cloaking: The Art of Deception · 2. Plagiarism: Because Originality is Overrated · 3. Keyword Stuffing: More is Always Better · 4 ...", "description": "How To Get Banned From Google · 1. Cloaking: The Art of Deception · 2. Plagiarism: Because Originality is Overrated · 3. Keyword Stuffing: More is Always Better · 4 ...",
"ad": false "ad": false
}, }
] ]
``` ```
### Proxy Support
Both browser mode and raw mode support HTTP and SOCKS5 proxies with authentication:
```bash
# HTTP proxy with auth
openserp search google "query" --proxy http://user:pass@127.0.0.1:8080
# Serve with SOCKS5 proxy
openserp serve --proxy socks5://127.0.0.1:1080
# For HTTPS sites through HTTP proxy, use --insecure to ignore certificate errors
openserp search google "query" --proxy http://127.0.0.1:8080 --insecure
openserp search google "query" -x http://127.0.0.1:8080 -k
```
## License
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
## Bugs + Questions 👾 ## Bugs + Questions 👾
If you have some issues/bugs/questions, feel free to open an issue.
If you have some issues/bugs/questions, feel free to open an issue.

View File

@@ -24,19 +24,11 @@ type imageDataJson struct {
Height int Height int
Width int Width int
IsCopyright int IsCopyright int
AdType string `json:"adType"`
URL []struct { URL []struct {
SourcePage string `json:"FromURL"` SourcePage string `json:"FromURL"`
Original string `json:"ObjURL"` Original string `json:"ObjURL"`
} `json:"replaceUrl"` } `json:"replaceUrl"`
// Versions []struct {
// Height int
// Width int
// ImgSourcePage string `json:"fromURL"`
// URL string `json:"objURL"`
// Type string
// } `json:"setList"`
} }
} }
@@ -63,18 +55,12 @@ func (baid *Baidu) GetRateLimiter() *rate.Limiter {
func (baid *Baidu) isCaptcha(page *rod.Page) bool { func (baid *Baidu) isCaptcha(page *rod.Page) bool {
_, err := page.Timeout(baid.GetSelectorTimeout()).Search("div.passMod_dialog-body") _, err := page.Timeout(baid.GetSelectorTimeout()).Search("div.passMod_dialog-body")
if err != nil { return err == nil
return false
}
return true
} }
func (baid *Baidu) isTimeout(page *rod.Page) bool { func (baid *Baidu) isTimeout(page *rod.Page) bool {
_, err := page.Timeout(baid.GetSelectorTimeout()).Search("button.timeout-button") _, err := page.Timeout(baid.GetSelectorTimeout()).Search("button.timeout-button")
if err != nil { return err == nil
return false
}
return true
} }
func (baid *Baidu) Search(query core.Query) ([]core.SearchResult, error) { func (baid *Baidu) Search(query core.Query) ([]core.SearchResult, error) {
@@ -88,7 +74,10 @@ func (baid *Baidu) Search(query core.Query) ([]core.SearchResult, error) {
return nil, err return nil, err
} }
page := baid.Navigate(url) page, err := baid.Navigate(url)
if err != nil {
return nil, err
}
results, err := page.Timeout(baid.Timeout).Search("div.c-container.new-pmd") results, err := page.Timeout(baid.Timeout).Search("div.c-container.new-pmd")
if err != nil { if err != nil {
@@ -152,7 +141,7 @@ func (baid *Baidu) Search(query core.Query) ([]core.SearchResult, error) {
} }
} }
return searchResults, nil return core.DeduplicateResults(searchResults), nil
} }
func (baid *Baidu) SearchImage(query core.Query) ([]core.SearchResult, error) { func (baid *Baidu) SearchImage(query core.Query) ([]core.SearchResult, error) {
@@ -168,7 +157,11 @@ func (baid *Baidu) SearchImage(query core.Query) ([]core.SearchResult, error) {
} }
// Get anti-crawler cookies first, then reload page // Get anti-crawler cookies first, then reload page
page := baid.Navigate(url) page, err := baid.Navigate(url)
if err != nil {
return nil, err
}
if !baid.LeavePageOpen { if !baid.LeavePageOpen {
defer page.Close() defer page.Close()
} }
@@ -226,7 +219,15 @@ func (baid *Baidu) SearchImage(query core.Query) ([]core.SearchResult, error) {
Rank: (searchPage * 30) + (i + 1), Rank: (searchPage * 30) + (i + 1),
URL: img.URL[0].Original, URL: img.URL[0].Original,
Title: img.Title, Title: img.Title,
Description: fmt.Sprintf("%v,%v,%vx%x,copyright:%v", img.PictureDate, img.Type, img.Height, img.Width, img.IsCopyright)} Description: fmt.Sprintf("%v,%v,%vx%x,copyright:%v", img.PictureDate, img.Type, img.Height, img.Width, img.IsCopyright),
Ad: func() bool {
if img.AdType != "0" {
return true
} else {
return false
}
}(),
}
searchResults = append(searchResults, res) searchResults = append(searchResults, res)
} }
@@ -237,5 +238,5 @@ func (baid *Baidu) SearchImage(query core.Query) ([]core.SearchResult, error) {
} }
} }
return searchResults, nil return core.DeduplicateResults(searchResults), nil
} }

View File

@@ -1,9 +1,12 @@
package baidu package baidu
import ( import (
"crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/corpix/uarand" "github.com/corpix/uarand"
@@ -11,8 +14,27 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func baiduRequest(searchURL string) (*http.Response, error) { func baiduRequest(searchURL string, query core.Query) (*http.Response, error) {
baseClient := &http.Client{} // Create HTTP transport with proxy
transport := &http.Transport{}
if query.ProxyURL != "" {
proxyUrl, err := url.Parse(query.ProxyURL)
if err != nil {
return nil, err
}
transport.Proxy = http.ProxyURL(proxyUrl)
}
// Set insecure TLS
if query.Insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
baseClient := &http.Client{
Transport: transport,
Timeout: time.Second * 10,
}
req, err := http.NewRequest("GET", searchURL, nil) req, err := http.NewRequest("GET", searchURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -27,7 +49,7 @@ func baiduRequest(searchURL string) (*http.Response, error) {
} }
func baiduResultParser(response *http.Response) ([]core.SearchResult, error) { func baiduResultParser(response *http.Response) ([]core.SearchResult, error) {
doc, err := goquery.NewDocumentFromResponse(response) doc, err := goquery.NewDocumentFromReader(response.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -79,7 +101,7 @@ func Search(query core.Query) ([]core.SearchResult, error) {
} }
logrus.Debugf("Baidu URL built: %s", googleURL) logrus.Debugf("Baidu URL built: %s", googleURL)
res, err := baiduRequest(googleURL) res, err := baiduRequest(googleURL, query)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -91,5 +113,5 @@ func Search(query core.Query) ([]core.SearchResult, error) {
} }
logrus.Debugf("Baidu Raw results : %v", results) logrus.Debugf("Baidu Raw results : %v", results)
return results, nil return core.DeduplicateResults(results), nil
} }

View File

@@ -12,7 +12,7 @@ import (
) )
const ( const (
version = "0.3" version = "0.4.1"
defaultConfigFilename = "config" defaultConfigFilename = "config"
envPrefix = "OPENSERP" envPrefix = "OPENSERP"
) )
@@ -40,6 +40,9 @@ type AppConfig struct {
IsDebug bool `mapstructure:"debug"` IsDebug bool `mapstructure:"debug"`
IsVerbose bool `mapstructure:"verbose"` IsVerbose bool `mapstructure:"verbose"`
IsRawRequests bool `mapstructure:"raw_requests"` IsRawRequests bool `mapstructure:"raw_requests"`
ProxyURL string `mapstructure:"proxy"`
Insecure bool `mapstructure:"insecure"`
IsStealth bool `mapstructure:"stealth"`
} }
var config = Config{} var config = Config{}
@@ -133,4 +136,6 @@ func init() {
RootCmd.PersistentFlags().BoolVarP(&config.App.IsRawRequests, "raw", "r", false, "Disable browser usage, use HTTP requests") RootCmd.PersistentFlags().BoolVarP(&config.App.IsRawRequests, "raw", "r", false, "Disable browser usage, use HTTP requests")
RootCmd.PersistentFlags().BoolVarP(&config.App.IsLeaveHead, "leave", "", false, "Leave browser and tabs opened after search is made") RootCmd.PersistentFlags().BoolVarP(&config.App.IsLeaveHead, "leave", "", false, "Leave browser and tabs opened after search is made")
RootCmd.PersistentFlags().StringVarP(&config.Config2Capcha.ApiKey, "2captcha_key", "", "", "2 captcha api key") RootCmd.PersistentFlags().StringVarP(&config.Config2Capcha.ApiKey, "2captcha_key", "", "", "2 captcha api key")
RootCmd.PersistentFlags().StringVarP(&config.App.ProxyURL, "proxy", "x", "", "HTTP or Socks5 proxy URL (e.g. http://user:pass@127.0.0.1:8080)")
RootCmd.PersistentFlags().BoolVarP(&config.App.IsStealth, "stealth", "s", false, "Use stealth browser plugin")
} }

View File

@@ -26,8 +26,10 @@ func search(cmd *cobra.Command, args []string) {
var err error var err error
engineType := args[0] engineType := args[0]
query := core.Query{ query := core.Query{
Text: args[1], Text: args[1],
Limit: 10, Limit: 10,
ProxyURL: config.App.ProxyURL,
Insecure: config.App.Insecure,
} }
results := []core.SearchResult{} results := []core.SearchResult{}
@@ -59,6 +61,9 @@ func searchBrowser(engineType string, query core.Query) ([]core.SearchResult, er
Timeout: time.Second * time.Duration(config.App.Timeout), Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead, LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverApiKey: config.Config2Capcha.ApiKey, CaptchaSolverApiKey: config.Config2Capcha.ApiKey,
ProxyURL: config.App.ProxyURL,
Insecure: config.App.Insecure,
UseStealth: config.App.IsStealth,
} }
if config.App.IsDebug { if config.App.IsDebug {

View File

@@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"fmt"
"time" "time"
"github.com/karust/openserp/baidu" "github.com/karust/openserp/baidu"
@@ -9,8 +10,48 @@ import (
"github.com/karust/openserp/yandex" "github.com/karust/openserp/yandex"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/time/rate"
) )
// rawEngine implements SearchEngine interface for raw HTTP requests
type rawEngine struct {
name string
}
func (r *rawEngine) Search(q core.Query) ([]core.SearchResult, error) {
// Inject proxy settings from config
q.ProxyURL = config.App.ProxyURL
q.Insecure = config.App.Insecure
switch r.name {
case "google":
return google.Search(q)
case "yandex":
return yandex.Search(q)
case "baidu":
return baidu.Search(q)
default:
return nil, fmt.Errorf("unsupported engine: %s", r.name)
}
}
func (r *rawEngine) SearchImage(q core.Query) ([]core.SearchResult, error) {
return nil, fmt.Errorf("image search is not supported in raw mode for %s", r.name)
}
func (r *rawEngine) Name() string {
return r.name
}
func (r *rawEngine) IsInitialized() bool {
return true
}
func (r *rawEngine) GetRateLimiter() *rate.Limiter {
// Use default rate limiter for raw requests
return rate.NewLimiter(rate.Every(time.Second), 5)
}
var serveCMD = &cobra.Command{ var serveCMD = &cobra.Command{
Use: "serve", Use: "serve",
Aliases: []string{"listen"}, Aliases: []string{"listen"},
@@ -20,12 +61,26 @@ var serveCMD = &cobra.Command{
} }
func serve(cmd *cobra.Command, args []string) { func serve(cmd *cobra.Command, args []string) {
if config.App.IsRawRequests {
logrus.Warn("Browserless results are very inconsistent or may not even work!")
serv := core.NewServer(config.App.Host, config.App.Port,
&rawEngine{name: "google"},
&rawEngine{name: "yandex"},
&rawEngine{name: "baidu"},
)
serv.Listen()
return
}
opts := core.BrowserOpts{ opts := core.BrowserOpts{
IsHeadless: !config.App.IsBrowserHead, // Disable headless if browser head mode is set IsHeadless: !config.App.IsBrowserHead, // Disable headless if browser head mode is set
IsLeakless: config.App.IsLeakless, IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout), Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead, LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverApiKey: config.Config2Capcha.ApiKey, CaptchaSolverApiKey: config.Config2Capcha.ApiKey,
ProxyURL: config.App.ProxyURL,
Insecure: config.App.Insecure,
UseStealth: config.App.IsBrowserHead,
} }
if config.App.IsDebug { if config.App.IsDebug {
@@ -35,6 +90,7 @@ func serve(cmd *cobra.Command, args []string) {
browser, err := core.NewBrowser(opts) browser, err := core.NewBrowser(opts)
if err != nil { if err != nil {
logrus.Error(err) logrus.Error(err)
return
} }
yand := yandex.New(*browser, config.YandexConfig) yand := yandex.New(*browser, config.YandexConfig)

View File

@@ -7,6 +7,8 @@ app:
head: false head: false
leakless: false leakless: false
leave_head: false leave_head: false
stealth: false
insecure: false
2captcha: 2captcha:
apikey: "123123123123123" apikey: "123123123123123"

View File

@@ -1,6 +1,8 @@
package core package core
import ( import (
"fmt"
"net/url"
"time" "time"
"github.com/go-rod/rod" "github.com/go-rod/rod"
@@ -19,6 +21,9 @@ type BrowserOpts struct {
LeavePageOpen bool // Leave pages and browser open LeavePageOpen bool // Leave pages and browser open
WaitLoadTime time.Duration // Time to wait till page loads WaitLoadTime time.Duration // Time to wait till page loads
CaptchaSolverApiKey string // 2Captcha api key CaptchaSolverApiKey string // 2Captcha api key
ProxyURL string // Proxy URL
Insecure bool // Allow insecure TLS connections
UseStealth bool // Use go-rod stealth plugin
} }
@@ -47,9 +52,33 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) {
path, has := launcher.LookPath() path, has := launcher.LookPath()
logrus.Debug("Browser found: ", has) logrus.Debug("Browser found: ", has)
// Create launcher
l := launcher.New().Bin(path).Leakless(opts.IsLeakless).Headless(opts.IsHeadless)
// Configure proxy if specified
if opts.ProxyURL != "" {
proxyUrl, err := url.Parse(opts.ProxyURL)
if err != nil {
return nil, fmt.Errorf("invalid proxy URL: %v", err)
}
// Make sure the proxy URL includes the scheme when passed to launcher
// This ensures proper handling of SOCKS5 proxies
proxyStr := proxyUrl.String()
logrus.Debugf("Setting up proxy: %s", proxyStr)
l = l.Proxy(proxyStr)
// Check if proxy has auth credentials
if proxyUrl.User != nil {
username := proxyUrl.User.Username()
logrus.Debugf("Using proxy authentication: %s:****", username)
// We'll handle auth in the Navigate method
}
}
var err error var err error
b := Browser{BrowserOpts: opts} b := Browser{BrowserOpts: opts}
b.browserAddr, err = launcher.New().Bin(path).Leakless(opts.IsLeakless).Headless(opts.IsHeadless).Launch() b.browserAddr, err = l.Launch()
if opts.CaptchaSolverApiKey != "" { if opts.CaptchaSolverApiKey != "" {
b.CaptchaSolver = NewSolver(opts.CaptchaSolverApiKey) b.CaptchaSolver = NewSolver(opts.CaptchaSolverApiKey)
@@ -69,20 +98,47 @@ func (b *Browser) IsInitialized() bool {
} }
// Open URL // Open URL
func (b *Browser) Navigate(URL string) *rod.Page { func (b *Browser) Navigate(URL string) (*rod.Page, error) {
logrus.Debug("Navigate to: ", URL) logrus.Debug("Navigate to: ", URL)
b.browser = rod.New().ControlURL(b.browserAddr) b.browser = rod.New().ControlURL(b.browserAddr)
b.browser.MustConnect() b.browser.MustConnect()
b.browser.SetCookies(nil) b.browser.SetCookies(nil)
//page := b.browser.MustPage(URL) // Handle proxy authentication before any navigations
page := stealth.MustPage(b.browser) if b.ProxyURL != "" {
proxyUrl, _ := url.Parse(b.ProxyURL)
// Always ignore certificate errors when using proxies
// This fixes the ERR_CERT_AUTHORITY_INVALID error for SOCKS5 proxies
b.browser.MustIgnoreCertErrors(true)
if proxyUrl.User != nil {
username := proxyUrl.User.Username()
password, _ := proxyUrl.User.Password()
// Launch auth handler before any navigation occurs
go b.browser.MustHandleAuth(username, password)()
}
} else if b.Insecure {
// Still respect the insecure flag if no proxy is used
b.browser.MustIgnoreCertErrors(true)
}
var page *rod.Page
if b.UseStealth {
page = stealth.MustPage(b.browser)
} else {
page = b.browser.MustPage(URL)
}
page.MustEmulate(devices.Device{ page.MustEmulate(devices.Device{
//UserAgent: uarand.GetRandom(),
AcceptLanguage: b.LanguageCode, AcceptLanguage: b.LanguageCode,
}) })
page.MustNavigate(URL)
err := page.Navigate(URL)
if err != nil {
return nil, err
}
wait := page.MustWaitRequestIdle() wait := page.MustWaitRequestIdle()
// may cause bugs with google // may cause bugs with google
@@ -90,10 +146,7 @@ func (b *Browser) Navigate(URL string) *rod.Page {
wait() wait()
} }
// Wait till page loads return page, nil
//time.Sleep(b.WaitLoadTime)
return page
} }
func (b *Browser) Close() error { func (b *Browser) Close() error {

View File

@@ -38,13 +38,12 @@ func TestBot(t *testing.T) {
t.Fatalf("Error failed initializing browser: %s", err) t.Fatalf("Error failed initializing browser: %s", err)
} }
page := browser.Navigate("https://bot.sannysoft.com") page, _ := browser.Navigate("https://bot.sannysoft.com")
page.MustScreenshotFullPage("./test/screenshot_bot.png") page.MustScreenshotFullPage("./test/screenshot_bot.png")
page = browser.Navigate("https://www.whatismybrowser.com/") page, _ = browser.Navigate("https://www.whatismybrowser.com/")
page.MustScreenshotFullPage("./test/screenshot_browser.png") page.MustScreenshotFullPage("./test/screenshot_browser.png")
page = browser.Navigate("https://abrahamjuliot.github.io/creepjs/") page, _ = browser.Navigate("https://abrahamjuliot.github.io/creepjs/")
page.MustScreenshotFullPage("./test/screenshot_creep.png") page.MustScreenshotFullPage("./test/screenshot_creep.png")
} }

View File

@@ -20,6 +20,26 @@ type SearchResult struct {
Ad bool `json:"ad"` Ad bool `json:"ad"`
} }
func DeduplicateResults(results []SearchResult) []SearchResult {
unique := make(map[string]bool)
var deduped []SearchResult
for _, result := range results {
if result.URL == "" {
continue
}
if !unique[result.URL] {
unique[result.URL] = true
deduped = append(deduped, result)
}
}
sort.Slice(deduped, func(i, j int) bool {
return deduped[i].Rank < deduped[j].Rank
})
return deduped
}
func ConvertSearchResultsMap(searchResultsMap map[string]SearchResult) *[]SearchResult { func ConvertSearchResultsMap(searchResultsMap map[string]SearchResult) *[]SearchResult {
searchResults := []SearchResult{} searchResults := []SearchResult{}
@@ -41,6 +61,8 @@ type Query struct {
Site string // Search site Site string // Search site
Limit int // Limit the number of results Limit int // Limit the number of results
Answers bool // Include question and answers from SERP page to results with negative indexes Answers bool // Include question and answers from SERP page to results with negative indexes
ProxyURL string // Proxy URL for raw requests
Insecure bool // Allow insecure TLS connections
} }
func (q Query) IsEmpty() bool { func (q Query) IsEmpty() bool {

View File

@@ -70,7 +70,7 @@ func (gogl *Google) getTotalResults(page *rod.Page) (int, error) {
func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas string) bool { func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas string) bool {
logrus.Debugf("Solve google Captcha: sitekey=%s, datas=%s, url=%s", sitekey, datas, page.MustInfo().URL) logrus.Debugf("Solve google Captcha: sitekey=%s, datas=%s, url=%s", sitekey, datas, page.MustInfo().URL)
resp, err := gogl.CaptchaSolver.SolveReCaptcha2(sitekey, page.MustInfo().URL, datas) resp, err := gogl.CaptchaSolver.SolveReCaptcha2(sitekey, page.MustInfo().URL)
if err != nil { if err != nil {
logrus.Errorf("Error solving google captcha: %s", err) logrus.Errorf("Error solving google captcha: %s", err)
return false return false
@@ -132,6 +132,7 @@ func (gogl *Google) acceptCookies(page *rod.Page) {
btnElms[3].Click(proto.InputMouseButtonLeft, 1) btnElms[3].Click(proto.InputMouseButtonLeft, 1)
} }
func (gogl *Google) Search(query core.Query) ([]core.SearchResult, error) { func (gogl *Google) Search(query core.Query) ([]core.SearchResult, error) {
logrus.Tracef("Start Google search, query: %+v", query) logrus.Tracef("Start Google search, query: %+v", query)
@@ -142,7 +143,11 @@ func (gogl *Google) Search(query core.Query) ([]core.SearchResult, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
page := gogl.Navigate(url) page, err := gogl.Navigate(url)
if err != nil {
return nil, err
}
defer gogl.close(page) defer gogl.close(page)
gogl.preparePage(page) gogl.preparePage(page)
@@ -157,8 +162,8 @@ func (gogl *Google) Search(query core.Query) ([]core.SearchResult, error) {
gogl.acceptCookies(page) gogl.acceptCookies(page)
} }
// Find all results // Find all results using stable attributes
results, err := page.Timeout(gogl.Timeout).Search("div[data-hveid]") results, err := page.Timeout(gogl.Timeout).Search("div[data-hveid][data-ved]")
if err != nil { if err != nil {
logrus.Errorf("Cannot parse search results: %s", err) logrus.Errorf("Cannot parse search results: %s", err)
return nil, core.ErrSearchTimeout return nil, core.ErrSearchTimeout
@@ -265,35 +270,56 @@ func (gogl *Google) Search(query core.Query) ([]core.SearchResult, error) {
searchResults = append(searchResults, srchRes) searchResults = append(searchResults, srchRes)
} }
continue continue
} else if strings.Contains(attrs, "data-ved") && strings.Contains(attrs, "lang") { } else if strings.Contains(attrs, "data-ved") {
// 3. Parse regular search results // Parse regular search results
// Get URL // Get title from h3
link, err := resEl.Element("a") titleTag, err := resEl.Element("h3")
if err != nil { if err != nil {
continue continue
} }
href, err := link.Property("href") srchRes.Title, _ = titleTag.Text()
if err != nil {
logrus.Debug("No `href` tag found") // Get URL from parent link of h3
link, err := titleTag.Parent()
if err == nil && link.MustMatches("a") {
href, _ := link.Property("href")
srchRes.URL = href.String()
} }
srchRes.URL = href.String()
// Skip if URL is empty
if srchRes.URL == "" {
continue
}
// Get description using multiple fallback strategies
desc := ""
if descTag, err := resEl.Element("div[data-sncf='1'] div"); err == nil {
desc = descTag.MustText()
} else if descTag, err := resEl.Element("div.VwiC3b"); err == nil {
desc = descTag.MustText()
} else {
// Structural fallback
parent, err := titleTag.Parent()
if err == nil {
parent, err = parent.Parent()
if err == nil {
parent, err = parent.Parent()
if err == nil {
if descTag, err := parent.Next(); err == nil {
if descDiv, err := descTag.Element("div"); err == nil {
desc = descDiv.MustText()
}
}
}
}
}
}
srchRes.Description = desc
rank += 1 rank += 1
srchRes.Rank = rank
// Get title searchResults = append(searchResults, srchRes)
titleTag, err := link.Element("h3") continue
if err != nil {
continue
}
srchRes.Title, err = titleTag.Text()
if err != nil {
logrus.Debug("Cannot extract text from title")
}
// Get description
text := resEl.MustText()
textSliced := strings.Split(text, "\n")
srchRes.Description = strings.Join(textSliced[4:], "\n")
} else { } else {
//fmt.Println(i, attrs) //fmt.Println(i, attrs)
@@ -304,7 +330,7 @@ func (gogl *Google) Search(query core.Query) ([]core.SearchResult, error) {
searchResults = append(searchResults, srchRes) searchResults = append(searchResults, srchRes)
} }
return searchResults, nil return core.DeduplicateResults(searchResults), nil
} }
func (gogl *Google) SearchImage(query core.Query) ([]core.SearchResult, error) { func (gogl *Google) SearchImage(query core.Query) ([]core.SearchResult, error) {
@@ -316,17 +342,12 @@ func (gogl *Google) SearchImage(query core.Query) ([]core.SearchResult, error) {
return nil, err return nil, err
} }
page := gogl.Navigate(url) page, err := gogl.Navigate(url)
defer gogl.close(page) if err != nil {
return nil, err
}
//// TODO: Case with cookie accept (appears with VPN) defer gogl.close(page)
// if page.MustInfo().URL != url {
// results, _ := page.Search("button[aria-label][jsaction]")
// if results != nil {
// //buttons, _ := results.All()
// //buttons[1].Click(proto.InputMouseButtonLeft, 1)
// }
// }
for len(searchResultsMap) < query.Limit { for len(searchResultsMap) < query.Limit {
page.WaitLoad() page.WaitLoad()
@@ -365,18 +386,19 @@ func (gogl *Google) SearchImage(query core.Query) ([]core.SearchResult, error) {
continue continue
} }
dataID, err := r.Attribute("data-id") dataVed, err := r.Attribute("data-ved")
if err != nil { if err != nil {
logrus.Error("Cannot find `data-ved` attr")
continue continue
} }
// If already have image with this ID // If already have image with this ID
if _, ok := searchResultsMap[*dataID]; ok { if _, ok := searchResultsMap[*dataVed]; ok {
continue continue
} }
// Get URLs // Get URLs
link, err := r.Element("a[tabindex][role]") link, err := r.Element("a:not([ping])")
if err != nil { if err != nil {
continue continue
} }
@@ -411,7 +433,7 @@ func (gogl *Google) SearchImage(query core.Query) ([]core.SearchResult, error) {
Title: title, Title: title,
Description: fmt.Sprintf("Height:%v, Width:%v, Source Page: %v", imgSrc.Height, imgSrc.Width, imgSrc.PageURL), Description: fmt.Sprintf("Height:%v, Width:%v, Source Page: %v", imgSrc.Height, imgSrc.Width, imgSrc.PageURL),
} }
searchResultsMap[*dataID] = gR searchResultsMap[*dataVed] = gR
r.Remove() r.Remove()
} }

View File

@@ -1,8 +1,11 @@
package google package google
import ( import (
"crypto/tls"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/corpix/uarand" "github.com/corpix/uarand"
@@ -10,8 +13,27 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func googleRequest(searchURL string) (*http.Response, error) { func googleRequest(searchURL string, query core.Query) (*http.Response, error) {
baseClient := &http.Client{} // Create HTTP transport with proxy
transport := &http.Transport{}
if query.ProxyURL != "" {
proxyUrl, err := url.Parse(query.ProxyURL)
if err != nil {
return nil, err
}
transport.Proxy = http.ProxyURL(proxyUrl)
}
// Set insecure TLS
if query.Insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
baseClient := &http.Client{
Transport: transport,
Timeout: time.Second * 10,
}
req, err := http.NewRequest("GET", searchURL, nil) req, err := http.NewRequest("GET", searchURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -26,7 +48,7 @@ func googleRequest(searchURL string) (*http.Response, error) {
} }
func googleResultParser(response *http.Response) ([]core.SearchResult, error) { func googleResultParser(response *http.Response) ([]core.SearchResult, error) {
doc, err := goquery.NewDocumentFromResponse(response) doc, err := goquery.NewDocumentFromReader(response.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -34,23 +56,49 @@ func googleResultParser(response *http.Response) ([]core.SearchResult, error) {
results := []core.SearchResult{} results := []core.SearchResult{}
rank := 1 rank := 1
// Get individual results // Use data attributes instead of class names to find results
sel := doc.Find("div.g") // Both old and new DOM have data-hveid and data-ved attributes
sel := doc.Find("div[data-hveid][data-ved]")
for i := range sel.Nodes { for i := range sel.Nodes {
item := sel.Eq(i) item := sel.Eq(i)
// Find URL // Skip items without an h3 element (which indicates a search result)
linkTag := item.Find("a") if item.Find("h3").Length() == 0 {
link, _ := linkTag.Attr("href") continue
}
// Find URL - look for the anchor that contains the h3 title
linkTag := item.Find("h3").Parent()
if linkTag.Is("a") == false {
linkTag = item.Find("h3").Closest("a")
}
link, exists := linkTag.Attr("href")
if !exists || link == "" || link == "#" {
continue
}
link = strings.Trim(link, " ") link = strings.Trim(link, " ")
// Find title // Find title - this is inside the h3 element
titleTag := item.Find("h3") titleTag := item.Find("h3")
title := titleTag.Text() title := titleTag.Text()
// Find description // Find description - find div with text content after the heading
descTag := item.Find(`div[data-sncf~="1"]`) // Using attribute selectors that match the description container
descTag := item.Find("div[data-sncf='1']").Find("div").First()
if descTag.Length() == 0 {
// Try another selector approach if the first one fails
descTag = item.Find("div.VwiC3b")
if descTag.Length() == 0 {
// As a last resort, look for any div after the title that might contain description
titleParent := titleTag.Parent()
if titleParent.Is("a") {
titleParent = titleParent.Parent().Parent()
}
descTag = titleParent.NextAll().First().Find("div").First()
}
}
desc := descTag.Text() desc := descTag.Text()
if link != "" && link != "#" { if link != "" && link != "#" {
@@ -67,7 +115,7 @@ func googleResultParser(response *http.Response) ([]core.SearchResult, error) {
} }
logrus.Tracef("Google search document size: %d", len(doc.Text())) logrus.Tracef("Google search document size: %d", len(doc.Text()))
return results, err return core.DeduplicateResults(results), err
} }
func Search(query core.Query) ([]core.SearchResult, error) { func Search(query core.Query) ([]core.SearchResult, error) {
@@ -77,7 +125,7 @@ func Search(query core.Query) ([]core.SearchResult, error) {
} }
logrus.Debugf("Google URL built: %s", googleURL) logrus.Debugf("Google URL built: %s", googleURL)
res, err := googleRequest(googleURL) res, err := googleRequest(googleURL, query)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -2,28 +2,36 @@ package yandex
import ( import (
"encoding/json" "encoding/json"
"fmt"
"sort"
"time" "time"
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/go-rod/rod/lib/input"
"github.com/karust/openserp/core" "github.com/karust/openserp/core"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
type YandexImageData struct { type ImageEntity struct {
SerpItem struct { ID string `json:"id"`
Freshness string Rank int `json:"pos"`
Snippet struct { Width int `json:"origWidth"`
Title string Height int `json:"origHeight"`
Text string Title string `json:"alt"`
URL string OrigURL string `json:"origUrl"`
Domain string ThumbURL string `json:"image"`
ShopScore int Freshness string `json:"freshnessCounter"`
} IsGIF bool `json:"gifLabel"`
ImgHref string `json:"img_href"` }
Pos int
} `json:"serp-item"` type ImageData struct {
InitalState struct {
SerpList struct {
Items struct {
Entities map[string]ImageEntity `json:"entities"`
} `json:"items"`
} `json:"serpList"`
} `json:"initialState"`
} }
type Yandex struct { type Yandex struct {
@@ -52,27 +60,13 @@ func (yand *Yandex) GetRateLimiter() *rate.Limiter {
func (yand *Yandex) isCaptcha(page *rod.Page) bool { func (yand *Yandex) isCaptcha(page *rod.Page) bool {
_, err := page.Timeout(yand.GetSelectorTimeout()).Search("form#checkbox-captcha-form") _, err := page.Timeout(yand.GetSelectorTimeout()).Search("form#checkbox-captcha-form")
if err != nil { return err == nil
return false
}
return true
} }
// Check if nothig is found // Check if nothig is found
func (yand *Yandex) isNoResults(page *rod.Page) bool { func (yand *Yandex) isNoResults(page *rod.Page) bool {
noResFound := false _, err := page.Timeout(yand.GetSelectorTimeout()).Search("div.Correction.SearchCorrection")
return err == nil
_, err := page.Timeout(yand.GetSelectorTimeout()).Search("div.EmptySearchResults-Title")
if err == nil {
noResFound = true
}
_, err = page.Timeout(yand.GetSelectorTimeout()).Search("div>div.RequestMeta-Message")
if err == nil {
noResFound = true
}
return noResFound
} }
func (yand *Yandex) parseResults(results rod.Elements, pageNum int) []core.SearchResult { func (yand *Yandex) parseResults(results rod.Elements, pageNum int) []core.SearchResult {
@@ -130,7 +124,10 @@ func (yand *Yandex) Search(query core.Query) ([]core.SearchResult, error) {
return nil, err return nil, err
} }
page := yand.Navigate(url) page, err := yand.Navigate(url)
if err != nil {
return nil, err
}
// Get all search results in page // Get all search results in page
searchRes, err := page.Timeout(yand.Timeout).Search("li.serp-item") searchRes, err := page.Timeout(yand.Timeout).Search("li.serp-item")
@@ -175,31 +172,36 @@ func (yand *Yandex) Search(query core.Query) ([]core.SearchResult, error) {
time.Sleep(yand.pageSleep) time.Sleep(yand.pageSleep)
} }
return allResults, nil return core.DeduplicateResults(allResults), nil
} }
func (yand *Yandex) SearchImage(query core.Query) ([]core.SearchResult, error) { func (yand *Yandex) SearchImage(query core.Query) ([]core.SearchResult, error) {
logrus.Tracef("Start Yandex image search, query: %+v", query) logrus.Tracef("Start Yandex image search, query: %+v", query)
searchResultsMap := map[string]core.SearchResult{} searchResults := []core.SearchResult{}
url, err := BuildImageURL(query)
if err != nil {
return nil, err
}
page := yand.Navigate(url) searchPage := 0
if !yand.LeavePageOpen { for len(searchResults) < query.Limit {
defer page.Close() url, err := BuildImageURL(query, searchPage)
} if err != nil {
return nil, err
}
searchPage += 1
for len(searchResultsMap) < query.Limit { page, err := yand.Navigate(url)
page.WaitLoad() if err != nil {
page.Keyboard.Press(input.End) return nil, err
page.WaitLoad() }
time.Sleep(time.Duration(time.Second * 2))
// Get all search results in page if !yand.LeavePageOpen {
results, err := page.Timeout(yand.Timeout).Search("div.serp-item") defer page.Close()
}
//page.Keyboard.Press(input.End)
//page.WaitLoad()
//time.Sleep(time.Duration(time.Second * 2))
results, err := page.Timeout(yand.Timeout).Search("div[role='main'] div[data-state]")
if err != nil { if err != nil {
logrus.Errorf("Cannot find search results: %s", err) logrus.Errorf("Cannot find search results: %s", err)
} }
@@ -208,45 +210,33 @@ func (yand *Yandex) SearchImage(query core.Query) ([]core.SearchResult, error) {
if results == nil { if results == nil {
if yand.isCaptcha(page) { if yand.isCaptcha(page) {
logrus.Errorf("Yandex captcha occurred during: %s", url) logrus.Errorf("Yandex captcha occurred during: %s", url)
return *core.ConvertSearchResultsMap(searchResultsMap), core.ErrCaptcha return searchResults, core.ErrCaptcha
} else if yand.isNoResults(page) { } else if yand.isNoResults(page) {
logrus.Errorf("No results found") logrus.Errorf("No results found")
} }
return *core.ConvertSearchResultsMap(searchResultsMap), core.ErrSearchTimeout return searchResults, core.ErrSearchTimeout
} }
for i := 0; i < results.ResultCount; i++ { data, err := results.First.Attribute("data-state")
r, err := results.Get(i, 1) if err != nil {
if err != nil { return nil, err
logrus.Errorf("Cannot [%v] element from search result, [%v total]: %s", i, results.ResultCount, err) }
return *core.ConvertSearchResultsMap(searchResultsMap), err
var imgData ImageData
if err := json.Unmarshal([]byte(*data), &imgData); err != nil {
return nil, err
}
for id := range imgData.InitalState.SerpList.Items.Entities {
img := imgData.InitalState.SerpList.Items.Entities[id]
res := core.SearchResult{
Rank: img.Rank + 1,
URL: img.OrigURL,
Title: img.Title,
Description: fmt.Sprintf("%dx%d, freshness:%s, thumb_url:%s", img.Height, img.Width, img.Freshness, img.ThumbURL),
} }
dataAttr, err := r[0].Attribute("data-bem") searchResults = append(searchResults, res)
if err != nil {
continue
}
var data YandexImageData
err = json.Unmarshal([]byte(*dataAttr), &data)
if err != nil {
logrus.Errorf("Cannot unmarshal yandex image data: %v\nData: %v", err, *dataAttr)
continue
}
linkText := data.SerpItem.ImgHref
title := data.SerpItem.Snippet.Title
description := data.SerpItem.Snippet.Text
yR := core.SearchResult{
Rank: (i + 1),
URL: linkText,
Title: title,
Description: description,
}
searchResultsMap[linkText+title] = yR
} }
if !yand.LeavePageOpen { if !yand.LeavePageOpen {
@@ -254,5 +244,9 @@ func (yand *Yandex) SearchImage(query core.Query) ([]core.SearchResult, error) {
} }
} }
return *core.ConvertSearchResultsMap(searchResultsMap), nil sort.Slice(searchResults, func(i, j int) bool {
return searchResults[i].Rank < searchResults[j].Rank
})
return core.DeduplicateResults(searchResults), nil
} }

View File

@@ -1,8 +1,11 @@
package yandex package yandex
import ( import (
"crypto/tls"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/corpix/uarand" "github.com/corpix/uarand"
@@ -10,8 +13,27 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func yandexRequest(searchURL string) (*http.Response, error) { func yandexRequest(searchURL string, query core.Query) (*http.Response, error) {
baseClient := &http.Client{} // Create HTTP transport with proxy
transport := &http.Transport{}
if query.ProxyURL != "" {
proxyUrl, err := url.Parse(query.ProxyURL)
if err != nil {
return nil, err
}
transport.Proxy = http.ProxyURL(proxyUrl)
}
// Set insecure TLS
if query.Insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
baseClient := &http.Client{
Transport: transport,
Timeout: time.Second * 10,
}
req, err := http.NewRequest("GET", searchURL, nil) req, err := http.NewRequest("GET", searchURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -26,7 +48,7 @@ func yandexRequest(searchURL string) (*http.Response, error) {
} }
func yandexResultParser(response *http.Response) ([]core.SearchResult, error) { func yandexResultParser(response *http.Response) ([]core.SearchResult, error) {
doc, err := goquery.NewDocumentFromResponse(response) doc, err := goquery.NewDocumentFromReader(response.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -67,7 +89,7 @@ func yandexResultParser(response *http.Response) ([]core.SearchResult, error) {
} }
logrus.Tracef("Yandex search document size: %d", len(doc.Text())) logrus.Tracef("Yandex search document size: %d", len(doc.Text()))
return results, err return core.DeduplicateResults(results), err
} }
func Search(query core.Query) ([]core.SearchResult, error) { func Search(query core.Query) ([]core.SearchResult, error) {
@@ -77,7 +99,7 @@ func Search(query core.Query) ([]core.SearchResult, error) {
} }
logrus.Debugf("Yandex URL built: %s", googleURL) logrus.Debugf("Yandex URL built: %s", googleURL)
res, err := yandexRequest(googleURL) res, err := yandexRequest(googleURL, query)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -14,6 +14,19 @@ func init() {
browser, _ = core.NewBrowser(opts) browser, _ = core.NewBrowser(opts)
} }
// func TestParseImgData(t *testing.T) {
// jsonData, _ := os.ReadFile("./testImgData.json")
// var obj ImageData
// if err := json.Unmarshal(jsonData, &obj); err != nil {
// t.Fatal(err)
// }
// if (len(obj.InitalState.SerpList.Items.Entities)) != 30 {
// t.Fail()
// }
// }
func TestSearchYandex(t *testing.T) { func TestSearchYandex(t *testing.T) {
yand := New(*browser, core.SearchEngineOptions{}) yand := New(*browser, core.SearchEngineOptions{})
@@ -32,13 +45,13 @@ func TestSearchYandex(t *testing.T) {
func TestImageYandex(t *testing.T) { func TestImageYandex(t *testing.T) {
yand := New(*browser, core.SearchEngineOptions{}) yand := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "furry tiger", Limit: 90} query := core.Query{Text: "furry tiger", Limit: 30}
results, err := yand.SearchImage(query) results, err := yand.SearchImage(query)
if err != nil { if err != nil {
t.Fatalf("Cannot [ImageYandex]: %s", err) t.Fatalf("Cannot [ImageYandex]: %s", err)
} }
if len(results) < 90 { if len(results) < 30 {
t.Fatalf("[ImageYandex] returned empty result") t.Fatalf("[ImageYandex] returned empty result")
} }
} }

View File

@@ -35,14 +35,14 @@ func BuildURL(q core.Query, page int) (string, error) {
} }
if len(params.Get("text")) == 0 { if len(params.Get("text")) == 0 {
return "", errors.New("Empty query built") return "", errors.New("empty query built")
} }
base.RawQuery = params.Encode() base.RawQuery = params.Encode()
return base.String(), nil return base.String(), nil
} }
func BuildImageURL(q core.Query) (string, error) { func BuildImageURL(q core.Query, page int) (string, error) {
// TODO: Add other parameters // TODO: Add other parameters
base, _ := url.Parse(baseURL) base, _ := url.Parse(baseURL)
base.Path += "images/search/" base.Path += "images/search/"
@@ -56,11 +56,11 @@ func BuildImageURL(q core.Query) (string, error) {
} }
params.Add("text", text) params.Add("text", text)
//params.Add("p", fmt.Sprint(page)) params.Add("p", fmt.Sprint(page))
} }
if len(params.Get("text")) == 0 { if len(params.Get("text")) == 0 {
return "", errors.New("Empty query built") return "", errors.New("empty query built")
} }
if q.Site != "" { if q.Site != "" {