Package: bluemoss
bluemossscrape the webGetting StartedpipinstallbluemossFull DocumentationRead the docsIntroductionfrombluemossimportNode,scrapenode=Node('a')# extract the text contained in the first a-tagscrape(node,YOUR_HTML)With **bluemoss**, you create a `Node` object to scrape data from any website. 
Imagine that `Node` object to be like a recipe 👩‍🍳 which describes what to scrape from which tags, 
and how to transform and structure the scraped data it into the format you need.**bluemoss** uses XPath 1.0 to locate html tags. If you are new to XPath, no problem —ChatGPThas got your back.ExamplesThis section will show you how can to scrape the web withbluemoss.For all examples that follow, we are going to scrape the html document below.<html><body><li><ahref="/portfolio?company=apple">Apple</a><divclass="location_us"><p>Cupertino</p></div></li><li><ahref="/portfolio?company=google">Google</a><divclass="location_us"><p>Mountain View</p></div></li><li><ahref="/portfolio?company=deepmind">DeepMind</a><divclass="location_uk"><p>London</p></div></li></body></html>Example 1- Scrape the text in the firstatag.TheNodeobject below instructs thescrapefunction to find the firsta-tag and extract the text it contains.frombluemossimportNode,scrapenode=Node('a')scrape(node,HTML)=='Apple'Example 2- Scrape thesecondcompany headquarters which are located in the USnode=Node('div[contains(@class, "location_us")]',filter=1)scrape(node,HTML)=='Mountain View'Pro Tip: Learning XPath is easy and fast with ChatGPT.This example introduces thefilterargument which determines which of those tag(s) that match the xpath locator
will be scraped. The default value for thefilterarg is 0 (index 0), which will scrape the very first
tag that matches the given xpath. Our goal for this example was to extract the text of the second tag (index 1)
that matches our xpath, therefor we setfilter = 1.Example 3- ScrapeALLcompany headquarters which are located in the USnode=Node('div[contains(@class, "location_us")]',filter=None)scrape(node,HTML)==['Cupertino','Mountain View']Settingfilter=Nonewill filter for all tags matched against the given xpath.Example 4- Scrape the first and third company names.Node('a',filter=[0,2])scrape(node,HTML)==['Apple','DeepMind']In this example we set thefilterarg to a list of ints.
Those int values refer to the first and third index (0 and 2).Example 5- Scrape all company names, but exclude the first one.The expected scrape result is['Google','DeepMind']# 'Apple' not included, since it is located in the first a-tag (index 0)Below are a couple of valid solutions, and the first one is the most recommended one, 
since it keeps the arguments provided to the Node object as simple as possible:# solution 1, the recommended solutionfrombluemossimportNode,RangeNode('a',filter=Range(1))# Range(1) filters the matched tags from index 1 onwards# solution 2Node('li//a',filter=Range(1))# the xpaths 'a' and 'li//a' match the same tags in our html doc# solution 3Node('a',filter=[1,2])# solution 4Node('a',filter=Range(1,3))# The Range class accepts a second int argument (the stop index).# Range(x, y) filters for the matched tags at indices x, ..., y-1#   e.g. Range(2, 6) filters for indices 2, 3, 4, 5.Example 6- Scrape all company names from index 1 onwards in reverse order.The expected scrape result is['DeepMind','Google']# solution 1Node('a',filter=Range(1,reverse=True))# set reverse to True# solution 2Node('a',filter=Range(1),transform=lambdares:res[::-1])# use transform argThe first two examples simply set thereversearg of the Range object to True.
The last example uses a different approach. It introduces thetransformarg of theNodeclass.The transform function is the function being executed afterthe tags were matched with the given xpaththe matched tags were further filtered using theNode.filterargPro Tip: The transform-function defines the last step in computing the scrape-result of a Node object.Example 7- Scrape the last two company names and store the resultin a dictunder the key 'companies'Node('a',filter=[-2,-1],key='companies')scrape(node,HTML)=={'companies':['Google','DeepMind']}In the example above we provide the indexes -2 and -1 to the filter-list as those indices represent the last
two elements in a Python list.Example 8- Scrape the first company name and store the resultin a dictunder the key 'companies'Node('a',key='companies')scrape(node,HTML)=={'companies':'Apple'}Example 9- Scrape the first company-id.Let us first define a helper function that will receive the href-value as an argument and return the company id:# helper function to the solutionsdefget_company_id(href:str)->str:returnhref.split('=')[-1]# solution 1fromsrc.bluemossimportNode,ExNode('a',extract=Ex.HREF,transform=get_company_id)# Declare the tag-property to be extracted by using the 'extract' arg.# Introducing the 'Ex' Enum which provides handy types of extraction.# solution 2Node('a',extract='href',transform=get_company_id)# The 'extract' arg also accept string values.# solution 3Node('a/@href',transform=get_company_id)# We can also just use xpath to extract the href property.Example 10 - Advanced Scraping- Scrape the name and headquarters of every company.The expected result:[['Apple','Cupertino'],['Google','Mountain View'],['DeepMind','London']]# solutionNode('li',# match 'li' tagsfilter=None,# scrape all the matched 'li' tagsnodes=[Node('a'),# within the 'li' tag, match the first 'a' tag and extract the textNode('p')# within the 'li' tag, match the first 'p' tag and extract the text])Pro Tip: The **nodes** arg let's you scrape multiple different tags within the same parent tag.Example 11 - Advanced Scraping-
Scrape the name and headquarters of every company. The result shall be a list of dicts.The expected result:[{'name':'Apple','location':'Cupertino'},{'name':'Google','location':'Mountain View'},{'name':'DeepMind','location':'London'}]# solutionNode('li',filter=None,nodes=[Node('a',key='name'),Node('p',key='location')])Pro Tip: All Nodes in a nodes list either define the 'key' arg or none of them do.Example 12 - Advanced Scraping- In this last example, we want to scrape the name and location of every company,
as well as the total amount of companies located in the US and UK. We also want to store the scraped data
not in a dict or list as we did in the previous examples, but instead want to store the data in dataclass instances.Info- The code snippet below shows that we assume a dataclass calledCompaniesin which we will store the
entire scrape-result. The expected result also assumes, that theCompaniesinstance exposes two propertiesdictandjson# expected resultcompanies:Companies=scrape(node,HTML)companies.dict=={'companies':[{'name':'Apple','location':'Cupertino'},{'name':'Google','location':'Mountain View'},{'name':'DeepMind','location':'London'},],'amount_uk_companies':1,'amount_us_companies':2,'amount_companies':3}companies.json=="""{"companies": [{"name": "Apple","location": "Cupertino"},{"name": "Google","location": "Mountain View"},{"name": "DeepMind","location": "London"}],"amount_uk_companies": 1,"amount_us_companies": 2,"amount_companies": 3}"""# solutionfromdataclassesimportdataclassfrombluemossimportNode,Jsonify@dataclassclassCompany(Jsonify):name:strlocation:str@dataclassclassCompanies(Jsonify):companies:list[Company]amount_uk_companies:intamount_us_companies:int@propertydefamount_companies(self)->int:returnself.amount_us_companies+self.amount_uk_companies@propertydefdict(self):returnsuper().dict|{'amount_companies':self.amount_companies}Node(target=Companies,nodes=[Node('li',filter=None,# makes sure to filter all matched 'li' tagskey='companies',# will store the scrape-result as a dict under the key 'companies'target=Company,# the data scraped from every 'li' tag will be transformed into a 'Company' instancenodes=[# every 'Company' instance will be initialized with the parameters 'name' and 'location'Node('a',key='name'),# extracts the text from the 'li/a' tag and stores it in the Company.name parameterNode('p',key='location')# extracts the text from the 'li/p' tag and stores it in the Company.location parameter]),Node("div[@class='location_uk']",# finds all 'div' tags whose class property contains the text 'location_uk'filter=None,# filter all matched tagskey='amount_uk_companies',# stores the result in the Companies.amount_uk_companies parametertransform=lambdacompanies:len(companies)# The initial result is a list (due to filter=None) and it# will then be mapped to its length.# Therefor the final scrape-result is an int.),Node("div[@class='location_us']",filter=None,key='amount_us_companies',transform=lambdacompanies:len(companies))])The solution in the code snippet introduces two new things:TheNode.targetparameter allows us to specify what class or dataclass to use in order
to store the data scraped in theNode.nodeslist. If a Node instance sets atarget, then all instances in
Node.nodes must set theirkeyparameter and every key must map to one of the init parameters ofNode.target.TheJsonifyclass exposes the two propertiesdictandjson. It therefor makes it easy transform the
scraped data stored in dataclass instances into a Python dict or json string.Pro Tip: The Jsonify class will exclude any parameters starting with an underscore "_" from appearing in the return of the .dict and .json properties. This enables us to hide certain parameters from appearing in those returns.type safety- Dataclass instances as used in this example enforce typed parameters.properties- Sometimes we want our data transformations to take place inside the dataclass, e.g. through properties. Properties provide a simple way to derive data from the instance parameters of a class instance. By moving the data transformation step from theNode.transformparameter to adataclass property, we make the transformation explicitly available to the dataclass.post_init- Thepost_initmethod that is available in Python dataclasses is yet another nice step to manipulate the instance parameters and therefore move the data transformation step partially or as a whole from theNode.transformparameter to thepost_initmethod of the dataclass.Supported PlatformsLinuxMacOSWindowsSupported Python Versions3.93.103.113.12LicenseApache 2.0