{"id":2146,"date":"2012-03-12T00:12:59","date_gmt":"2012-03-11T22:12:59","guid":{"rendered":"http:\/\/yeti.albascout.ro\/blog\/?p=2146"},"modified":"2012-03-12T00:12:59","modified_gmt":"2012-03-11T22:12:59","slug":"google-spreadsheet-read-write-din-python","status":"publish","type":"post","link":"https:\/\/yeti.albascout.ro\/blog\/google-spreadsheet-read-write-din-python\/","title":{"rendered":"Google Spreadsheet read \/ write din python"},"content":{"rendered":"<p>Am primit de cur\u00e2nd o provocare interesant\u0103: Adi \u0218uhanea are un Google Spreadsheet (actualizat automat \/ manual de cineva) cu localit\u0103\u021bile \u0219i num\u0103rul de \u0219coli \u00eenscrise la cerceta\u0219i pentru <a title=\"Scoala Altfel\" href=\"http:\/\/scout.ro\/scoalaaltfel\" target=\"_blank\">\u0218coala Altfel<\/a>, \u0219i avea nevoie s\u0103 afi\u0219eze \u00eentr-o pagin\u0103 a site-ului (bazat pe Joomla) o hart\u0103 cu pe care s\u0103 fie distribuite \u0219colile, \u00een func\u021bie de localitate.<!--more--><\/p>\n<p>Solu\u021bia ini\u021bial\u0103 era un scriptule\u021b care rezolva numele localit\u0103\u021bilor \u00een coordonate, \u0219i apoi afi\u0219a pe un Google Map &#8211; problema cu solu\u021bia asta este c\u0103, de c\u00e2nd Google Maps API a introdus obligativitatea unui API key, daca ai cateva zeci de hituri pe pagina respectiv\u0103 pe zi \u0219i c\u00e2teva zeci de localit\u0103\u021bi de afi\u0219at (\u0219i <a href=\"http:\/\/www.scout.ro\/scoalaaltfel\/index.php\/despre-cercetasi\/169-harta-cu-scolile-inscrise\" target=\"_blank\">sunt<\/a>), ajungi foarte foarte repede la limita de request-uri pentru Geocoder-ul google.<\/p>\n<p>Pe scurt, cea mai bun\u0103 solu\u021bie \u00een cazul \u0103sta este s\u0103 salvezi undeva rezultatul geocod\u0103rii (long \u0219i lat), ca s\u0103 nu mai faci codarea \u0219i data viitoare c\u00e2nd \u00ee\u021bi este afi\u0219at\u0103 pagina &#8211; dar av\u00e2nd \u00een vedere c\u0103 localit\u0103\u021bile se salveaz\u0103 \u00een Google Spreadsheet, cea mai bun\u0103 solu\u021bie este s\u0103 p\u0103strezi rezultatele tot acolo. Dar cum?<\/p>\n<p>Enter <a href=\"http:\/\/code.google.com\/apis\/gdata\/\" target=\"_blank\">Google Data API<\/a> (for <a href=\"http:\/\/code.google.com\/p\/gdata-python-client\/\" target=\"_blank\">python<\/a>, \u00een cazul nostru). API-ul este destul de complicat pentru utilizarea elementar\u0103 de care avem noi nevoie, dar am g\u0103sit modulul <em><a href=\"http:\/\/code.google.com\/p\/gdata-python-client\/source\/browse\/src\/gdata\/spreadsheet\/text_db.py\" target=\"_blank\">text_db<\/a> <\/em>din acela\u0219i API, care pune la dispozi\u021bie o interfa\u021b\u0103 mai simpl\u0103 pentru accesarea Spreadsheet-urilor.<\/p>\n<p>Sigur, pe l\u00e2ng\u0103 scrierea \u00een document, e nevoie \u0219i de traducerea efectiv\u0103 a adresei \u00een long \u0219i lat, lucru pentru care este nevoie de un Geocoder (\u0219i am folosit o interfa\u021b\u0103 python pentru Google Geocoder &#8211; <a href=\"http:\/\/code.google.com\/p\/geopy\/\" target=\"_blank\">geopy<\/a>)<\/p>\n<p>So here we go:<\/p>\n<pre lang=\"python\">from geopy import geocoders\r\nfrom gdata.spreadsheet import text_db\r\n\r\nusername = \"test@example.com\"\r\npassword = \"secret\"\r\ndocument_name = \"Numele documentului\"\r\n\r\n# se obtine interfata catre gdata-api\r\nclient = DatabaseClient(username, password)\r\n# se deschide documentul (care va fi tratat database-like)\r\ndatabase = client.GetDatabases(name = document_name)\r\n# si obtin toate sheet-urile (in contextul asta, tabele)\r\ntables = database[0].GetTables()\r\n\r\n# LookupFields initializeaza numele campurilor din coloane cu \r\n# textul din prima linie a fiecarei coloane\r\ntables[0].LookupFields()\r\n\r\n# obtinem o lista cu toate liniile ceva in coloane cu header-ul\r\n# localitate (povestea cu \\\" \\\" este necesara, am incercat si cu \r\n# single quote ('), si nu \"vrea\"\r\nlines = tables[0].FindRecords(\"localitate != \\\"\\\"\")\r\n\r\n# initializam geocoder-ul\r\n# din documentatie, se poate initializa cu domeniul tarii, unde\r\n# este disponibil, pentru a \"tuna\" rezultatele cautarii\r\ng = geocoders.Google(GMAP_API, domain = \"maps.google.ro\")\r\n\r\nfor record in lines:\r\n\u00a0\u00a0\u00a0 # datele sunt initializate in dictionarul contents\r\n\u00a0\u00a0\u00a0 # numele coloanelor, indiferent de cum sunt trecute in document\r\n\u00a0\u00a0\u00a0 # sunt normalizate - lowercased, fara spatii, si daca sunt doua cu acelasi nume\r\n\u00a0\u00a0\u00a0 # primesc nume gen nume1, nume2 ... (este o functie in text_db care detaliaza)\r\n\u00a0\u00a0\u00a0 if record.content.get(\"lat\") == None:\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 try:\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 # aici pot fi doua probleme - una, ca nu este niciun rezultat, si atunci va fi ridicata \r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 # o exceptie (prinsa mai jos), sau vor fi mai multe - de aceea o selectam doar pe prima\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 place, (lat, lng) = g.geocode(record.content.get(\"localitate\"), exactly_one = False)[0]\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 # documentul meu vrea ca despartitor decimal , nu ., desi reprezentarea default in python\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 # este cu . - este deci nevoie sa operam usor stringul.\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 lat = \"%f\" % record.content['lat']\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 lng = \"%f\" % record.content['long']\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 record.content['lat'] = \"%f\" % lat.replace(\".\", \",\")\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 record.content['long'] = \"%f\" % lng.replace(\".\", \",\")\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 record.content['place'] = place\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 # push trimite documentului datele\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 record.Push()\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 except Exception:\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 continue<\/pre>\n<p>Ok, that&#8217;s it &#8211; ori de c\u00e2te ori se adaug\u0103 \u00een document o localitate care NU are nimic trecut \u00een c\u00e2mpurile pentru long \u0219i lat, se va face o cerere c\u0103tre geocoder \u0219i se vor salva rezultatele primite \u00een acela\u0219i spreadsheet.<\/p>\n<p>Mai este un side effect fericit aici: dac\u0103 primul rezultat nu este cel mai bun, operatorul poate corecta manual, ad\u0103ug\u00e2nd mai multe informa\u021bii l\u00e2ng\u0103 localitate (spre exemplu, jude\u021bul), \u0219i \u0219terg\u00e2nd longitutidea \u0219i latitudinea.<\/p>\n<p>Pe backend, script-ul face treaba descris\u0103 mai sus o dat\u0103 la c\u00e2teva ore. Pentru afi\u0219are, am legat repede un mini-app Django cu un view, un model \u0219i un template, care afi\u0219eaz\u0103 harta.<\/p>\n<p>Ah, \u0219i \u00een procesul \u0103sta am inventat \u0219i un marker de hart\u0103 pentru Google Maps, pe care \u00eel pute\u021bi lua de mai jos, \u00een formate png (direct) sau svg (Inkscape), bazat pe .eps-ul de <a href=\"http:\/\/www.tonyhaddon.com\/site\/2011\/03\/vector-drawn-google-map-marker\/\" target=\"_blank\">aici<\/a>.<\/p>\n<p><a href=\"http:\/\/yeti.albascout.ro\/blog\/wp-content\/uploads\/2012\/03\/icon_oncr_map.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-2147\" title=\"icon_oncr_map\" src=\"http:\/\/yeti.albascout.ro\/blog\/wp-content\/uploads\/2012\/03\/icon_oncr_map.png\" alt=\"\" width=\"80\" height=\"80\" \/><\/a><\/p>\n<p><a  data-e-Disable-Page-Transition=\"true\" class=\"download-link\" title=\"\" href=\"http:\/\/yeti.albascout.ro\/blog\/download\/9018\/?tmstv=1775651072\" rel=\"nofollow\" id=\"download-link-9018\" data-redirect=\"false\" >\n\tVarianta PNG\t(1015 downloads\t)\n<\/a>\n<br \/>\n<a  data-e-Disable-Page-Transition=\"true\" class=\"download-link\" title=\"\" href=\"http:\/\/yeti.albascout.ro\/blog\/download\/9020\/?tmstv=1775651073\" rel=\"nofollow\" id=\"download-link-9020\" data-redirect=\"false\" >\n\tVarianta SVG (Inkscape)\t(1646 downloads\t)\n<\/a>\n<\/p>\n<p>\u0218i rezultatul final, mai jos:<\/p>\n<p><iframe loading=\"lazy\" src=\"http:\/\/yeti.albascout.ro\/ymaps\/\" width=\"100%\" height=\"300px\"><\/iframe><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Am primit de cur\u00e2nd o provocare interesant\u0103: Adi \u0218uhanea are un Google Spreadsheet (actualizat automat \/ manual de cineva) cu localit\u0103\u021bile \u0219i num\u0103rul de \u0219coli \u00eenscrise la cerceta\u0219i pentru \u0218coala Altfel, \u0219i avea nevoie s\u0103 afi\u0219eze \u00eentr-o pagin\u0103 a site-ului (bazat pe Joomla) o hart\u0103 cu pe care s\u0103 fie distribuite \u0219colile, \u00een func\u021bie de &hellip; <a href=\"https:\/\/yeti.albascout.ro\/blog\/google-spreadsheet-read-write-din-python\/\" class=\"more-link\">Continu\u0103 s\u0103 cite\u0219ti <span class=\"screen-reader-text\">Google Spreadsheet read \/ write din python<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":2147,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[764,557],"tags":[],"class_list":["post-2146","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aplicatii-web","category-facultate"],"jetpack_featured_media_url":"https:\/\/yeti.albascout.ro\/blog\/wp-content\/uploads\/2012\/03\/icon_oncr_map.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/posts\/2146","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/comments?post=2146"}],"version-history":[{"count":3,"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/posts\/2146\/revisions"}],"predecessor-version":[{"id":2150,"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/posts\/2146\/revisions\/2150"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/media\/2147"}],"wp:attachment":[{"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/media?parent=2146"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/categories?post=2146"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/yeti.albascout.ro\/blog\/wp-json\/wp\/v2\/tags?post=2146"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}