{"id":243,"date":"2010-03-15T01:57:21","date_gmt":"2010-03-15T01:57:21","guid":{"rendered":"http:\/\/yuguangzhang.com\/blog\/?p=243"},"modified":"2010-03-15T01:57:21","modified_gmt":"2010-03-15T01:57:21","slug":"uw-course-calendar-scraper","status":"publish","type":"post","link":"http:\/\/yuguangzhang.com\/blog\/uw-course-calendar-scraper\/","title":{"rendered":"UW Course Calendar Scraper"},"content":{"rendered":"<p>I&#8217;ve had the idea of making a self-updating, navigable tree of Waterloo courses. This is the first step. (Actually, not the first step for me. It started with Django, which had to do with my last work report&#8217;s comparison to Zen Cart. Some credit goes to Thomas Dimson for inspiration. He made the <a href=\"http:\/\/coursequalifier.com\">Course Qualifier<\/a>.) The main idea for this step is to gather all the information to be stored in a database. With that (the idea and plan) begins the coding phase:<br \/>\n[cc lang=&#8221;python&#8221;]<br \/>\nfrom scrapy.item import Item, Field<\/p>\n<p>class UcalendarItem(Item):<br \/>\n    course = Field()<br \/>\n    name = Field()<br \/>\n    desc = Field()<br \/>\n    prereq = Field()<br \/>\n    offered = Field()<br \/>\n[\/cc]<br \/>\nI wanted to gather the course (&#8220;SE 101&#8221;), name (&#8220;Introduction to Methods of Software Engineering&#8221;), desc (&#8220;An introduction &#8230;&#8221;), prereq (&#8220;Software Engineering students only&#8221;), and offered (&#8220;F&#8221;) each as separate fields<br \/>\n<a href=\"http:\/\/yuguangzhang.com\/blog\/wp-content\/uploads\/2010\/03\/snapshot4.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-medium wp-image-244\" title=\"snapshot4\" src=\"http:\/\/yuguangzhang.com\/blog\/wp-content\/uploads\/2010\/03\/snapshot4-300x78.png\" alt=\"snapshot4\" width=\"300\" height=\"78\" srcset=\"http:\/\/yuguangzhang.com\/blog\/wp-content\/uploads\/2010\/03\/snapshot4-300x78.png 300w, http:\/\/yuguangzhang.com\/blog\/wp-content\/uploads\/2010\/03\/snapshot4.png 738w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><br \/>\nIn order to do that,  I wrote a spider to crawl the page:<br \/>\n[cc lang=&#8221;python&#8221;]<br \/>\nfrom scrapy.spider import BaseSpider<br \/>\nfrom scrapy.selector import HtmlXPathSelector<br \/>\nfrom ucalendar.items import UcalendarItem<\/p>\n<p>class UcalendarSpider(BaseSpider):<br \/>\n    domain_name = &#8220;uwaterloo.ca&#8221;<br \/>\n    start_urls = [<br \/>\n        &#8220;http:\/\/www.ucalendar.uwaterloo.ca\/0910\/COURSE\/course-SE.html&#8221;<br \/>\n    ]<\/p>\n<p>    def parse(self, response):<br \/>\n\thxs = HtmlXPathSelector(response)<br \/>\n\ttables = hxs.select(&#8216;\/\/table[@width=&#8221;80%&#8221;]&#8217;)<br \/>\n\titems = []<br \/>\n        for table in tables:<br \/>\n\t    item = UcalendarItem()<br \/>\n\t    item[&#8216;desc&#8217;] = table.select(&#8216;tr[3]\/td\/text()&#8217;).extract()<br \/>\n\t    item[&#8216;name&#8217;] = table.select(&#8216;tr[2]\/td\/b\/text()&#8217;).extract()<br \/>\n\t    item[&#8216;course&#8217;] = table.select(&#8216;tr[1]\/td\/b\/text()&#8217;).re(&#8216;([A-Z]{2,5} \\d{3})&#8217;)<br \/>\n\t    item[&#8216;offered&#8217;] = table.select(&#8216;tr[3]\/td&#8217;).re(&#8216;.*\\[.*Offered: (F|W|S)+,* *(F|W|S)*,* *(F|W|S)*\\]&#8217;)<br \/>\n\t    item[&#8216;prereq&#8217;] = table.select(&#8216;tr[5]\/td\/i\/text()&#8217;).re(&#8216;([A-Z]{2,5} \\d{3})&#8217;)<br \/>\n            items.append(item)<br \/>\n\treturn items<\/p>\n<p>SPIDER = UcalendarSpider()<br \/>\n[\/cc]<br \/>\nThere are several things to note:<\/p>\n<ul>\n<li>The prereq field here cannot identify &#8220;For Software Engineering students only&#8221;. The regular expression only matches the course code.<\/li>\n<li>Offered, unlike other fields, can contain more than one item<\/li>\n<li>Prereq may be empty<\/li>\n<\/ul>\n<p>Finally, the spider pipes its results to an output format. CSV format meets the requirements, as it can be inserted into a database.<br \/>\n[cc lang=&#8221;python&#8221;]<br \/>\nimport csv<\/p>\n<p>class CsvWriterPipeline(object):<\/p>\n<p>    def __init__(self):<br \/>\n        self.csvwriter = csv.writer(open(&#8216;items.csv&#8217;, &#8216;wb&#8217;))<\/p>\n<p>    def process_item(self, spider, item):<br \/>\n\ttry:<br \/>\n\t    self.csvwriter.writerow([item[&#8216;course&#8217;][0], item[&#8216;name&#8217;][0], item[&#8216;desc&#8217;][0], item[&#8216;prereq&#8217;][0], item[&#8216;offered&#8217;][0]])<br \/>\n\texcept IndexError:<br \/>\n\t    self.csvwriter.writerow([item[&#8216;course&#8217;][0], item[&#8216;name&#8217;][0], item[&#8216;desc&#8217;][0], &#8216; &#8216;.join(item[&#8216;prereq&#8217;]), &#8216; &#8216;.join(item[&#8216;offered&#8217;])])<br \/>\n        return item<br \/>\n[\/cc]<br \/>\nTwo gotchas:<\/p>\n<ul>\n<li>Because prereq might be empty, there needs to be an exception handler<\/li>\n<li>Offered may be variable length. The list needs to be joined to output all of the terms the course is offered.<\/li>\n<\/ul>\n<p>This part of the project was done in 2 hours with <a href=\"http:\/\/doc.scrapy.org\/intro\/tutorial.html\">Scrapy<\/a>. The project can be found in <a href=\"http:\/\/yuguangzhang.com\/home\/view_files.php\">the downloads section<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;ve had the idea of making a self-updating, navigable tree of Waterloo courses. This is the first step. (Actually, not the first step for me. It started with Django, which had to do with my last work report&#8217;s comparison to Zen Cart. Some credit goes to Thomas Dimson for inspiration. He made the Course Qualifier.) [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_import_markdown_pro_load_document_selector":0,"_import_markdown_pro_submit_text_textarea":"","footnotes":""},"categories":[29,23],"tags":[22],"class_list":["post-243","post","type-post","status-publish","format-standard","hentry","category-coursetree","category-programming","tag-python"],"aioseo_notices":[],"_links":{"self":[{"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/posts\/243","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/comments?post=243"}],"version-history":[{"count":0,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/posts\/243\/revisions"}],"wp:attachment":[{"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/media?parent=243"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/categories?post=243"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/yuguangzhang.com\/blog\/wp-json\/wp\/v2\/tags?post=243"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}