Damnit again

Former-commit-id: b394f26caf0df7d113ac4cc7dacc9c544af6897f
This commit is contained in:
bel
2019-06-21 18:12:31 -06:00
commit 90a31495c9
32 changed files with 3464 additions and 0 deletions

132
copart/auction/.auction.go Normal file
View File

@@ -0,0 +1,132 @@
package auction
import (
"encoding/json"
"local/sandbox/selenium/copart/copart/browser"
"local/sandbox/selenium/copart/copart/config"
"log"
"time"
"github.com/tebeka/selenium"
)
type Auction struct {
browser *browser.Browser
cars []*Car
stop bool
stopped bool
}
func New(url string) (*Auction, error) {
b, err := browser.New()
if err != nil {
b.Close()
return nil, err
}
if err := b.Get(url); err != nil {
b.Close()
return nil, err
}
var elems []selenium.WebElement
deadline := time.Now().Add(time.Second * 10) // TODO ctx configurable and load more later
for deadline.After(time.Now()) {
time.Sleep(time.Second)
elems, err = b.Driver.FindElements("xpath", "//iframe")
if err != nil {
b.Close()
return nil, err
}
if len(elems) > 0 {
break
}
}
if time.Now().After(deadline) {
b.Close()
return nil, b.Close()
}
url, err = elems[0].GetAttribute("src")
if err != nil {
b.Close()
return nil, err
}
if err := b.Get(url); err != nil {
b.Close()
return nil, err
}
return &Auction{
browser: b,
cars: []*Car{},
}, nil
}
func (a *Auction) Start() error {
log.Println("START", a.stop)
go func() {
defer func() {
a.stopped = true
}()
for !a.stop {
log.Println("TOP")
c := NewCar()
if err := c.Parse(a.browser.Driver); err != nil {
log.Println(err)
return
}
log.Println("C", c)
for !a.stop {
time.Sleep(time.Second)
d := NewCar()
if err := d.Parse(a.browser.Driver); err != nil {
log.Println(err)
return
}
log.Printf("%v = (%v == %v)", c.Equals(d), c.String(), d.String())
if !c.Equals(d) {
break
}
c = d
}
b, err := c.Encode()
if err != nil {
log.Println(err)
return
}
if err := config.Values().DB.Set(c.String(), b); err != nil {
log.Println(err)
return
}
var list []string
if b, err := config.Values().DB.Get("LIST"); err != nil {
log.Println(err)
return
} else if err := json.Unmarshal(b, &list); err != nil {
log.Println(err)
return
} else {
list = append(list, c.String())
b, _ := json.Marshal(list)
if err := config.Values().DB.Set("LIST", b); err != nil {
log.Println(err)
return
}
}
}
}()
return nil
}
func (a *Auction) Stop() error {
if a == nil {
return nil
}
a.stop = true
for !a.stopped {
time.Sleep(time.Second)
}
if a.browser == nil || a.browser.Driver == nil {
return nil
}
return a.browser.Driver.Close()
}

222
copart/auction/auction.go Normal file
View File

