mirror of
https://github.com/karust/openserp.git
synced 2026-08-26 09:42:08 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffe33d6bc5 | ||
|
|
df972585c1 | ||
|
|
dd5e0a2069 | ||
|
|
72bddf7fc9 | ||
|
|
c175d0c386 | ||
|
|
3f84503ce3 | ||
|
|
6c4eb8db1a |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ dev.md
|
|||||||
.gitignore
|
.gitignore
|
||||||
logrus.txt
|
logrus.txt
|
||||||
logs.txt
|
logs.txt
|
||||||
|
.release
|
||||||
|
core/test/
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ RUN go build -o /app/openserp .
|
|||||||
FROM zenika/alpine-chrome:with-chromedriver
|
FROM zenika/alpine-chrome:with-chromedriver
|
||||||
|
|
||||||
COPY --from=builder /app/openserp /usr/local/bin/openserp
|
COPY --from=builder /app/openserp /usr/local/bin/openserp
|
||||||
|
ADD config.yaml /usr/src/app
|
||||||
|
|
||||||
ENTRYPOINT ["openserp"]
|
ENTRYPOINT ["openserp"]
|
||||||
|
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -3,14 +3,13 @@
|
|||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/karust/openserp)
|
[](https://goreportcard.com/report/github.com/karust/openserp)
|
||||||
[](https://pkg.go.dev/github.com/karust/openserp)
|
[](https://pkg.go.dev/github.com/karust/openserp)
|
||||||
<!-- 
|
[](https://github.com/karust/openserp/releases)
|
||||||
 -->
|
<!--  -->
|
||||||
|
|
||||||
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), [CLI](#cli) usage examples below.
|
See [Docker](#docker) and [CLI](#cli) usage examples below.
|
||||||
|
|
||||||
## Docker usage <a name="docker"></a> 🐳
|
## Docker usage <a name="docker"></a> 🐳
|
||||||
* Run API server:
|
* Run API server:
|
||||||
@@ -18,13 +17,13 @@ See [Docker](#docker), [CLI](#cli) usage examples below.
|
|||||||
# 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
|
||||||
|
|
||||||
# Or
|
# Or build one and run using docker-compose.yaml
|
||||||
docker-compose up --build
|
docker-compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
### *Example request*
|
### *Example request*
|
||||||
Get 20 **Google** results for `hello world`, only in English:
|
Get 20 **Google** results for `hello world`, only in English:
|
||||||
```JSON
|
```
|
||||||
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.
|
You can replace `google` to `yandex` or `baidu` in query to change search engine.
|
||||||
@@ -36,14 +35,14 @@ You can replace `google` to `yandex` or `baidu` in query to change search engine
|
|||||||
| lang | Search pages in selected language (`EN`, `DE`, `RU`...) |
|
| lang | Search pages in selected language (`EN`, `DE`, `RU`...) |
|
||||||
| date | Date in `YYYYMMDD..YYYYMMDD` format (e.g. 20181010..20231010) |
|
| date | Date in `YYYYMMDD..YYYYMMDD` format (e.g. 20181010..20231010) |
|
||||||
| file | File extension to search (e.g. `PDF`, `DOC`) |
|
| file | File extension to search (e.g. `PDF`, `DOC`) |
|
||||||
| site | Search only in selected site |
|
| site | Search within a specific website |
|
||||||
| limit | Limit the number of results |
|
| limit | Limit the number of results |
|
||||||
|
|
||||||
### *Example response*
|
### *Example response*
|
||||||
```JSON
|
```JSON
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"rank": 0,
|
"rank": 1,
|
||||||
"url": "https://en.wikipedia.org/wiki/%22Hello,_World!%22_program",
|
"url": "https://en.wikipedia.org/wiki/%22Hello,_World!%22_program",
|
||||||
"title": "\"Hello, World!\" program",
|
"title": "\"Hello, World!\" program",
|
||||||
"description": "A \"Hello, World!\" program is generally a computer program that ignores any input, and outputs or displays a message similar to \"Hello, World!\"."
|
"description": "A \"Hello, World!\" program is generally a computer program that ignores any input, and outputs or displays a message similar to \"Hello, World!\"."
|
||||||
@@ -51,8 +50,6 @@ You can replace `google` to `yandex` or `baidu` in query to change search engine
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## CLI <a name="cli"></a> ⌨️
|
## CLI <a name="cli"></a> ⌨️
|
||||||
* Use `-h` flag to see commands.
|
* Use `-h` flag to see commands.
|
||||||
* You can use `serve` command to serve API:
|
* You can use `serve` command to serve API:
|
||||||
@@ -61,13 +58,13 @@ 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 in 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": 0,
|
"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 ..."
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func TestUrlBuild(t *testing.T) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
func TestSearchBaidu(t *testing.T) {
|
func TestSearchBaidu(t *testing.T) {
|
||||||
baid := New(*browser)
|
baid := New(*browser, core.SearchEngineOptions{})
|
||||||
results, err := baid.Search(testQuery)
|
results, err := baid.Search(testQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -2,26 +2,50 @@ package baidu
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
"github.com/karust/openserp/core"
|
"github.com/karust/openserp/core"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Baidu struct {
|
type Baidu struct {
|
||||||
core.Browser
|
core.Browser
|
||||||
checkTimeout time.Duration
|
core.SearchEngineOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(browser core.Browser) *Baidu {
|
func New(browser core.Browser, opts core.SearchEngineOptions) *Baidu {
|
||||||
baid := Baidu{Browser: browser}
|
baid := Baidu{Browser: browser}
|
||||||
baid.checkTimeout = time.Second * 2
|
opts.Init()
|
||||||
|
baid.SearchEngineOptions = opts
|
||||||
return &baid
|
return &baid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (baid *Baidu) Name() string {
|
func (baid *Baidu) Name() string {
|
||||||
return "baidu"
|
return "baidu"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (baid *Baidu) GetRateLimiter() *rate.Limiter {
|
||||||
|
ratelimit := rate.Every(baid.GetRatelimit())
|
||||||
|
return rate.NewLimiter(ratelimit, baid.RateBurst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (baid *Baidu) isCaptcha(page *rod.Page) bool {
|
||||||
|
_, err := page.Timeout(baid.GetSelectorTimeout()).Search("div.passMod_dialog-body")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (baid *Baidu) isTimeout(page *rod.Page) bool {
|
||||||
|
_, err := page.Timeout(baid.GetSelectorTimeout()).Search("button.timeout-button")
|
||||||
|
if 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) {
|
||||||
logrus.Tracef("Start Baidu search, query: %+v", query)
|
logrus.Tracef("Start Baidu search, query: %+v", query)
|
||||||
|
|
||||||
@@ -37,7 +61,23 @@ func (baid *Baidu) Search(query core.Query) ([]core.SearchResult, error) {
|
|||||||
|
|
||||||
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 {
|
||||||
return nil, err
|
defer page.Close()
|
||||||
|
logrus.Errorf("Cannot parse search results: %s", err)
|
||||||
|
return nil, core.ErrSearchTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check why no results, maybe captcha?
|
||||||
|
if results == nil {
|
||||||
|
defer page.Close()
|
||||||
|
|
||||||
|
if baid.isCaptcha(page) {
|
||||||
|
logrus.Errorf("Baidu captcha occurred during: %s", url)
|
||||||
|
return nil, core.ErrCaptcha
|
||||||
|
} else if baid.isTimeout(page) {
|
||||||
|
logrus.Errorf("Baidu timeout occurred during: %s", url)
|
||||||
|
return nil, core.ErrCaptcha
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resultElements, err := results.All()
|
resultElements, err := results.All()
|
||||||
|
|||||||
126
cmd/root.go
126
cmd/root.go
@@ -1,7 +1,9 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/karust/openserp/core"
|
"github.com/karust/openserp/core"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@@ -11,42 +13,55 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "0.1.1"
|
version = "0.2.1"
|
||||||
defaultConfigFilename = "config"
|
defaultConfigFilename = "config"
|
||||||
envPrefix = "OPENSERP"
|
envPrefix = "OPENSERP"
|
||||||
replaceHyphenWithCamelCase = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppConfig struct {
|
type Config struct {
|
||||||
Host string
|
App AppConfig `mapstructure:"app"`
|
||||||
Port int
|
GoogleConfig core.SearchEngineOptions `mapstructure:"google"`
|
||||||
Timeout int
|
YandexConfig core.SearchEngineOptions `mapstructure:"yandex"`
|
||||||
ConfigPath string
|
BaiduConfig core.SearchEngineOptions `mapstructure:"baidu"`
|
||||||
IsBrowserHead bool `mapstructure:"head"`
|
|
||||||
IsLeaveHead bool `mapstructure:"leave_head"`
|
|
||||||
IsLeakless bool `mapstructure:"leakless"`
|
|
||||||
IsDebug bool `mapstructure:"debug"`
|
|
||||||
IsVerbose bool `mapstructure:"verbose"`
|
|
||||||
IsRawRequests bool `mapstructure:"raw_requests"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var appConf = AppConfig{}
|
type AppConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Timeout int `mapstructure:"timeout"`
|
||||||
|
ConfigPath string `mapstructure:"config_path"`
|
||||||
|
IsBrowserHead bool `mapstructure:"head"`
|
||||||
|
IsLeaveHead bool `mapstructure:"leave_head"`
|
||||||
|
IsLeakless bool `mapstructure:"leakless"`
|
||||||
|
IsDebug bool `mapstructure:"debug"`
|
||||||
|
IsVerbose bool `mapstructure:"verbose"`
|
||||||
|
IsRawRequests bool `mapstructure:"raw_requests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = Config{}
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
var RootCmd = &cobra.Command{
|
||||||
Use: "openserp",
|
Use: "openserp",
|
||||||
Short: "Open SERP",
|
Short: "Open SERP",
|
||||||
Long: `Search via Google, Yandex and Baidu`,
|
Long: `Get [Google, Yandex, Baidu] search engine results via API or CLI.`,
|
||||||
Version: version,
|
Version: version,
|
||||||
|
SilenceUsage: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
core.InitLogger(appConf.IsVerbose, appConf.IsDebug)
|
core.InitLogger(config.App.IsVerbose, config.App.IsDebug)
|
||||||
|
|
||||||
err := initializeConfig(cmd)
|
err := initializeConfig(cmd)
|
||||||
logrus.Debugf("Config: %+v", appConf)
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Final config: %+v", config)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
|
|
||||||
// Run: func(cmd *cobra.Command, args []string) {
|
// Run: func(cmd *cobra.Command, args []string) {
|
||||||
// // Working with OutOrStdout/OutOrStderr allows us to unit test our command easier
|
// // Working with OutOrStdout/OutOrStderr allows us to unit test our command easier
|
||||||
// //out := cmd.OutOrStdout()
|
// //out := cmd.OutOrStdout()
|
||||||
// logrus.Trace("Config:", appConf)
|
// logrus.Trace("Config:", config)
|
||||||
// },
|
// },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,14 +69,10 @@ var RootCmd = &cobra.Command{
|
|||||||
func bindFlags(cmd *cobra.Command, vpr *viper.Viper) {
|
func bindFlags(cmd *cobra.Command, vpr *viper.Viper) {
|
||||||
cmd.Flags().VisitAll(func(flg *pflag.Flag) {
|
cmd.Flags().VisitAll(func(flg *pflag.Flag) {
|
||||||
configName := "app." + flg.Name
|
configName := "app." + flg.Name
|
||||||
//if replaceHyphenWithCamelCase {
|
|
||||||
// configName = strings.ReplaceAll(f.Name, "-", "")
|
|
||||||
//}
|
|
||||||
|
|
||||||
// Apply viper config value to the flag if viper has a value
|
// Apply viper config value to the flag if viper has a value
|
||||||
if !flg.Changed && vpr.IsSet(configName) {
|
if flg.Changed {
|
||||||
val := vpr.Get(configName)
|
vpr.Set(configName, flg.Value)
|
||||||
cmd.Flags().Set(flg.Name, fmt.Sprintf("%v", val))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -74,34 +85,47 @@ func initializeConfig(cmd *cobra.Command) error {
|
|||||||
v.SetConfigName(defaultConfigFilename)
|
v.SetConfigName(defaultConfigFilename)
|
||||||
v.AddConfigPath(".")
|
v.AddConfigPath(".")
|
||||||
|
|
||||||
// Return an error if we cannot parse the config file.
|
// 1. Config. Return an error if we cannot parse the config file.
|
||||||
if err := v.ReadInConfig(); err != nil {
|
err := v.ReadInConfig()
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
if err != nil {
|
||||||
return err
|
err = errors.New(fmt.Sprintf("Cannot read config: %v", err))
|
||||||
|
logrus.Warn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Env. Bind environment variables to their equivalent keys with underscores
|
||||||
|
for _, key := range v.AllKeys() {
|
||||||
|
envKey := envPrefix + "_" + strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
|
||||||
|
err := v.BindEnv(key, envKey)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Unable to bind ENV valye: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v.SetEnvPrefix(envPrefix)
|
// 3. Cmd flags. Bind the current command's flags to viper
|
||||||
|
|
||||||
// Bind environment variables to their equivalent keys with underscores
|
|
||||||
//v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
|
||||||
v.AutomaticEnv()
|
|
||||||
|
|
||||||
// Bind the current command's flags to viper
|
|
||||||
bindFlags(cmd, v)
|
bindFlags(cmd, v)
|
||||||
|
|
||||||
|
// Dump Viper values to config struct
|
||||||
|
err = v.Unmarshal(&config)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Cannot unmarshall config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.App.IsDebug {
|
||||||
|
logrus.Debug("Viper config:")
|
||||||
|
v.Debug()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.PersistentFlags().IntVarP(&appConf.Port, "port", "p", 7070, "Port number to run server")
|
RootCmd.PersistentFlags().IntVarP(&config.App.Port, "port", "p", 7070, "Port number to run server")
|
||||||
RootCmd.PersistentFlags().StringVarP(&appConf.Host, "host", "a", "127.0.0.1", "Host address to run server")
|
RootCmd.PersistentFlags().StringVarP(&config.App.Host, "host", "a", "127.0.0.1", "Host address to run server")
|
||||||
RootCmd.PersistentFlags().IntVarP(&appConf.Timeout, "timeout", "t", 30, "Timeout to fail request")
|
RootCmd.PersistentFlags().IntVarP(&config.App.Timeout, "timeout", "t", 30, "Timeout to fail request")
|
||||||
RootCmd.PersistentFlags().StringVarP(&appConf.ConfigPath, "config", "c", "./config.yaml", "Configuration file path")
|
RootCmd.PersistentFlags().StringVarP(&config.App.ConfigPath, "config", "c", "", "Configuration file path")
|
||||||
RootCmd.PersistentFlags().BoolVarP(&appConf.IsVerbose, "verbose", "v", false, "Use verbose output")
|
RootCmd.PersistentFlags().BoolVarP(&config.App.IsVerbose, "verbose", "v", false, "Use verbose output")
|
||||||
RootCmd.PersistentFlags().BoolVarP(&appConf.IsDebug, "debug", "d", false, "Use debug output. Disable headless browser")
|
RootCmd.PersistentFlags().BoolVarP(&config.App.IsDebug, "debug", "d", false, "Use debug output. Disable headless browser")
|
||||||
RootCmd.PersistentFlags().BoolVarP(&appConf.IsBrowserHead, "head", "", false, "Enable browser UI")
|
RootCmd.PersistentFlags().BoolVarP(&config.App.IsBrowserHead, "head", "", false, "Enable browser UI")
|
||||||
RootCmd.PersistentFlags().BoolVarP(&appConf.IsLeakless, "leakless", "l", false, "Use leakless mode to insure browser instances are closed after search")
|
RootCmd.PersistentFlags().BoolVarP(&config.App.IsLeakless, "leakless", "l", false, "Use leakless mode to insure browser instances are closed after search")
|
||||||
RootCmd.PersistentFlags().BoolVarP(&appConf.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(&appConf.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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func search(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
results := []core.SearchResult{}
|
results := []core.SearchResult{}
|
||||||
|
|
||||||
if appConf.IsRawRequests {
|
if config.App.IsRawRequests {
|
||||||
results, err = searchRaw(engineType, query)
|
results, err = searchRaw(engineType, query)
|
||||||
} else {
|
} else {
|
||||||
results, err = searchBrowser(engineType, query)
|
results, err = searchBrowser(engineType, query)
|
||||||
@@ -54,13 +54,13 @@ func searchBrowser(engineType string, query core.Query) ([]core.SearchResult, er
|
|||||||
var engine core.SearchEngine
|
var engine core.SearchEngine
|
||||||
|
|
||||||
opts := core.BrowserOpts{
|
opts := core.BrowserOpts{
|
||||||
IsHeadless: !appConf.IsBrowserHead, // Disable headless if browser head mode is set
|
IsHeadless: !config.App.IsBrowserHead, // Disable headless if browser head mode is set
|
||||||
IsLeakless: appConf.IsLeakless,
|
IsLeakless: config.App.IsLeakless,
|
||||||
Timeout: time.Second * time.Duration(appConf.Timeout),
|
Timeout: time.Second * time.Duration(config.App.Timeout),
|
||||||
LeavePageOpen: appConf.IsLeaveHead,
|
LeavePageOpen: config.App.IsLeaveHead,
|
||||||
}
|
}
|
||||||
|
|
||||||
if appConf.IsDebug {
|
if config.App.IsDebug {
|
||||||
opts.IsHeadless = false
|
opts.IsHeadless = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,11 +71,11 @@ func searchBrowser(engineType string, query core.Query) ([]core.SearchResult, er
|
|||||||
|
|
||||||
switch strings.ToLower(engineType) {
|
switch strings.ToLower(engineType) {
|
||||||
case "yandex":
|
case "yandex":
|
||||||
engine = yandex.New(*browser)
|
engine = yandex.New(*browser, config.YandexConfig)
|
||||||
case "google":
|
case "google":
|
||||||
engine = google.New(*browser)
|
engine = google.New(*browser, config.GoogleConfig)
|
||||||
case "baidu":
|
case "baidu":
|
||||||
engine = baidu.New(*browser)
|
engine = baidu.New(*browser, config.BaiduConfig)
|
||||||
default:
|
default:
|
||||||
logrus.Infof("No `%s` search engine found", engineType)
|
logrus.Infof("No `%s` search engine found", engineType)
|
||||||
}
|
}
|
||||||
|
|||||||
18
cmd/serve.go
18
cmd/serve.go
@@ -21,13 +21,13 @@ var serveCMD = &cobra.Command{
|
|||||||
|
|
||||||
func serve(cmd *cobra.Command, args []string) {
|
func serve(cmd *cobra.Command, args []string) {
|
||||||
opts := core.BrowserOpts{
|
opts := core.BrowserOpts{
|
||||||
IsHeadless: !appConf.IsBrowserHead, // Disable headless if browser head mode is set
|
IsHeadless: !config.App.IsBrowserHead, // Disable headless if browser head mode is set
|
||||||
IsLeakless: appConf.IsLeakless,
|
IsLeakless: config.App.IsLeakless,
|
||||||
Timeout: time.Second * time.Duration(appConf.Timeout),
|
Timeout: time.Second * time.Duration(config.App.Timeout),
|
||||||
LeavePageOpen: appConf.IsLeaveHead,
|
LeavePageOpen: config.App.IsLeaveHead,
|
||||||
}
|
}
|
||||||
|
|
||||||
if appConf.IsDebug {
|
if config.App.IsDebug {
|
||||||
opts.IsHeadless = false
|
opts.IsHeadless = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,11 +36,11 @@ func serve(cmd *cobra.Command, args []string) {
|
|||||||
logrus.Error(err)
|
logrus.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
yand := yandex.New(*browser)
|
yand := yandex.New(*browser, config.YandexConfig)
|
||||||
gogl := google.New(*browser)
|
gogl := google.New(*browser, config.GoogleConfig)
|
||||||
baidu := baidu.New(*browser)
|
baidu := baidu.New(*browser, config.BaiduConfig)
|
||||||
|
|
||||||
serv := core.NewServer(appConf.Host, appConf.Port, gogl, yand, baidu)
|
serv := core.NewServer(config.App.Host, config.App.Port, gogl, yand, baidu)
|
||||||
serv.Listen()
|
serv.Listen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
config.yaml
14
config.yaml
@@ -5,4 +5,16 @@ app:
|
|||||||
verbose: true
|
verbose: true
|
||||||
timeout: 15
|
timeout: 15
|
||||||
head: false
|
head: false
|
||||||
leakless: true
|
leakless: false
|
||||||
|
|
||||||
|
google:
|
||||||
|
rate_requests: 4 # Number of requests per Minute
|
||||||
|
rate_burst: 2 # Number of non-ratelimited requests per Minute
|
||||||
|
|
||||||
|
yandex:
|
||||||
|
rate_requests: 4
|
||||||
|
rate_burst: 2
|
||||||
|
|
||||||
|
baidu:
|
||||||
|
rate_requests: 4
|
||||||
|
rate_burst: 2
|
||||||
|
|||||||
@@ -16,18 +16,19 @@ type BrowserOpts struct {
|
|||||||
IsLeakless bool // Force to kill browser
|
IsLeakless bool // Force to kill browser
|
||||||
Timeout time.Duration // Timeout
|
Timeout time.Duration // Timeout
|
||||||
LanguageCode string
|
LanguageCode string
|
||||||
WaitRequests bool // Wait requests to complete after navigation
|
WaitRequests bool // Wait requests to complete after navigation
|
||||||
LeavePageOpen bool // Leave pages and browser open
|
LeavePageOpen bool // Leave pages and browser open
|
||||||
|
WaitLoadTime time.Duration // Time to wait till page loads
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize browser parameters with default values if they are not set
|
// Initialize browser parameters with default values if they are not set
|
||||||
func (o *BrowserOpts) Init() {
|
func (o *BrowserOpts) Check() {
|
||||||
if o.Timeout == 0 {
|
if o.Timeout == 0 {
|
||||||
o.Timeout = time.Second * 30
|
o.Timeout = time.Second * 30
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.LanguageCode == "" {
|
if o.WaitLoadTime == 0 {
|
||||||
o.LanguageCode = "en"
|
o.WaitLoadTime = time.Second * 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ type Browser struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewBrowser(opts BrowserOpts) (*Browser, error) {
|
func NewBrowser(opts BrowserOpts) (*Browser, error) {
|
||||||
opts.Init()
|
opts.Check()
|
||||||
logrus.Debugf("Browser options: %+v", opts)
|
logrus.Debugf("Browser options: %+v", opts)
|
||||||
|
|
||||||
path, has := launcher.LookPath()
|
path, has := launcher.LookPath()
|
||||||
@@ -66,11 +67,11 @@ func (b *Browser) Navigate(URL string) *rod.Page {
|
|||||||
|
|
||||||
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 := stealth.MustPage(b.browser)
|
page := stealth.MustPage(b.browser)
|
||||||
wait := page.MustWaitRequestIdle()
|
wait := page.MustWaitRequestIdle()
|
||||||
page.Navigate(URL)
|
page.MustNavigate(URL)
|
||||||
|
|
||||||
// causes bugs in google
|
// causes bugs in google
|
||||||
if b.WaitRequests {
|
if b.WaitRequests {
|
||||||
@@ -83,7 +84,7 @@ func (b *Browser) Navigate(URL string) *rod.Page {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Wait till page loads
|
// Wait till page loads
|
||||||
time.Sleep(time.Second * 1)
|
time.Sleep(b.WaitLoadTime)
|
||||||
|
|
||||||
return page
|
return page
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,3 +27,24 @@ func TestCreateBrowser(t *testing.T) {
|
|||||||
// t.Fatalf("Error failed initializing leakless browser: %s", err)
|
// t.Fatalf("Error failed initializing leakless browser: %s", err)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// Manually observe test results for now
|
||||||
|
func TestBot(t *testing.T) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
opts := BrowserOpts{IsHeadless: false, IsLeakless: true, LeavePageOpen: true}
|
||||||
|
browser, err = NewBrowser(opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error failed initializing browser: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := browser.Navigate("https://bot.sannysoft.com")
|
||||||
|
page.MustScreenshotFullPage("./test/screenshot_bot.png")
|
||||||
|
|
||||||
|
page = browser.Navigate("https://www.whatismybrowser.com/")
|
||||||
|
page.MustScreenshotFullPage("./test/screenshot_browser.png")
|
||||||
|
|
||||||
|
page = browser.Navigate("https://abrahamjuliot.github.io/creepjs/")
|
||||||
|
page.MustScreenshotFullPage("./test/screenshot_creep.png")
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ package core
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrCaptcha = errors.New("Captcha detected")
|
||||||
|
var ErrSearchTimeout = errors.New("Timeout. Cannot find element on page")
|
||||||
|
|
||||||
type SearchResult struct {
|
type SearchResult struct {
|
||||||
Rank int `json:"rank"`
|
Rank int `json:"rank"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
@@ -49,3 +53,33 @@ func (q *Query) InitFromContext(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SearchEngineOptions struct {
|
||||||
|
RateRequests int `mapstructure:"rate_requests"`
|
||||||
|
RateTime int64 `mapstructure:"rate_seconds"`
|
||||||
|
RateBurst int `mapstructure:"rate_burst"`
|
||||||
|
SelectorTimeout int64 `mapstructure:"selector_timeout"` // CSS selector timeout in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SearchEngineOptions) Init() {
|
||||||
|
if o.RateRequests == 0 {
|
||||||
|
o.RateRequests = 6
|
||||||
|
}
|
||||||
|
if o.RateTime == 0 {
|
||||||
|
o.RateTime = 60
|
||||||
|
}
|
||||||
|
if o.RateBurst == 0 {
|
||||||
|
o.RateBurst = 1
|
||||||
|
}
|
||||||
|
if o.SelectorTimeout == 0 {
|
||||||
|
o.SelectorTimeout = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SearchEngineOptions) GetRatelimit() time.Duration {
|
||||||
|
return (time.Duration(o.RateTime) * time.Second) / time.Duration(o.RateRequests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *SearchEngineOptions) GetSelectorTimeout() time.Duration {
|
||||||
|
return time.Duration(o.SelectorTimeout) * time.Second
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SearchEngine interface {
|
type SearchEngine interface {
|
||||||
Search(Query) ([]SearchResult, error)
|
Search(Query) ([]SearchResult, error)
|
||||||
IsInitialized() bool
|
IsInitialized() bool
|
||||||
Name() string
|
Name() string
|
||||||
|
GetRateLimiter() *rate.Limiter
|
||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -28,19 +32,32 @@ func NewServer(host string, port int, searchEngines ...SearchEngine) *Server {
|
|||||||
|
|
||||||
for _, engine := range searchEngines {
|
for _, engine := range searchEngines {
|
||||||
locEngine := engine
|
locEngine := engine
|
||||||
|
limiter := engine.GetRateLimiter()
|
||||||
|
|
||||||
serv.app.Get(fmt.Sprintf("/%s/search", strings.ToLower(locEngine.Name())), func(c *fiber.Ctx) error {
|
serv.app.Get(fmt.Sprintf("/%s/search", strings.ToLower(locEngine.Name())), func(c *fiber.Ctx) error {
|
||||||
q := Query{}
|
q := Query{}
|
||||||
err := q.InitFromContext(c)
|
err := q.InitFromContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("Error while setting %s query: %s", locEngine.Name(), err)
|
logrus.Errorf("Error while setting %s query: %s", locEngine.Name(), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = limiter.Wait(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Ratelimiter error during %s query: %s", locEngine.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
res, err := locEngine.Search(q)
|
res, err := locEngine.Search(q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case ErrCaptcha:
|
||||||
|
err = errors.New(fmt.Sprintf("Captcha found, please stop sending requests for a while\n%s", err))
|
||||||
|
case ErrSearchTimeout:
|
||||||
|
err = errors.New(fmt.Sprintf("Error: %s\nProbably need to update CSS selector", err))
|
||||||
|
}
|
||||||
|
|
||||||
logrus.Errorf("Error during %s search: %s", locEngine.Name(), err)
|
logrus.Errorf("Error during %s search: %s", locEngine.Name(), err)
|
||||||
return err
|
return fiber.NewError(fiber.StatusServiceUnavailable, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(res)
|
return c.JSON(res)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -25,6 +27,9 @@ func (SeMock) IsInitialized() bool {
|
|||||||
func (s SeMock) Search(q Query) (res []SearchResult, err error) {
|
func (s SeMock) Search(q Query) (res []SearchResult, err error) {
|
||||||
return []SearchResult{{Title: s.EngineName}}, nil
|
return []SearchResult{{Title: s.EngineName}}, nil
|
||||||
}
|
}
|
||||||
|
func (s SeMock) GetRateLimiter() *rate.Limiter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateServer(t *testing.T) {
|
func TestCreateServer(t *testing.T) {
|
||||||
se1 := SeMock{"mock_engine_1"}
|
se1 := SeMock{"mock_engine_1"}
|
||||||
|
|||||||
@@ -3,7 +3,16 @@ version: '3'
|
|||||||
services:
|
services:
|
||||||
openserp:
|
openserp:
|
||||||
container_name: serp
|
container_name: serp
|
||||||
image: openserp
|
build:
|
||||||
|
context: .
|
||||||
ports:
|
ports:
|
||||||
- 7000:7000
|
- 7000:7000
|
||||||
command: serve -a 0.0.0.0 -p 7000
|
command: serve -l
|
||||||
|
#volumes:
|
||||||
|
# - ./config.yaml:/usr/src/app/config.yaml
|
||||||
|
environment:
|
||||||
|
OPENSERP_APP_HOST: "0.0.0.0"
|
||||||
|
OPENSERP_APP_PORT: 7000
|
||||||
|
OPENSERP_BAIDU_RATE_REQUESTS: 6 # Number of requests per Minute
|
||||||
|
OPENSERP_BAIDU_RATE_BURST: 2 # Number of non-ratelimited requests per Minute
|
||||||
|
|
||||||
1
go.mod
1
go.mod
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.16.0
|
github.com/spf13/viper v1.16.0
|
||||||
|
golang.org/x/time v0.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -421,6 +421,8 @@ golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
|||||||
@@ -5,34 +5,43 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-rod/rod"
|
"github.com/go-rod/rod"
|
||||||
"github.com/karust/openserp/core"
|
"github.com/karust/openserp/core"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Google struct {
|
type Google struct {
|
||||||
core.Browser
|
core.Browser
|
||||||
findNumRgxp *regexp.Regexp
|
core.SearchEngineOptions
|
||||||
checkTimeout time.Duration
|
findNumRgxp *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(browser core.Browser) *Google {
|
func New(browser core.Browser, opts core.SearchEngineOptions) *Google {
|
||||||
gogl := Google{Browser: browser}
|
gogl := Google{Browser: browser}
|
||||||
gogl.checkTimeout = time.Second * 5
|
opts.Init()
|
||||||
|
gogl.SearchEngineOptions = opts
|
||||||
|
|
||||||
gogl.findNumRgxp = regexp.MustCompile("\\d")
|
gogl.findNumRgxp = regexp.MustCompile("\\d")
|
||||||
return &gogl
|
return &gogl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gogl *Google) Name() string {
|
func (gogl *Google) Name() string {
|
||||||
return "google"
|
return "google"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gogl *Google) FindTotalResults(page *rod.Page) (int, error) {
|
func (gogl *Google) GetRateLimiter() *rate.Limiter {
|
||||||
resultsStats, err := page.Timeout(gogl.checkTimeout).Search("div#result-stats")
|
ratelimit := rate.Every(gogl.GetRatelimit())
|
||||||
|
return rate.NewLimiter(ratelimit, gogl.RateBurst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gogl *Google) findTotalResults(page *rod.Page) (int, error) {
|
||||||
|
resultsStats, err := page.Timeout(gogl.GetSelectorTimeout()).Search("div#result-stats")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.New("Result stats not found: " + err.Error())
|
return 0, errors.New("Result stats not found: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := resultsStats.First.Text()
|
stats, err := resultsStats.First.Text()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.New("Cannot extract result stats text: " + err.Error())
|
return 0, errors.New("Cannot extract result stats text: " + err.Error())
|
||||||
@@ -49,6 +58,14 @@ func (gogl *Google) FindTotalResults(page *rod.Page) (int, error) {
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gogl *Google) isCaptcha(page *rod.Page) bool {
|
||||||
|
_, err := page.Timeout(gogl.GetSelectorTimeout()).Search("form#captcha-form")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (gogl *Google) preparePage(page *rod.Page) {
|
func (gogl *Google) preparePage(page *rod.Page) {
|
||||||
// Remove "similar queries" lists
|
// Remove "similar queries" lists
|
||||||
page.Eval(";(() => { document.querySelectorAll(`div[data-initq]`).forEach( el => el.remove()); })();")
|
page.Eval(";(() => { document.querySelectorAll(`div[data-initq]`).forEach( el => el.remove()); })();")
|
||||||
@@ -68,21 +85,30 @@ func (gogl *Google) Search(query core.Query) ([]core.SearchResult, error) {
|
|||||||
page := gogl.Navigate(url)
|
page := gogl.Navigate(url)
|
||||||
gogl.preparePage(page)
|
gogl.preparePage(page)
|
||||||
|
|
||||||
totalResults, err := gogl.FindTotalResults(page)
|
results, err := page.Timeout(gogl.Timeout).Search("div[data-hveid][data-ved][lang], div[data-surl][jsaction]")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
defer page.Close()
|
||||||
}
|
logrus.Errorf("Cannot parse search results: %s", err)
|
||||||
logrus.Tracef("%d total results found", totalResults)
|
return nil, core.ErrSearchTimeout
|
||||||
|
|
||||||
if totalResults == 0 {
|
|
||||||
return searchResults, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := page.Timeout(gogl.Timeout).Search("div[data-hveid][data-ved][lang]")
|
// Check why no results, maybe captcha?
|
||||||
if err != nil {
|
if results == nil {
|
||||||
|
defer page.Close()
|
||||||
|
|
||||||
|
if gogl.isCaptcha(page) {
|
||||||
|
logrus.Errorf("Google captcha occurred during: %s", url)
|
||||||
|
return nil, core.ErrCaptcha
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalResults, err := gogl.findTotalResults(page)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error capturing total results: %v", err)
|
||||||
|
}
|
||||||
|
logrus.Infof("%d total results found", totalResults)
|
||||||
|
|
||||||
resultElements, err := results.All()
|
resultElements, err := results.All()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchGoogle(t *testing.T) {
|
func TestSearchGoogle(t *testing.T) {
|
||||||
gogl := New(*browser)
|
gogl := New(*browser, core.SearchEngineOptions{})
|
||||||
|
|
||||||
query := core.Query{Text: "HEY", Limit: 10}
|
query := core.Query{Text: "HEY", Limit: 10}
|
||||||
results, err := gogl.Search(query)
|
results, err := gogl.Search(query)
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import (
|
|||||||
"github.com/go-rod/rod"
|
"github.com/go-rod/rod"
|
||||||
"github.com/karust/openserp/core"
|
"github.com/karust/openserp/core"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Yandex struct {
|
type Yandex struct {
|
||||||
core.Browser
|
core.Browser
|
||||||
checkTimeout time.Duration // Timeout for secondary elements check
|
core.SearchEngineOptions
|
||||||
pageSleep time.Duration // Sleep between pages
|
pageSleep time.Duration // Sleep between pages
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(browser core.Browser) *Yandex {
|
func New(browser core.Browser, opts core.SearchEngineOptions) *Yandex {
|
||||||
yand := Yandex{Browser: browser}
|
yand := Yandex{Browser: browser}
|
||||||
yand.checkTimeout = time.Second * 2
|
opts.Init()
|
||||||
|
yand.SearchEngineOptions = opts
|
||||||
|
|
||||||
yand.pageSleep = time.Second * 1
|
yand.pageSleep = time.Second * 1
|
||||||
return &yand
|
return &yand
|
||||||
}
|
}
|
||||||
@@ -25,8 +28,13 @@ func (yand *Yandex) Name() string {
|
|||||||
return "yandex"
|
return "yandex"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (yand *Yandex) GetRateLimiter() *rate.Limiter {
|
||||||
|
ratelimit := rate.Every(yand.GetRatelimit())
|
||||||
|
return rate.NewLimiter(ratelimit, yand.RateBurst)
|
||||||
|
}
|
||||||
|
|
||||||
func (yand *Yandex) isCaptcha(page *rod.Page) bool {
|
func (yand *Yandex) isCaptcha(page *rod.Page) bool {
|
||||||
_, err := page.Timeout(yand.checkTimeout).Search("form#checkbox-captcha-form")
|
_, err := page.Timeout(yand.GetSelectorTimeout()).Search("form#checkbox-captcha-form")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -37,12 +45,12 @@ func (yand *Yandex) isCaptcha(page *rod.Page) bool {
|
|||||||
func (yand *Yandex) isNoResults(page *rod.Page) bool {
|
func (yand *Yandex) isNoResults(page *rod.Page) bool {
|
||||||
noResFound := false
|
noResFound := false
|
||||||
|
|
||||||
_, err := page.Timeout(yand.checkTimeout).Search("div.EmptySearchResults-Title")
|
_, err := page.Timeout(yand.GetSelectorTimeout()).Search("div.EmptySearchResults-Title")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
noResFound = true
|
noResFound = true
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = page.Timeout(yand.checkTimeout).Search("div>div.RequestMeta-Message")
|
_, err = page.Timeout(yand.GetSelectorTimeout()).Search("div>div.RequestMeta-Message")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
noResFound = true
|
noResFound = true
|
||||||
}
|
}
|
||||||
@@ -110,15 +118,20 @@ func (yand *Yandex) Search(query core.Query) ([]core.SearchResult, error) {
|
|||||||
// 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")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
defer page.Close()
|
||||||
logrus.Errorf("Cannot parse search results: %s", err)
|
logrus.Errorf("Cannot parse search results: %s", err)
|
||||||
|
return nil, core.ErrSearchTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check why no results, maybe captcha?
|
// Check why no results, maybe captcha?
|
||||||
if searchRes == nil {
|
if searchRes == nil {
|
||||||
|
defer page.Close()
|
||||||
|
|
||||||
if yand.isNoResults(page) {
|
if yand.isNoResults(page) {
|
||||||
logrus.Errorf("No results found")
|
logrus.Errorf("No results found")
|
||||||
} else if yand.isCaptcha(page) {
|
} else if yand.isCaptcha(page) {
|
||||||
logrus.Errorf("Yandex captcha occurred during: %s", url)
|
logrus.Errorf("Yandex captcha occurred during: %s", url)
|
||||||
|
return nil, core.ErrCaptcha
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func init() {
|
|||||||
|
|
||||||
func TestSearchYandex(t *testing.T) {
|
func TestSearchYandex(t *testing.T) {
|
||||||
|
|
||||||
yand := New(*browser)
|
yand := New(*browser, core.SearchEngineOptions{})
|
||||||
|
|
||||||
query := core.Query{Text: "HEY", Limit: 10}
|
query := core.Query{Text: "HEY", Limit: 10}
|
||||||
results, err := yand.Search(query)
|
results, err := yand.Search(query)
|
||||||
|
|||||||
Reference in New Issue
Block a user