217 lines
8.3 KiB
Racket
Executable File
217 lines
8.3 KiB
Racket
Executable File
#!/usr/bin/env racket
|
|
#lang racket
|
|
; vim: ft=racket
|
|
|
|
; __ __ __
|
|
; __/ // /_/ /___ _____ ____ _ __ ___ ____ __
|
|
; /_ _ __/ / __ `/ __ \/ __ `/ / / / / | /| / / / / /
|
|
; /_ _ __/ / /_/ / / / / /_/ / / /_/ /| |/ |/ / /_/ /
|
|
; /_//_/ /_/\__,_/_/ /_/\__, / \__,_/ |__/|__/\__,_/
|
|
; /____/
|
|
|
|
(require net/url-string
|
|
smol-http
|
|
"private/util.rkt")
|
|
|
|
(define HOST "fring.ccs.neu.edu")
|
|
(define ROOT-PATH "/fakebook/")
|
|
(define LOGIN-PATH "/accounts/login/")
|
|
(define DEFAULT-HDRS '((user-agent . "🦈 hash-lang-uwu crawler v1.0")
|
|
;; christo pls do not track thanks
|
|
(dnt . "1")))
|
|
|
|
;; ---------------------------------------------------------------------------------------
|
|
;; HTTP fetch logic
|
|
|
|
;; Returns response for request without closing the socket
|
|
(define (crawler-fetch/noclose method path [body #f])
|
|
;; Create the request based on the given parameters
|
|
(define req
|
|
(let ([basic-req (make-http-req method path '())])
|
|
(if body (http-set-body basic-req body) basic-req)))
|
|
;; Do the thing, with the thing
|
|
;; Include cookie jar cookies
|
|
(define sock
|
|
(http-connect
|
|
HOST #:headers (cons (cookie-jar->header (current-cookie-jar))
|
|
DEFAULT-HDRS)))
|
|
(define rsp (http-request sock req))
|
|
;; If we got any new cookies (yum!!) add them to the jar
|
|
(update-cookie-jar! (current-cookie-jar) (http-msg-headers rsp))
|
|
;; Abstract over some response codes we can handle directly here
|
|
(match (http-rsp-code rsp)
|
|
[(or 301 302)
|
|
;; handle redirects transparently
|
|
(define new-location (http-msg-header rsp 'location))
|
|
(http-close sock)
|
|
(crawler-fetch/noclose method new-location body)]
|
|
[500
|
|
;; handle server failure retries transparently
|
|
(crawler-fetch/noclose method path body)]
|
|
[_
|
|
;; other stuff like 403/404 up to caller
|
|
(values rsp sock)]))
|
|
|
|
;; Returns response for request, closing socket
|
|
(define (crawler-fetch . params)
|
|
(define-values [rsp sock] (apply crawler-fetch/noclose params))
|
|
(http-close sock)
|
|
rsp)
|
|
|
|
;; Fetches request and tries to parse the result as xexpr
|
|
(define (crawler-fetch/xexpr . params)
|
|
(define-values [rsp sock] (apply crawler-fetch/noclose params))
|
|
(match (http-rsp-code rsp)
|
|
;; return an empty response for 403 and 404. the page won't be visited again
|
|
;; because it will have been added to the complete set
|
|
[(or 403 404)
|
|
(http-close sock)
|
|
'()]
|
|
;; we shouldn't run into this one
|
|
[400 (error "you screwed up, got error 400 :angery:" params)]
|
|
;; normal response yay!!
|
|
[200
|
|
(define xe
|
|
(string->xexpr
|
|
(bytes->string/utf-8
|
|
(for/fold ([res #""])
|
|
([chunk (in-http-body-chunks (http-msg-body rsp))])
|
|
(bytes-append res chunk)))))
|
|
(http-close sock)
|
|
xe]
|
|
[code (http-close sock) (error "unexpected response code" code)]))
|
|
|
|
;; ---------------------------------------------------------------------------------------
|
|
;; Utilities
|
|
|
|
;; Logs in with the given username and password
|
|
(define (crawler-login username password)
|
|
(crawler-fetch 'GET LOGIN-PATH)
|
|
(define form-body
|
|
(format "username=~a&password=~a&csrfmiddlewaretoken=~a&next="
|
|
username password (cookie-jar-ref (current-cookie-jar) "csrftoken")))
|
|
(crawler-fetch 'POST LOGIN-PATH (string->bytes/utf-8 form-body))
|
|
(void))
|
|
|
|
;; Checks if this is a URL we should crawl
|
|
(define (crawler-valid-url? page-url)
|
|
(match page-url
|
|
[(url "http" _ (? (curry equal? HOST)) _ _ _ _ _) #t]
|
|
[_ #f]))
|
|
|
|
;; ---------------------------------------------------------------------------------------
|
|
;; Main crawler iterator
|
|
|
|
;; Fetches one page, prints any flags, and returns a list of urls to continue with
|
|
;; (may contain duplicates)
|
|
(define (crawler-iterate-one page-url)
|
|
(let* ([path (format "/~a" (string-join (map path/param-path (url-path page-url)) "/"))]
|
|
[xe (crawler-fetch/xexpr 'GET path)]
|
|
[flags (find-flags xe)]
|
|
[page-links (map (lambda (x) (combine-url/relative page-url x))
|
|
(find-hrefs xe))])
|
|
;; only return URLs that are OK to crawl
|
|
(values (filter crawler-valid-url? page-links) flags)))
|
|
|
|
;; ---------------------------------------------------------------------------------------
|
|
;; Tasking logic
|
|
|
|
(define NUM-TASKS 256)
|
|
|
|
;; Worker thread: recieves tasks, does the HTTP, returns yummy links and flags
|
|
(define (crawler-worker-thread worker-id jobserver)
|
|
(let loop ()
|
|
(define next-job (thread-receive))
|
|
;; jobserver sends a #f when it's time to exit
|
|
(when next-job
|
|
(define-values (new-urls flags) (crawler-iterate-one next-job))
|
|
(thread-send jobserver (list worker-id next-job new-urls flags))
|
|
(loop))))
|
|
|
|
;; Starts up all the threads and schedules jobs until crawling is complete
|
|
(define (crawler-jobserver root-url)
|
|
;; Inner function to be run in its own thread
|
|
(define (jobserver-thread)
|
|
;; Create workers
|
|
(define worker-threads
|
|
(let ([parent (current-thread)])
|
|
(for/vector ([i (in-range NUM-TASKS)])
|
|
(thread (lambda () (crawler-worker-thread i parent))))))
|
|
;; Create worker status - #t means the worker is ready for the next task
|
|
;; #f means the worker is currently busy
|
|
(define worker-ready (make-vector NUM-TASKS #t))
|
|
;; URLs we have yet to crawl
|
|
(define pending (mutable-set root-url))
|
|
;; URLs we have crawled already
|
|
(define completed (mutable-set))
|
|
;; All flags seen so far
|
|
(define all-flags (mutable-set))
|
|
;; Main loop
|
|
(let loop ()
|
|
(define any-busy (for/or ([r (in-vector worker-ready)]) (not r)))
|
|
;; Loop until there are no more busy workers and there are also no more pending URLs
|
|
;; Then, we are done
|
|
(unless (and (not any-busy) (set-empty? pending))
|
|
;; Find any available worker to give the next job to
|
|
(define available-worker
|
|
(for/first ([w (in-naturals)]
|
|
[r (in-vector worker-ready)]
|
|
#:when r)
|
|
w))
|
|
;; If there is a worker and a job, assign the job
|
|
(when (and (not (set-empty? pending)) available-worker)
|
|
(define next-job (set-first pending))
|
|
(set-remove! pending next-job)
|
|
(thread-send (vector-ref worker-threads available-worker) next-job)
|
|
(vector-set! worker-ready available-worker #f)
|
|
(set! any-busy #t))
|
|
|
|
;; If there are no more jobs right now or there are no available workers, wait for one of
|
|
;; the workers to complete its job
|
|
(when (or (set-empty? pending) (false? available-worker))
|
|
(match-define (list worker-id job-url new-urls flags) (thread-receive))
|
|
(vector-set! worker-ready worker-id #t)
|
|
;; Update our state with the results
|
|
(set-add! completed job-url)
|
|
(for ([u (in-list new-urls)])
|
|
(unless (set-member? completed u)
|
|
(set-add! pending u)))
|
|
(for ([flag (in-list flags)] #:when (not (set-member? all-flags flag)))
|
|
(print-flag flag)
|
|
(set-add! all-flags flag)))
|
|
|
|
;; Useful debug status is printed in debug mode
|
|
(print-progress (+ (set-count completed) (set-count pending))
|
|
(for/sum ([v (in-vector worker-ready)] #:when (not v)) 1)
|
|
(set-count completed) (set-count all-flags))
|
|
|
|
(loop)))
|
|
(print-complete (set-count completed) (set-count all-flags))
|
|
;; send all workers the shutdown message and wait
|
|
(for ([thd (in-vector worker-threads)])
|
|
(thread-send thd #f)
|
|
(thread-wait thd)))
|
|
;; start a new thread so we get a unique mailbox
|
|
(thread-wait (thread jobserver-thread)))
|
|
|
|
;; ---------------------------------------------------------------------------------------
|
|
;; public static void main open paren string square brackets args
|
|
|
|
(define (run-webcrawler username password)
|
|
(printf-debug "the credentials are: ~s ~s\n" username password)
|
|
(printf-debug "logging in...\n")
|
|
(crawler-login username password)
|
|
(printf-debug "logged in. Starting crawl\n")
|
|
(define base-url (url "http" #f HOST #f #t (list (path/param ROOT-PATH '())) '() #f))
|
|
(crawler-jobserver base-url))
|
|
|
|
;; Parse command line arguments and run
|
|
(module+ main
|
|
(command-line
|
|
#:program "webcrawler"
|
|
#:once-each
|
|
[("-d") "Debug mode" (debug-mode? #t)]
|
|
#:args
|
|
(username password)
|
|
(run-webcrawler username password)))
|