@@ -0,0 +1,222 @@
package auction
import (
"context"
"encoding/json"
"errors"
"local/sandbox/selenium/copart/copart/browser"
"local/sandbox/selenium/copart/copart/config"
"local/storage"
"log"
"strconv"
"strings"
"time"
)
type Auction struct {
browser *browser.Browser
cars chan *Car
ctx context.Context
can context.CancelFunc
routines chan struct{}
}
func New(b *browser.Browser, url string) (*Auction, error) {
if err := b.Get(url); err != nil {
b.Close()
return nil, err
}
ctx, can := context.WithTimeout(context.Background(), time.Second*10)
defer can()
var elem WebElement
for err := ctx.Err(); err == nil; err = ctx.Err() {
elem, err = b.Driver.FindElement("xpath", "//iframe")
if err == nil {
break
}
time.Sleep(time.Second)
}
if err := ctx.Err(); err != nil {
b.Close()
return nil, err
}
iframeURL, err := elem.GetAttribute("src")
if err != nil {
b.Close()
return nil, err
} else if iframeURL == "" {
b.Close()
return nil, errors.New("auction iframe has no src")
}
if err := b.Get(iframeURL); err != nil {
b.Close()
return nil, err
}
ctx, can = context.WithCancel(context.Background())
return &Auction{
browser: b,
cars: make(chan *Car),
ctx: ctx,
can: can,
routines: make(chan struct{}, 5),
}, nil
}
func (a *Auction) Start() error {
go a.parseCars()
a.routines <- struct{}{}
go a.saveCars()
a.routines <- struct{}{}
return nil
}
func (a *Auction) parseCars() error {
log.Printf("[parseCars] starting")
defer func() {
if err := recover(); err != nil {
log.Println("[parseCars] recover:", err)
}
log.Printf("[parseCars] stopping")
close(a.cars)
a.can()
a.routines <- struct{}{}
}()
for err := a.ctx.Err(); err == nil; err = a.ctx.Err() {
c, err := a.nextCar()
if err != nil {
panic(err)
}
log.Printf("[parseCars] found car %v", c.String())
if !strings.Contains(c.Bid, "$") {
continue
}
if b, _ := c.MarshalJSON(); strings.Contains(strings.ToLower(string(b)), ", hi") {
continue
} else if strings.Contains(strings.ToLower(string(b)), "guam") {
continue
} else if len(c.Title) < 4 {
continue
} else if v, err := strconv.Atoi(c.Title[:4]); err != nil {
continue
} else if v+5 < time.Now().Year() {
continue
}
select {
case a.cars <- c:
case <-a.ctx.Done():
panic(a.ctx.Err())
}
}
return a.ctx.Err()
}
func (a *Auction) nextCar() (*Car, error) {
c := NewCar()
if err := c.Parse(a.browser.Driver); err != nil {
if !strings.Contains(err.Error(), "stale element") {
return nil, err
} else {
return a.nextCar()
}
}
for err := a.ctx.Err(); err == nil; err = a.ctx.Err() {
time.Sleep(time.Second * 2)
d := NewCar()
if err := d.Parse(a.browser.Driver); err != nil {
if !strings.Contains(err.Error(), "stale element") {
return nil, err
}
}
if !c.Equals(d) {
return c, nil
}
if d.Bid != "" {
c = d
}
}
return nil, a.ctx.Err()
}
func (a *Auction) saveCars() error {
log.Printf("[saveCars] starting")
defer func() {
if err := recover(); err != nil {
log.Println("[saveCars] recover:", err)
}
log.Printf("[saveCars] stopping")
a.can()
a.routines <- struct{}{}
}()
for err := a.ctx.Err(); err == nil; err = a.ctx.Err() {
select {
case <-a.ctx.Done():
panic(a.ctx.Err())
case c := <-a.cars:
go func(d *Car) {
if d == nil {
return
}
log.Printf("[saveCars] saving %v", d.String())
if err := a.saveCar(d); err != nil {
log.Printf("[saveCars] err: %v", err)
}
}(c)
}
}
return a.ctx.Err()
}
func (a *Auction) saveCar(c *Car) error {
db := config.Values().DB
b, err := c.Encode()
if err != nil {
return err
}
if err := db.Set(c.String(), b); err != nil {
return err
}
var list []string
b, err = db.Get("LIST")
if err == storage.ErrNotFound {
b = []byte("[]")
} else if err != nil {
return err
}
if err := json.Unmarshal(b, &list); err != nil {
return err
}
list = append([]string{c.String()}, list...)
if b, err := json.Marshal(list); err != nil {
return err
} else if err := db.Set("LIST", b); err != nil {
return err
}
return nil
}
func (a *Auction) Stop() error {
l := len(a.routines)
a.can()
ctx, can := context.WithTimeout(context.Background(), time.Second*5)
defer can()
for i := 0; i < 2*l; i++ {
select {
case <-a.routines:
case <-ctx.Done():
}
}
if len(a.routines) > 0 {
return errors.New("not all routines exited")
}
return nil
}

View File

