- Common Lisp 100%
| src | Add a new load-resource method for lists | |
| .gitignore | Initial commit | |
| LICENSE | Add MIT license | |
| README.org | Highlight | |
| scraper.asd | Bump version to 1.0.1 | |
scraper
About
- This is a small library that provides some policy on how to organize web scraping code.
- It wants to know the following.
-
WHATkind of page do you want to process?- This is accomplished by making small subclasses of
abstract-page.
- This is accomplished by making small subclasses of
-
WHEREis the page?- This is handled polymorphically by
load-resourceandload-resource-as-dom.
- This is handled polymorphically by
(process (page 'all-images "https://metacpan.org/"))
;; WHAT WHERE
That's all this does. It just provides a little bit of structure.
I really hesitated to call this a library, because it's so small. I think of it more as a policy for you to follow.
Extending Scraper For Your Own Use
Package Setup
- This is how I would pull in scraper into your own package.
(defpackage whatever
(:use ; Pull in the libraries you want to use.
:cl ; This is a very minimal example.
:quri) ; Feel free to pull in more libraries that you like.
(:import-from #:lquery
#:$
#:1ドル)
(:import-from #:scraper ; Import these symbols from scraper.
#:abstract-page
#:src
#:process
#:load-resource-as-dom
#:page))
(in-package :whatever)
Create a subclass of abstract-page.
(defclass all-images (abstract-page)
()
(:documentation "This is a general page type for scraping all images from a page."))
- The main purpose of creating a subclass is to allow
processto dispatch on this new type. - Subclasses of
abstract-pageinherit a slot namedsrc. - If you want to add more slots for your own use, you may.
Implement a process method that dispatches on your new type.
(defmethod process ((page all-images))
"Return a list of all images from the `page'."
(let* ((dom (load-resource-as-dom (src page)))
(images ($ dom "img" (attr "src"))))
(coerce images 'list)))
- A
processmethod should take apageas a parameter. - However, there are no restrictions on what it returns as long as it makes sense to you.
Example Usage
Basic Usage
First, let's look at the most typical way this would be used.
;; For demonstration purposes, go into the scraper package
;; to make symbols easily accessible.
(ql:quickload :scraper)
(in-package :scraper)
(defparameter mi (page 'all-images "https://metacpan.org/"))
(process mi)
("/static/images/metacpan-logo.svg" "/static/images/metacpan-logo.svg"
"/static/images/sponsors/bytemark_logo.svg"
"/static/images/sponsors/datadog.svg" "/static/images/sponsors/deriv.svg"
"/static/images/sponsors/elastic.svg"
"/static/images/sponsors/fastly_logo.svg"
"/static/images/sponsors/geocodelogo.svg"
"/static/images/sponsors/iban-logo.png"
"/static/images/sponsors/liquidweb_logo.png"
"/static/images/sponsors/open-cage.svg"
"/static/images/sponsors/perl-onion.svg")
Polymorphism on Resource Type
What if the HTML you want is in the file system?
cd /tmp
wget https://metacpan.org/
# saved as /tmp/index.html
We can use the same process method on a different source.
(defparameter mi-local (page 'all-images "file:///tmp/index.html"))
(process mi-local)
("/static/images/metacpan-logo.svg" "/static/images/metacpan-logo.svg"
"/static/images/sponsors/bytemark_logo.svg"
"/static/images/sponsors/datadog.svg" "/static/images/sponsors/deriv.svg"
"/static/images/sponsors/elastic.svg"
"/static/images/sponsors/fastly_logo.svg"
"/static/images/sponsors/geocodelogo.svg"
"/static/images/sponsors/iban-logo.png"
"/static/images/sponsors/liquidweb_logo.png"
"/static/images/sponsors/open-cage.svg"
"/static/images/sponsors/perl-onion.svg")
You can also work directly with plump-dom elements.
(defparameter dom (load-resource-as-dom (uri "file:///tmp/index.html")))
(process (page 'all-images dom))
("/static/images/metacpan-logo.svg" "/static/images/metacpan-logo.svg"
"/static/images/sponsors/bytemark_logo.svg"
"/static/images/sponsors/datadog.svg" "/static/images/sponsors/deriv.svg"
"/static/images/sponsors/elastic.svg"
"/static/images/sponsors/fastly_logo.svg"
"/static/images/sponsors/geocodelogo.svg"
"/static/images/sponsors/iban-logo.png"
"/static/images/sponsors/liquidweb_logo.png"
"/static/images/sponsors/open-cage.svg"
"/static/images/sponsors/perl-onion.svg")
This lets you even use the same scraping code on a specific part of the document.
(process (page 'all-images ($ dom ".footer-social")))
("/static/images/metacpan-logo.svg")
Polymorphism on Page Type
What if I wanted to parse feeds instead of images while using the same source?
(defparameter mf (page 'all-feeds "https://metacpan.org/"))
(process mf)
((:HREF "/recent.rss" :TYPE "application/rss+xml" :TITLE
"Recent CPAN Uploads - MetaCPAN"))
Changing the type from all-images to all-feeds changes what process method gets used. Notice that the return value has a different shape that the previous calls the process. Specializations of process are allowed to return data in any shape that makes sense to the author.
API Reference
Class: abstract-page
- Whenever you want to scrape a certain kind of page, you make a subclass of
abstract-page. - It must contain a
srcslot that letsload-resourcereturn the data of the resource. -
A few concrete page subclasses have been provided as examples.
all-imagesall-feeds
Generic Function: (load-resource src)
- This is a generic function that dispatches on various types to return the data of a resource.
- It's usually being passed the value of the
srcslot of a page instance. - Whether a page is being loaded from the web or frome the file system,
load-resourceshould be able to handle it.
| Supported Types | Description |
|---|---|
| quri:uri-http | Load from the web. Both HTTP and HTTPS are supported. |
| quri:uri-file | Load from the filesystem. |
| simple-string | Use the given string as the source. |
| cons | Assumed to be a list of strings that can be concatenated to form one document |
| vector | Assumed to be a vector of plump-dom elements |
- This should be enough for most people, but you could always add more if you have another data source you want to support.
Function: (load-resource-as-dom src)
- This function uses
load-resourceand converts the result to a vector ofplump-domelements that are ready to be queried. - This is the function that should be used when implementing your own
processmethods.
;; load via HTTP
(load-resource-as-dom (uri "https://codeberg.org/"))
;; load via filesystem
(load-resource-as-dom (uri "file:///tmp/index.html"))
Function: (page subtype src)
- This is a convenience function for instantiating page subtypes.
- It saves a little bit of typing.
(page 'all-images "https://slashdot.org/")
;; is the same as
(make-instance 'all-images :src (uri "https://slashdot.org/"))
- To pass in a string that you don't want to be treated as a URI, wrap it in a list.
- This can be useful to those who want to fetch HTML in another way.
(page 'all-images (list "<html></html>"))
Generic Function: (process page)
- Every page type should have its own specialized
processmethod for doing the actual scraping.
(defparameter si (page 'all-images "https://slashdot.org/"))
(defparameter images (process si))
Variable: *proxy*
- As a convenience, an HTTP proxy may be specified here if you need one.
- Anything that dexador can use as a proxy is valid here.
(setq *proxy* "http://user:password@myproxy:3128")
Variable: *headers*
- As a convenience, HTTP headers can be set here if needed.
- Anything that dexador can use as a header is valid here.
(let* ((*headers* '(("User-Agent" . "FooBar")))
(res (load-resource (uri "https://httpbin.org/anything?q=life"))))
(princ res))
{
"args": {
"q": "life"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Content-Length": "0",
"Host": "httpbin.org",
"User-Agent": "FooBar"
},
"json": null,
"method": "GET",
"url": "https://httpbin.org/anything?q=life"
}