查看: 569|回复: 9
|
用Scrapy做網頁爬蟲
[复制链接]
|
|
本帖最后由 nsda 于 20-4-2018 09:17 PM 编辑
1. 簡介
2. 安裝
3. 用Scrapy爬佳禮的帖
- 策劃
- coding
- 執行
- 建議
1. 簡介
Scrapy是什麼?
參考Scrapy官方網站是最準確的, 基本上Scrapy就是網頁爬蟲工具/框架,開源(https://github.com/scrapy/scrapy),用簡單的Python可以輕易爬幾千幾萬頁的網站。
為什麼要用Scrapy?
以前我嘗試自己寫過網頁爬蟲,分別用過Java/Scala/Python,但是每次都很難重用在其他爬蟲Projects(除非你肯花時間認真設計,並且抽象化所有接口)。
其次,網路上可以發生的事情太多,自己寫的爬蟲需要聰明的去retry以下的狀況:
- 可能你的電腦斷線,
- 可能對方網路不穩定,
- 可能雙方網路都沒問題不過就是Request Timeout,
- 也可能對方網站有Bugs,
自己寫的爬蟲也要到處去找適合的libraries來幫你extract HTML Elements,以前有時候懶惰找,自己寫Regex來當HTML Extractor,最後自己都看不懂自己的代碼
而Scrapy就是自動幫你完成這些麻煩的東西,簡單如以下的代碼可以爬完一個blog:
- # file name: myspider.py
- import scrapy
- class BlogSpider(scrapy.Spider):
- name = 'blogspider'
- start_urls = ['https://blog.scrapinghub.com']
- def parse(self, response):
- for title in response.css('h2.entry-title'):
- yield {'title': title.css('a ::text').extract_first()}
- for next_page in response.css('div.prev-post > a'):
- yield response.follow(next_page, self.parse)
复制代码- pip install scrapy # 安裝scrapy
- scrapy runspider myspider.py # 執行爬蟲
复制代码
當中你只需要懂url跟html elements的css selector,其他boilerplate代碼都大同小異的
|
|
|
|
|
|
|
|
发表于 20-4-2018 07:15 AM
|
显示全部楼层
|
|
|
|
|
|
|
楼主 |
发表于 20-4-2018 09:20 PM
|
显示全部楼层
本帖最后由 nsda 于 20-4-2018 11:22 PM 编辑
1. 簡介
2. 安裝
3. 用Scrapy爬佳禮的帖
- 策劃
- coding
- 執行
- 建議
安裝是沒什麼好寫的,因為用pip來安裝實在太容易了
注意:我的執行環境是python3,python2理應沒問題(讀取csv那邊可能要把'r'改成'rb'),不過我沒試過
|
|
|
|
|
|
|
|
楼主 |
发表于 20-4-2018 09:29 PM
|
显示全部楼层
|
|
|
|
|
|
|
楼主 |
发表于 20-4-2018 10:20 PM
|
显示全部楼层
本帖最后由 nsda 于 20-4-2018 11:25 PM 编辑
1. 簡介
2. 安裝
3. 用Scrapy爬佳禮的帖
- 策劃
- coding
- 執行
- 建議
好了,有了對策,可以寫Code了
- import scrapy
- class ThreadSpider(scrapy.Spider):
- name = "thread_spider"
- start_urls = ['https://cforum1.cari.com.my/forum.php?mod=forumdisplay&fid=562']
- def parse(self, response):
- SELECTOR = "tbody[id^='normalthread_']"
- for thread in response.css(SELECTOR):
- NAME_SELECTOR = 'span.thaut ::text'
- USER_ID_SELECTOR = 'tr td.avtx a ::attr(href)'
- TITLE_SELECTOR = 'tr th div div a.xst ::text'
- VIEW_SELECTOR = 'span.thview ::text'
- COMMENTS_SELECTOR = 'span.thcmd ::text'
- ID_SELECTOR = '::attr(id)'
- yield {
- 'id': int(thread.css(ID_SELECTOR).extract_first().replace('normalthread_', '')),
- 'user_id': int(thread.css(USER_ID_SELECTOR).extract_first().replace('home.php?mod=space&uid=', '')),
- 'user_name': thread.css(NAME_SELECTOR).extract_first(),
- 'title': thread.css(TITLE_SELECTOR).extract_first(),
- 'view': int(thread.css(VIEW_SELECTOR).extract_first().strip().replace('查看:', '')),
- 'comments': int(thread.css(COMMENTS_SELECTOR).extract_first().strip().replace('评论:', '').replace('-', '0')),
- }
- NEXT_PAGE_SELECTOR = 'a.nxt ::attr(href)'
- next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
- if next_page:
- yield scrapy.Request(
- response.urljoin(next_page),
- callback=self.parse
- )
复制代码
start_urls裡的https://cforum1.cari.com.my/forum.php?mod=forumdisplay&fid=562就是國內政治時事的url所以的selector都是css selector,如果你在chrome,用view page source會大概明白我在做什麼
以上圖為例,我先用“SELECTOR = "tbody[id^='normalthread_']"”來找單一主題,這個selector知道每個"normalthread_"為ID開頭的tbody都是一個主題。
然後用“TITLE_SELECTOR = 'tr th div div a.xst ::text'”來找主題的題目,仔細看你會發現,我只是沿著tbody->tr->th->div->a.xst一直找到::text,然後用extract_first來抓第一個符合的value(通常都是,depends on html design)
一些value我拿到後會做些filtering,比如每個主題都有“查看:xxx”,我只要xxx並且要收成數字,所以用了replace跟int
- import scrapy
- import urllib.parse as urlparse
- import csv
- import os
- class PostSpider(scrapy.Spider):
- name = "post_spider"
- file_timestamp = os.getenv('TIMESTAMP')
- threads_file = 'threads.csv'
-
- if file_timestamp:
- threads_file = 'threads_' + file_timestamp + '.csv'
-
- data_562 = []
- with open(threads_file, 'r') as f:
- reader = csv.DictReader(f)
- data_562 = list(reader)
- ids_562 = [d['id'] for d in data_562]
- ids = list(set(ids_562)) # avoid duplicate ids
- print(ids)
- start_urls = ["https://cforum1.cari.com.my/forum.php?mod=viewthread&tid={}".format(id) for id in ids]
- def parse(self, response):
- POST = "div[id^='post_'] table[id^='pid']"
- for post in response.css(POST):
- POST_ID = '::attr(id)'
- USER_ID = 'div.favatar div.pi div.authi a ::attr(href)'
- USER_NAME = 'div.favatar div.pi div.authi a ::text'
- CREATED_AT = 'em[id^=authorposton] ::text'
- ORDER = "a[id^=postnum] em ::text"
- yield {
- 'thread_id': int(urlparse.parse_qs(urlparse.urlparse(response.url).query)['tid'][0]),
- 'post_id': int(post.css(POST_ID).extract_first().replace('pid', '')),
- 'user_id': int(post.css(USER_ID).extract_first().replace('home.php?mod=space&uid=', '')),
- 'user_name': post.css(USER_NAME).extract_first(),
- 'order': int(post.css(ORDER).extract_first()),
- 'created_at': post.css(CREATED_AT).extract_first().replace('发表于 ', ''),
- }
- NEXT_PAGE_SELECTOR = 'a.nxt ::attr(href)'
- next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
- if next_page:
- yield scrapy.Request(
- response.urljoin(next_page),
- callback=self.parse
- )
复制代码
這裡跟上面大致一樣,只是多了一個讀取url的步驟,第一個爬蟲是從第一頁開始,但是這個爬蟲是根據第一個爬蟲的結果,所以需要這個步驟。
可能你會奇怪,為什麼會有file_timestamp這個東西,這是拿來方便做automation的
|
|
|
|
|
|
|
|
楼主 |
发表于 20-4-2018 10:21 PM
|
显示全部楼层
本帖最后由 nsda 于 20-4-2018 11:01 PM 编辑
1. 簡介
2. 安裝
3. 用Scrapy爬佳禮的帖
- 策劃
- coding
- 執行
- 建議
好了,可以開始執行(確定你已經安裝了Scrapy):
- scrapy runspider threads.py -o threads.csv -s "DEPTH_LIMIT=2"
复制代码
執行完畢,你會看到一些statistics:
- {'downloader/request_bytes': 1780,
- 'downloader/request_count': 3,
- 'downloader/request_method_count/GET': 3,
- 'downloader/response_bytes': 144564,
- 'downloader/response_count': 3,
- 'downloader/response_status_count/200': 3,
- 'finish_reason': 'finished',
- 'finish_time': datetime.datetime(2018, 4, 20, 13, 34, 18, 825890),
- 'item_scraped_count': 85,
- 'log_count/DEBUG': 90,
- 'log_count/INFO': 8,
- 'memusage/max': 49709056,
- 'memusage/startup': 49704960,
- 'request_depth_max': 2,
- 'response_received_count': 3,
- 'scheduler/dequeued': 3,
- 'scheduler/dequeued/memory': 3,
- 'scheduler/enqueued': 3,
- 'scheduler/enqueued/memory': 3,
- 'start_time': datetime.datetime(2018, 4, 20, 13, 34, 14, 529640)}
复制代码
而results也會存在一個叫threads.csv的文件
- $ head -n10 threads.csv
- id,user_id,user_name,title,view,comments
- 4049311,2136456,Cari_YWQ,“被人叫走狗” 陈胜尧感叹只是道出真相,到底做错了什么?,620,45
- 4049151,273449,蓝火爆,投希盟的理由!!!,1215,76
- 4049112,2236753,一燈大師,繼『兩千塊拿你命』後,現在升級到策劃『自殺式攻擊』了,826,40
- 4049393,1580059,abc74,国阵候选人名单难产,选情吃紧打乱纳吉布局?,33,1
- 4048519,2306283,華沙論賤,可以投票了!來透露你 509 當天 囯席 會投給哪個陣營?,1103,34
- 4049229,173051,行动人,PACABA希盟上台的最后防线。,274,18
- 4043080,2236753,一燈大師,希盟的票房毒藥,1218,58
- 4047879,1997663,顺水,感谢东马联邦朋友,要不是你们反风不强,现在半岛......,1102,69
- 4049386,2183919,Cari_LSV,网友接到诈骗电话 一秒识破还把对方气得急挂电话!,125,17
复制代码
第二個爬蟲,它需要threads.csv來執行:
- scrapy runspider posts.py -o posts.csv
复制代码
一樣當執行完畢,你會看到一些statistics:
- {'downloader/request_bytes': 194009,
- 'downloader/request_count': 257,
- 'downloader/request_method_count/GET': 257,
- 'downloader/response_bytes': 12838667,
- 'downloader/response_count': 257,
- 'downloader/response_status_count/200': 257,
- 'finish_reason': 'shutdown',
- 'finish_time': datetime.datetime(2018, 4, 20, 14, 2, 8, 591056),
- 'item_scraped_count': 4237,
- 'log_count/DEBUG': 4495,
- 'log_count/ERROR': 1,
- 'log_count/INFO': 9,
- 'memusage/max': 49569792,
- 'memusage/startup': 49569792,
- 'request_depth_max': 15,
- 'response_received_count': 257,
- 'scheduler/dequeued': 257,
- 'scheduler/dequeued/memory': 257,
- 'scheduler/enqueued': 258,
- 'scheduler/enqueued/memory': 258,
- 'spider_exceptions/TypeError': 1,
- 'start_time': datetime.datetime(2018, 4, 20, 14, 1, 20, 871405)}
复制代码
這裡你會看到finish_reason是shutdown,因為我沒等它跑完就按Ctrl+C來終止了,依然會給你看report。
而results也會存在一個叫posts.csv的文件
- thread_id,post_id,user_id,user_name,order,created_at
- 4048618,132260179,2306283,華沙論賤,1,18-4-2018 06:11 AM
- 4048618,132260184,2249107,Cinabaku,2,18-4-2018 06:15 AM
- 4048618,132260189,2306283,華沙論賤,3,18-4-2018 06:25 AM
- 4048618,132261005,1843869,步惊云义侠,4,18-4-2018 10:22 AM
- 4048618,132261046,2065200,+_o_+,5,18-4-2018 10:30 AM
- 4048618,132261604,1844841,liau1977,6,18-4-2018 12:10 PM
- 4048618,132261695,1730941,Weida,7,18-4-2018 12:25 PM
- 4049164,132269109,1126865,tunrazak,1,20-4-2018 12:57 AM
- 4048742,132262476,2222531,snsv,1,18-4-2018 02:57 PM
复制代码
好了,為什麼我要放DEPTH_LIMIT呢?這裡告訴爬蟲我只要另外2層的數據,即使到第三頁就會停止(第二頁算是第一層,以此類推...),如果你拿掉DEPTH_LIMIT,爬蟲會一直執行直到無法再執行(第2000頁)。
完成了?還沒!
每次要跑二個爬蟲也很麻煩,最方便就是寫個script (run_spider.sh):
- #!/bin/bash
- set -e
- TIMESTAMP=$(date +%s)
- LIMIT=${1:-2000}
- scrapy runspider threads.py -o threads_$TIMESTAMP.csv -s "DEPTH_LIMIT=$LIMIT"
- scrapy runspider posts.py -o posts_$TIMESTAMP.csv
- echo
- echo "Threads data has been saved to threads_$TIMESTAMP.csv"
- echo "Posts data has been saved to posts_$TIMESTAMP.csv"
复制代码
記得給它執行的權限
執行
跑完後你會看見類似以下的訊息:
- Threads data has been saved to threads_1524233820.csv
- Posts data has been saved to posts_1524233820.csv
复制代码
|
|
|
|
|
|
|
|
楼主 |
发表于 20-4-2018 10:21 PM
|
显示全部楼层
本帖最后由 nsda 于 20-4-2018 11:21 PM 编辑
1. 簡介
2. 安裝
3. 用Scrapy爬佳禮的帖
- 策劃
- coding
- 執行
- 建議
從之前的代碼,你可以看到爬完整個國內版都不需要用到Regex,方便日後修改。
第二個爬蟲我沒有抓取回帖內容,因為內容佔很多空間,需要內容的可以自己嘗試修改,當作練習,有問題可以膠流膠流
以下是一些跑完爬蟲後到建議:
1. 不需要每次都跑完全部頁數,比如二個星期後你打算更新data,跑前面100頁已經卓卓有餘了,節省自己的時間也減輕對方server的負擔
- 可以用DEPTH_LIMIT做到這點
- 或者用我已經寫好的shell script
2. Scrapy可以同時進行的,但是這對網站來說也是一個額外的負擔,建議不要用,盡量做個文明的爬蟲
3. 爬完全部主題只需要少過一個小時的時間,但是爬完全部回帖需要幾天,所以:
- 建議申請一個aws ec2 free tier來跑,最好選新加坡region,連結到馬來西亞的佳禮server比較快,需要配合Screen command(http://dasunhegoda.com/unix-screen-command/263/),不然一logout爬蟲就完蛋了
- 或者把手上擁有的主題分批跑,這需要改以上的python script
- 佳禮每個星期1/星期4點早上凌晨維修,跑爬蟲時注意這點
最後,這篇只是Scrapy的教學,如果你需要以上爬蟲所爬到的數據(超過150萬),我願意分享出來,所以佳禮不用被虐待
我也是Scrapy新手,這篇教學其實也很基礎,如果你有其他更複雜的網頁,歡迎一起研究
|
|
|
|
|
|
|
|
楼主 |
发表于 20-4-2018 11:23 PM
|
显示全部楼层
歡迎當爬蟲
|
|
|
|
|
|
|
|
发表于 30-5-2018 11:17 AM
|
显示全部楼层
可以拿来开发打水跟单软件
希望有机会交流
|
|
|
|
|
|
|
|
楼主 |
发表于 30-5-2018 02:58 PM
|
显示全部楼层
歡迎交流 最近拿完假又沒時間玩其他東西了
|
|
|
|
|
|
|
| |
本周最热论坛帖子
|