@@ -0,0 +1,185 @@
package auction
import (
"context"
"crypto/rand"
"fmt"
"local/sandbox/selenium/copart/copart/browser"
"local/sandbox/selenium/copart/copart/config"
"math/big"
"net/url"
"os"
"strings"
"sync"
"testing"
"time"
)
func randStr() string {
n := 3
letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
k := big.NewInt(int64(len(letters)))
b := make([]byte, n)
for i := 0; i < n; i++ {
j, _ := rand.Int(rand.Reader, k)
b[i] = letters[int(j.Int64())]
}
return string(b)
}
func randomAuctionBody() []browser.MockElement {
return []browser.MockElement{browser.MockElement{
Attrs: map[string]string{
"src": "anything",
},
Ret: map[string][]browser.MockElement{
"//bidding-dialer-refactor": []browser.MockElement{browser.MockElement{
Ret: map[string][]browser.MockElement{
"text": []browser.MockElement{browser.MockElement{
Txt: "$bid" + randStr(),
}},
},
}},
"//lot-details-primary-refactored": []browser.MockElement{browser.MockElement{
Ret: map[string][]browser.MockElement{
"label": []browser.MockElement{browser.MockElement{
Txt: "label" + randStr(),
Ret: map[string][]browser.MockElement{
"../div[1]": []browser.MockElement{browser.MockElement{
Txt: "value" + randStr(),
}},
},
}},
},
}},
"//lot-details-secondary-refactored": []browser.MockElement{browser.MockElement{
Ret: map[string][]browser.MockElement{
"label": []browser.MockElement{browser.MockElement{
Txt: "label" + randStr(),
Ret: map[string][]browser.MockElement{
"../div[1]": []browser.MockElement{browser.MockElement{
Txt: "value" + randStr(),
}},
},
}},
},
}},
"//ngx-carousel": []browser.MockElement{browser.MockElement{
Ret: map[string][]browser.MockElement{
"ngx-item": []browser.MockElement{},
},
}},
"//lot-header": []browser.MockElement{browser.MockElement{
Ret: map[string][]browser.MockElement{
"section": []browser.MockElement{browser.MockElement{
Ret: map[string][]browser.MockElement{
"div": []browser.MockElement{browser.MockElement{
Ret: map[string][]browser.MockElement{
"div": []browser.MockElement{browser.MockElement{
Txt: "title" + randStr(),
}},
},
}},
},
}},
},
}},
"//widget-header-sale": []browser.MockElement{browser.MockElement{
Ret: map[string][]browser.MockElement{
".//div[@class=\"watchlist\"]": []browser.MockElement{},
},
}},
},
}}
}
func mockAuction() (*Auction, context.CancelFunc) {
m := &sync.Map{}
r := randomAuctionBody()
m.Store("body", r)
go func() {
time.Sleep(time.Second * 2)
m.Store("//iframe", r)
}()
driver := browser.NewMockDriver(m)
ctx, can := context.WithCancel(context.Background())
go func() {
for ctx.Err() == nil {
time.Sleep(time.Millisecond * 100)
m.Store("body", randomAuctionBody())
}
}()
return &Auction{
browser: &browser.Browser{Driver: driver},
cars: make(chan *Car),
ctx: ctx,
can: can,
routines: make(chan struct{}, 5),
}, can
}
func TestNewAuction(t *testing.T) {
a, can := mockAuction()
defer can()
b := a.browser
if _, err := New(b, "any"); err != nil {
t.Fatal(err)
}
}
func TestAuctionStartStop(t *testing.T) {
a, can := mockAuction()
defer can()
if err := a.Start(); err != nil {
t.Fatal(err)
}
if len(a.routines) != 2 {
t.Fatal("not 2 routines")
}
if err := a.Stop(); err != nil {
t.Fatal(err)
}
}
func TestAuctionSaveCar(t *testing.T) {
os.Args = []string{"a", "-db", "map"}
config.New()
a, can := mockAuction()
defer can()
c := NewCar()
c.Bid = "123"
c.Title = "abc"
c.Details = map[string]string{
"x": "y",
}
c.Images = []*Image{&Image{Src: &url.URL{Host: "img"}}}
d := NewCar()
if err := a.saveCar(c); err != nil {
t.Fatal(err)
}
if b, err := config.Values().DB.Get("LIST"); err != nil {
t.Fatal(err)
} else if !strings.Contains(string(b), c.String()) {
t.Fatal("list does not contain new car")
}
if b, err := config.Values().DB.Get(c.String()); err != nil {
t.Fatal(err)
} else if err := d.Decode(b); err != nil {
t.Fatal(err)
} else if !c.Equals(d) {
t.Fatal("saved car != fetched car")
}
}
func TestAuctionNextCar(t *testing.T) {
os.Args = []string{"a", "-db", "map"}
config.New()
a, can := mockAuction()
defer can()
if c, err := a.nextCar(); err != nil {
t.Fatal(err)
} else if c == nil || fmt.Sprintf("%v", *c) == fmt.Sprintf("%v", Car{}) {
t.Fatal("parsed car is nil")
}
}

