Python の urllib.robotparser が失敗するときの対処法
Python の urllib.robotparse
を使ってrobot.txtをパースしようとしたらハマったのでメモ。
Python でクローラーを作成していて、URLへのアクセス許可やクロールする際の遅延時間を urllib.robotparse
で取得しようとしたら、なぜか技術評論社の robots.txt
の内容が読み込めなくて困った。
https://gihyo.jp/robots.txt(2020/11/26現在)
User-agent: * Disallow: /tagList Disallow: /*? Allow: /*?page Allow: /*?start Allow: /book/genre Allow: /book/series Allow: /book/topics User-agent: bingbot Crawl-delay: 5 User-agent: Pinterest Disallow: / Sitemap: http://gihyo.jp/sitemap.xml
robots.txt
によれば、任意のユーザーエージェントに対して https://gihyo.jp/book/genre
へのアクセスは許可が設定されており、ユーザーエージェント bingbot
に対して Crawl-delay
は5秒が設定されているが、urllib.robotparser
でそれらの設定がとれない。
>>> import urllib.robotparser >>> import urllib.request >>> url = 'https://gihyo.jp/robots.txt' >>> rp = urllib.robotparser.RobotFileParser(url) >>> rp.read() >>> rp <urllib.robotparser.RobotFileParser object at 0x7f2678d15780> >>> print(rp.can_fetch("*", "https://gihyo.jp/book/genre")) False # <- Trueが返ってきてほしい >>> print(rp.crawl_delay("bingbot")) None # <- 5が返ってきてほしい
ユーザーエージェントの変更
上記サイトによると、urllibがrobots.txtを開く際に使用しているデフォルトのユーザーエージェントがサイトからブロックされている場合があるようだ。
たしかに、urllib.request.urlopen()を直接叩いて https://gihyo.jp/robots.txt にアクセスしようとすると、403が返ってくる。
>>> import urllib.request >>> url = 'https://gihyo.jp/robots.txt' >>> urllib.request.urlopen(url) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.6/urllib/request.py", line 223, in urlopen return opener.open(url, data, timeout) File "/usr/lib/python3.6/urllib/request.py", line 532, in open response = meth(req, response) File "/usr/lib/python3.6/urllib/request.py", line 642, in http_response 'http', request, response, code, msg, hdrs) File "/usr/lib/python3.6/urllib/request.py", line 570, in error return self._call_chain(*args) File "/usr/lib/python3.6/urllib/request.py", line 504, in _call_chain result = func(*args) File "/usr/lib/python3.6/urllib/request.py", line 650, in http_error_default raise HTTPError(req.full_url, code, msg, hdrs, fp) urllib.error.HTTPError: HTTP Error 403: Forbidden
これを回避するには、urllib.request.install_opener()を使って、urllibにデフォルトと異なるユーザーエージェントを使うよう指示すればいい。
>>> opener = urllib.request.build_opener() >>> opener.addheaders = [('User-agent', 'MyUa/0.1')] >>> urllib.request.install_opener(opener) >>> urllib.request.urlopen(url) <http.client.HTTPResponse object at 0x7feb220ee240> >>> res = urllib.request.urlopen(url) >>> res.status 200 # <- OK
urllib.robotparser
でもちゃんと robots.txt
の内容を読めるようになった。
>>> import urllib.robotparser >>> import urllib.request >>> opener = urllib.request.build_opener() >>> opener.addheaders = [('User-agent', 'MyUa/0.1')] >>> urllib.request.install_opener(opener) >>> url = 'https://gihyo.jp/robots.txt' >>> rp = urllib.robotparser.RobotFileParser(url) >>> rp.read() >>> print(rp.can_fetch("*", "https://gihyo.jp/book/genre")) True >>> print(rp.crawl_delay("bingbot")) 5
rloginでviの表示が崩れるときの対処法
職場のSolarisで別のマシンにrloginでリモートログインして作業していたら、viの表示が崩れる(スクロールができないなど)という現象が起きた。
調べてみると、telnetやsshにはリアルタイムに端末のサイズを取得できる機能があるが、その他の接続形式ではそれが保証されていないことがあるためなようだ。
参考サイト:
(ふつうにsshを使えという話だが、なぜか今の現場ではrloginを使う人が多いので、これだと困ってしまう。)
解決法
ログインシェルのドットファイル(bashなら~/.bashrc
、cshなら~/.cshrc
)に以下を追記することで、viの表示が崩れなくなった。
eval `resize`
Pythonでスクレイピングの練習
スクレイピングに興味があったので、PythonによるWebスクレイピング 第2版を読んでいる。
GithubでJupyter notebooksが公開されていて(REMitchell/python-scraping: Code samples from the book Web Scraping with Python)、実際にコードを動かしながら読めるのでわかりやすい。3章までに読んだ内容を元に、スクレイピングの練習をしてみる。
架空のオンライン書店のスクレイピング
スクレイピングの対象は以下のサイトにした。
スクレイピングの学習者向けに公開されているサイトで、架空のオンライン書店の形をしている。自由にスクレイピングしていいらしいので、とてもありがたい。
今回は、このオンライン書店内にある全ての本のタイトルと価格を抽出して表示するプログラムを作る。
ページ内のリンクを再帰的に取得する
以下のコードは、http://books.toscrape.com/ を起点として、ページ内のリンクを再帰的に辿って表示する。
from urllib.request import urlopen from bs4 import BeautifulSoup import re from urllib.parse import urljoin # すでに訪れたページのリスト visitedPages = set() def getLinks(pageUrl): global visitedPages html = urlopen(pageUrl) bs = BeautifulSoup(html, 'html.parser') print(pageUrl) for link in bs.find_all('a'): if 'href' in link.attrs: newPage = urljoin(pageUrl, link.attrs['href']) if newPage not in visitedPages: visitedPages.add(newPage) getLinks(newPage) getLinks('http://books.toscrape.com/')
実行するとサイト内のすべてのページのURLが出力される。
これだけのコード量でサイト内のページをすべて取得できるのだから、再帰というものは魔法みたいだと思う。
$ python3 book-crawler.py http://books.toscrape.com/ http://books.toscrape.com/index.html http://books.toscrape.com/catalogue/category/books_1/index.html http://books.toscrape.com/catalogue/category/books/travel_2/index.html http://books.toscrape.com/catalogue/category/books/mystery_3/index.html http://books.toscrape.com/catalogue/category/books/historical-fiction_4/index.html http://books.toscrape.com/catalogue/category/books/sequential-art_5/index.html http://books.toscrape.com/catalogue/category/books/classics_6/index.html http://books.toscrape.com/catalogue/category/books/philosophy_7/index.html http://books.toscrape.com/catalogue/category/books/romance_8/index.html http://books.toscrape.com/catalogue/category/books/womens-fiction_9/index.html 〜〜以下省略〜〜
基本的には 『PythonによるWebスクレイピング』の第3章のコード例 とそれほど変わらないが、いくつか変えたところもある。以下で説明する。
相対URLを絶対URLに変換する
『PythonによるWebスクレイピング』ではWikipediaの英語版をスクレイピングの対象としていて、内部リンクがすべて /wiki/
から始まる相対URLとなっているため、絶対URLをもとめるのが簡単になっていた(https://en.wikipedia.org
とその相対URLを結合するだけで絶対URLになる)。
一方、http://books.toscrape.com/ では、
<a href="../../../../index.html">Home</a>
のようなダブルドット(..
)を使った相対URLが使われているため、単純な文字列の結合だけでは絶対URLを得ることができない。
幸い、PythonにはURLを解析していい感じに分解したり結合したりできる urllib.parse.urljoin
という標準ライブラリが用意されている
(urllib.parse --- URL を解析して構成要素にする — Python 3.8.5 ドキュメント)。
使い方は簡単で、以下のように、第一引数に自分がいま見ているページの絶対URLを、第二引数にそのページからの相対URLを渡すと、相対URLを絶対URLにして返してくれる。
>>> from urllib.parse import urljoin >>> urljoin('http://books.toscrape.com/catalogue/category/books/travel_2/index.html', '../../../../index.html') 'http://books.toscrape.com/index.html'
コードの以下の部分でページ内のリンクを絶対URLに変換するのに使った。
newPage = urljoin(pageUrl, link.attrs['href'])
本の情報を抽出する
単にURLを出力するだけではおもしろくないので、本のタイトルと価格を表示してみる。
商品ページの特徴
本の情報を抽出するためには、いま見ているページが商品のページなのか、それともトップページ(All products | Books to Scrape - Sandbox)やカテゴリごとのページ(Poetry | Books to Scrape - Sandbox)など商品の一覧のページなのか区別する必要がある。商品のページのときだけ本の情報を抽出したい。
URLに商品ページに固有の特徴を見つけられなかったので、ページの内容に特徴を探すことにした。
適当な商品のページ(A Light in the Attic | Books to Scrape - Sandbox)のソースを観察してみると、商品のページでは、
<article class="product_page">
のように <article>
タグに商品ページであることを示すクラス属性 product_page
が付与されていることがわかった。
このタグは以下のようにCSSセレクタを使って取得できるから、その結果から商品ページかどうか判断できる。
bs.select(".product_page")
タイトルの取得
次に商品ページから本のタイトルを取得する。
ブラウザの開発者ツールを使って本のタイトル部分の要素を選択すると、タイトルは単純に h1
タグの中にあることがわかる。
したがって、タイトルは以下のように取得できる。
title = bs.h1.get_text()
本の価格
本の価格の場合はすこし面倒で、タイトルと同様に要素を調べると、<p class="price_color">£51.77</p>
のように price_color
というクラス属性が付与されているのが価格だとわかる。
ただし、本によっては他の本の情報を下の方に表示しているものがあるため(例えばThe Black Maria | Books to Scrape - Sandbox)、ページの上の方に載っているメインの商品の価格のみを取得できるようにしたい。開発者ツールを使ってメインの商品の情報の部分を選択すると、product_main
というクラス属性が付与された div
タグで囲まれていることがわかる。
したがって、本の価格は以下のように取得できる。
price = bs.select_one(".product_main>.price_color").get_text()
最終的なコード
以上を組み合わせて、最終的なコードは以下のようになった。
from urllib.request import urlopen from bs4 import BeautifulSoup import re from urllib.parse import urljoin # すでに訪れたページのリスト visitedPages = set() def getLinks(pageUrl): """ ページ内のリンクを再帰的に辿って、タイトルと価格およびURLを表示する Parameters ---------- pageUrl : str スクレイピングするページのURL """ global visitedPages html = urlopen(pageUrl) bs = BeautifulSoup(html, 'html.parser') try: if bs.select(".product_page"): title = bs.h1.get_text() price = bs.select_one(".product_main>.price_color").get_text() print("{}, {}, {}".format(title, price, pageUrl)) except AttributeError: pass for link in bs.find_all('a'): if 'href' in link.attrs: newPage = urljoin(pageUrl, link.attrs['href']) if newPage not in visitedPages: visitedPages.add(newPage) getLinks(newPage) getLinks('http://books.toscrape.com/')
実行すると、すべての本のタイトル、価格、商品のページのURLが出力された。
$ python3 book-crawler2.py The Long Shadow of Small Ghosts: Murder and Memory in an American City, £10.97, http://books.toscrape.com/catalogue/the-long-shadow-of-small-ghosts-murder-and-memory-in-an-american-city_848/index.html Dark Notes, £19.19, http://books.toscrape.com/catalogue/dark-notes_800/index.html Amid the Chaos, £36.58, http://books.toscrape.com/catalogue/amid-the-chaos_788/index.html Chasing Heaven: What Dying Taught Me About Living, £37.80, http://books.toscrape.com/catalogue/chasing-heaven-what-dying-taught-me-about-living_797/index.html The Activist's Tao Te Ching: Ancient Advice for a Modern Revolution, £32.24, http://books.toscrape.com/catalogue/the-activists-tao-te-ching-ancient-advice-for-a-modern-revolution_928/index.html The Four Agreements: A Practical Guide to Personal Freedom, £17.66, http://books.toscrape.com/catalogue/the-four-agreements-a-practical-guide-to-personal-freedom_970/index.html We Are All Completely Beside Ourselves, £24.04, http://books.toscrape.com/catalogue/we-are-all-completely-beside-ourselves_301/index.html 〜〜以下省略〜〜
まとめ
『PythonによるWebスクレイピング』の第3章までで学んだ内容を元に、架空のオンライン書店から本の情報をスクレイピングする練習をしてみた。
コードを汎用的にしたり、集めたデータをDBに蓄積したりするのは、もっと後の章をよんでから。
WSL2 を有効化したら VirtualBox が動作しなくなったので戻した
WSL2を有効化したところ、VirtualBoxに入れていたUbuntuが起動しなくなってしまった。 (ログイン画面までは表示されるが、ログイン後にデスクトップ画面がいつまでたっても表示されない。)
- VirtualBoxのバージョン: 6.1.8
- Windows10のバージョン: Windows10 Home 2004
- WSL2 を有効化する際に実施した手順: Windows Subsystem for Linux (WSL) を Windows 10 にインストールする | Microsoft Docs
WSL2 を有効化することで Hyper-V が有効化され、VirtualBox がこれまで使用していた仮想化機構(VT-X)が無効化されるのが原因のようだった。
WSL 2 についてよく寄せられる質問 | Microsoft Docs
VirtualBox 6.0 以降は Hyper-V と共存できるようになったはずだが、環境によってはうまく動かない場合があるようだ。
VirtualBoxが使えないのは困るので、とりあえず WSL1 に戻した。
手順
PowerShellを起動し、以下のコマンドで既定のバージョンを WSL1 に戻す。
wsl --set-default-version 1
また、自分の場合、WSL1 で使っていた Ubuntu のバージョンを WSL2 に変更していた(wsl --set-version Ubuntu 2
を実行した)ので、以下のコマンドで WSL1 に戻した。
wsl --set-version Ubuntu 1
PowerShellを管理者として起動し、以下のコマンドで VirtualMachinePlatform を無効化する。
dism.exe /online /Disable-Feature /featurename:VirtualMachinePlatform
マシンの再起動を求められるので、それに従って再起動すると、VirtualBoxが動くようになった。
dism コマンドによる Windows の機能の無効化については、以下のサイトを参考にした。
無印良品 USBデスクファンの分解と掃除
暑くなってきたので、無印良品のUSBデスクファンを引っ張り出してきた。残念なことにほこりがついていたので、分解して掃除した。
この製品はファンの後ろ側の部分が簡単に外れるようになっているけど、一見しただけでは外し方がわかりにくいようになっている。
ファンの後ろ側の部分をつかんで、(ファンの後ろ側を手前として)反時計周りに回すと、分離できる。
また、後ろ側の羽は引っ張るだけで簡単にとれる。
さらに、ドライバを使えば前側の部分も外れるみたいだが、ドライバがないのでできなかった。
すこしやりづらかったが、ウェットティッシュで拭いて掃除は終わり。
これで気持ちよく使えるようになった。