251
copart/auction/car.go Normal file
View File

@@ -0,0 +1,251 @@
package auction
import (
"bytes"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
)
type Car struct {
Bid string
Details map[string]string
Images []*Image
Title string
Watched bool
TS time.Time
}
type CarOption func(*Car)
func NewCar(options ...CarOption) *Car {
c := &Car{
Details: map[string]string{},
Images: []*Image{},
}
c.parseTS(nil)
for _, opt := range options {
opt(c)
}
return c
}
func (c *Car) MarshalJSON() ([]byte, error) {
m := map[string]interface{}{}
m["bid"] = c.Bid
m["details"] = c.Details
m["images"] = c.Images
m["title"] = c.Title
m["TS"] = c.TS.Format("2006-01-02")
return json.Marshal(m)
}
func (c *Car) Encode() ([]byte, error) {
buff := bytes.NewBuffer(nil)
enc := gob.NewEncoder(buff)
err := enc.Encode(c)
return buff.Bytes(), err
}
func (c *Car) Decode(b []byte) error {
buff := bytes.NewBuffer(b)
enc := gob.NewDecoder(buff)
return enc.Decode(c)
}
func (c *Car) Copy() *Car {
d := *c
return &d
}
func (c *Car) Equals(d *Car) bool {
if c == d {
return true
}
if d == nil && c != nil {
return false
}
if c == nil && d != nil {
return false
}
for k, v := range c.Details {
if w, ok := d.Details[k]; ok && w != v {
return false
}
}
for k, v := range c.Details {
if w, ok := c.Details[k]; ok && w != v {
return false
}
}
if c.Title != d.Title {
return false
}
return true
}
func (c *Car) String() string {
return fmt.Sprintf(
"[bid:%v title:%v watched:%v images:%v when:%v]",
c.Bid,
c.Title,
c.Watched,
len(c.Images),
c.TS,
)
}
func (c *Car) Parse(wd WebDriver) error {
we, err := wd.FindElement("tag name", "body")
if err != nil {
return err
}
do := []func(WebElement) error{
c.parseTitle,
c.parseBid,
c.parseWatched,
c.parseImages,
c.parseDetails,
c.parseTS,
}
for _, d := range do {
if err := d(we); err != nil {
return err
}
}
return nil
}
func (c *Car) parseTS(WebElement) error {
c.TS = time.Now()
return nil
}
func (c *Car) parseBid(wd WebElement) error {
elem := waitForXPath(wd, "//bidding-dialer-refactor")
elem, err := elem.FindElement("tag name", "text")
if err != nil {
return err
}
text, err := elem.Text()
if err != nil {
return err
}
if strings.Contains(text, "$") {
c.Bid = text
}
return nil
}
func (c *Car) parseDetails(wd WebElement) error {
details := make(map[string]string)
elems, err := wd.FindElements("tag name", "perfect-scrollbar")
if err != nil {
return err
}
if len(elems) > 2 {
elems = elems[:2]
}
for _, elem := range elems {
labels, err := elem.FindElements("tag name", "label")
if err != nil {
return err
}
for _, label := range labels {
ltext, err := label.Text()
if err != nil {
return err
}
if ltext == "" {
continue
}
value, err := label.FindElement("xpath", "../div[1]")
if err != nil {
return err
}
vtext, err := value.Text()
if err != nil {
return err
}
details[ltext] = vtext
}
}
if len(details) > 0 {
c.Details = details
}
return nil
}
func (c *Car) parseImages(wd WebElement) error {
elem := waitForXPath(wd, "//ngx-carousel")
elems, err := elem.FindElements("tag name", "ngx-item")
if err != nil {
return err
}
if len(elems) == 0 {
return err
}
images := make([]*Image, 0)
for _, elem := range elems {
elems, err := elem.FindElements("tag name", "img")
if err != nil {
return err
}
for _, img := range elems {
attr, err := img.GetAttribute("src")
if err != nil {
return err
}
url, err := url.Parse(attr)
if err != nil {
return err
}
images = append(images, NewImage(url))
}
}
c.Images = images
return nil
}
func (c *Car) parseTitle(wd WebElement) error {
elem := waitForXPath(wd, "//lot-header")
elem = followTags(elem, "section", "div", "div")
if elem == nil {
return errors.New("not found")
}
text, err := elem.Text()
if err != nil {
return err
}
c.Title = text
return nil
}
func (c *Car) parseWatched(wd WebElement) error {
elem := waitForXPath(wd, "//widget-header-sale")
elems, err := elem.FindElements("tag name", ".//div[@class=\"watchlist\"]")
if err != nil {
return err
}
c.Watched = len(elems) > 0
return nil
}

View File

@@ -0,0 +1,36 @@
package auction
import "testing"
func TestCar(t *testing.T) {
c := NewCar()
c.Title = "title"
c.Bid = "bid"
c.Watched = true
c.Details = map[string]string{"a": "b"}
d := c.Copy()
if !c.Equals(d) {
t.Errorf("equals failed post copy")
}
c.Details = map[string]string{"a": "c"}
if c.Equals(d) {
t.Errorf("equals passes post edit")
}
t.Log(c.String())
b, err := c.Encode()
if err != nil {
t.Fatal(err)
}
e := NewCar()
if err := e.Decode(b); err != nil {
t.Fatal(err)
}
if !c.Equals(e) {
t.Fatalf("decoded != encoded: %v vs %v", c, e)
}
if _, err := c.MarshalJSON(); err != nil {
t.Fatal(err)
}
}

39
copart/auction/image.go Normal file
View File

@@ -0,0 +1,39 @@
package auction
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"os"
)
type Image struct {
Raw []byte
Src *url.URL
}
func NewImage(url *url.URL) *Image {
return &Image{
Src: url,
}
}
func (img *Image) MarshalJSON() ([]byte, error) {
return json.Marshal(img.Src.String())
}
func (img *Image) Fetch() error {
resp, err := http.Get(img.Src.String())
if err != nil {
return err
}
defer resp.Body.Close()
Raw, err := ioutil.ReadAll(resp.Body)
img.Raw = Raw
return err
}
func (img *Image) Save(path string) error {
return ioutil.WriteFile(path, img.Raw, os.ModePerm)
}

231
copart/auction/today.go Normal file
View File

@@ -0,0 +1,231 @@
package auction
import (
"context"
"local/sandbox/selenium/copart/copart/browser"
"local/sandbox/selenium/copart/copart/config"
"local/storage"
"log"
"os"
"strings"
"syscall"
"time"
"golang.org/x/time/rate"
)
type Today struct {
today *browser.Browser
Auctions []*Auction
ctx context.Context
can context.CancelFunc
routines chan struct{}
sigc chan os.Signal
}
func NewToday(sigc chan os.Signal) (*Today, error) {
today, err := today()
if err != nil {
today.Close()
return nil, err
}
if _, err := config.Values().DB.Get("LIST"); err == storage.ErrNotFound {
if err := config.Values().DB.Set("LIST", []byte("[]")); err != nil {
today.Close()
return nil, err
}
} else if err != nil {
today.Close()
return nil, err
}
deadline := time.Now()
deadline = deadline.Add(time.Hour * 24)
deadline = deadline.Add(time.Duration(-1*time.Now().Hour()) * time.Hour)
ctx, can := context.WithDeadline(context.Background(), deadline)
return &Today{
today: today,
Auctions: make([]*Auction, 0),
ctx: ctx,
can: can,
routines: make(chan struct{}, 10),
sigc: sigc,
}, nil
}
func today() (*browser.Browser, error) {
today, err := browser.New()
if err != nil {
return today, err
}
if err := today.Get("https://www.copart.com/todaysAuction/"); err != nil {
return today, err
}
return today, nil
}
func (t *Today) Start() error {
ch := make(chan string)
go t.watchAuctionList(ch)
t.routines <- struct{}{}
go t.startAuctions(ch)
t.routines <- struct{}{}
return t.ctx.Err()
}
func (t *Today) watchAuctionList(ch chan string) {
log.Printf("[watchAuctionList] starting")
defer func() {
if err := recover(); err != nil {
log.Println("[watchAuctionList] recover:", err)
}
log.Printf("[watchAuctionList] stopping")
t.routines <- struct{}{}
t.Stop()
}()
found := make(map[string]struct{})
retryCD := 3
for err := t.ctx.Err(); err == nil; err = t.ctx.Err() {
elems, err := t.today.Driver.FindElements("link text", "Join Auction")
if err != nil {
panic(err)
}
log.Printf("[watchAuctionList] found %v", len(elems))
for _, elem := range elems {
url, err := elem.GetAttribute("href")
if err != nil {
panic(err)
}
key := time.Now().Format("2006-01-02") + url
if _, ok := found[key]; ok {
log.Printf("[watchAuctionList] skipping %v", url)
continue
}
found[key] = struct{}{}
log.Printf("[watchAuctionList] sending %v", len(url))
ch <- url
}
log.Printf("[watchAuctionList] blocking 5 minutes")
select {
case <-t.ctx.Done():
panic(t.ctx.Err())
case <-time.After(time.Minute * 5):
retryCD -= 1
if retryCD == 0 {
retryCD = 3
for {
if t.ctx.Err() != nil {
panic(t.ctx.Err())
}
err := t.today.Driver.Refresh()
if err == nil {
break
}
if strings.Contains(err.Error(), "timeout") {
select {
case <-time.After(time.Second * 15):
case <-t.ctx.Done():
panic(t.ctx.Err())
}
}
}
}
}
}
}
func (t *Today) startAuctions(ch chan string) {
log.Printf("[startAuctions] starting")
defer func() {
if err := recover(); err != nil {
log.Println("[startAuctions] recover:", err)
}
log.Printf("[startAuctions] stopping")
t.routines <- struct{}{}
t.Stop()
}()
limiter := rate.NewLimiter(rate.Every(time.Second*15), 1)
for {
select {
case <-t.ctx.Done():
panic(t.ctx.Err())
case url := <-ch:
if err := limiter.Wait(t.ctx); err != nil {
panic(err)
}
log.Printf("[startAuctions] spawning %v", url)
b, err := browser.New()
if err != nil {
time.Sleep(time.Second * 15)
if err := limiter.Wait(t.ctx); err != nil {
panic(err)
}
b, err = browser.New()
if err != nil {
panic(err)
}
}
a, err := New(b, url)
if a != nil {
t.Auctions = append(t.Auctions, a)
}
if err != nil {
log.Println("[startAuctions] failed to New:", err)
} else if err := a.Start(); err != nil {
log.Println("[startAuctions] failed to New().Start:", err)
} else {
log.Printf("[startAuctions] spawned %v", url)
}
}
}
}
func (t *Today) Stop() error {
last := make(chan error)
if t == nil {
return nil
}
l := len(t.routines)
t.can()
ctx, can := context.WithTimeout(context.Background(), time.Second*10)
defer can()
for i := 0; i < l*2; i++ {
select {
case <-t.routines:
case <-ctx.Done():
select {
case last <- ctx.Err():
default:
}
}
}
for _, a := range t.Auctions {
go func() {
if err := a.Stop(); err != nil {
select {
case last <- err:
case <-time.After(time.Second * 5):
}
}
}()
}
if err := t.today.Driver.Close(); err != nil {
select {
case last <- err:
default:
}
}
var err error
select {
case err = <-last:
case <-time.After(time.Second * 5):
}
select {
case t.sigc <- syscall.SIGHUP:
default:
}
return err
}

View File

@@ -0,0 +1,46 @@
package auction
import (
"local/sandbox/selenium/copart/copart/browser"
"time"
"github.com/tebeka/selenium"
)
type WebDriver interface {
browser.WebDriver
}
type WebElement interface {
selenium.WebElement
}
func waitForXPath(wd WebElement, path string) selenium.WebElement {
var we selenium.WebElement
test := func() bool {
v, err := wd.FindElements("xpath", path)
if err != nil {
return false
}
if len(v) < 1 {
return false
}
we = v[0]
return true
}
for !test() {
time.Sleep(1)
}
return we
}
func followTags(wd WebElement, tag ...string) selenium.WebElement {
for _, t := range tag {
var err error
wd, err = wd.FindElement("tag name", t)
if err != nil {
return nil
}
}
return wd
}