溫馨小提示
全文加上程式碼總6.8k個字,閱讀大約10分鐘,謝謝你的點選,願能解決你的問題;
前言
前幾天在小豬群裡,有同學問,有人知道怎麼做豆瓣自動回覆功能嗎?然後群裡就各種大神出馬相助,各種填程式碼給資料的,也有同學說用selenium模擬下就好了等等,其實大家都說的對,伸手黨固然不好,但是考慮到讓一個不瞭解的同學去做這個事,的確有門檻,更別說查資料用selenium了;
豆瓣回覆功能嘗試
一開始的想法,也是用selenium的,但是想著,還是先模擬下,看看豆瓣的回覆流程吧;
先開啟需要回復的帖子,然後接著登入豆瓣,然後回到剛剛這個帖子上,觀察下介面,回覆按鈕就在底部,點選傳送就是評論了;
那我們就抓包看下請求吧,瀏覽器按F12,選擇network,點選左邊紅色按鈕兩次,把之前的資料都清除;
接著就輸入內容,點選傳送按鈕,然後檢視network介面,不難找到傳送評論的請求,從名字上看,也能確認是傳送評論的請求;
然後看了下右邊的請求資料,不難發現介面地址是:https://www.douban.com/group/topic/121989778/add_comment
複製程式碼
而且發現,請求的時候,要帶4個引數:
ck=TXEg
rv_comment=層主真帥,贊贊贊 #這個就是要回復的內容
start=0
submit_btn=傳送
複製程式碼
既然如此,那就用postman試一下吧:
請求頭引數用了常規的cookie、user-agent、referer、host;
而body這塊,雖然上面抓包看到有4個引數,但是實際驗證只需要2個即可,傳送的內容就是jbtest;
那在postman上點選send,然後在那個帖子上重新整理下頁面,居然能看到剛剛回復的內容
看來,豆瓣回覆功能只需要調介面就行了,都不用selenium了;
既然如此,不能寫出下面這程式碼:
import requests
#豆瓣具體帖子回覆的介面,格式是帖子連結+/add_comment
db_url = "https://www.douban.com/group/topic/121989778//add_comment"
headers = {
"Host": "www.douban.com",
"Referer": "https://www.douban.com/group/topic/121989778/?start=0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Cookie":"your cookie" #這裡需要輸入你自己的cookie資訊,如果遇到轉義字元,轉
義字元前面加\即可
}
params = {
"ck": "TXEg",
"rv_comment": "jbtest11111",#或者替換成想評論的東西
}
requests.post(db_url,headers=headers, data=params)
複製程式碼
上面的程式碼就是定義請求頭跟body引數,像豆瓣的評論介面發一個請求即可,執行下指令碼,重新整理下網頁:
沒問題,回覆功能多簡單,so easy;
自動功能
嗯,回覆功能可以了,那Python有沒有類似定時器的功能?定時執行上面的post請求就好了?
答案是有的,那就是APScheduler;
APScheduler簡介
APScheduler是Python的一個定時任務框架,可以很方便的滿足使用者定時執行或者週期執行任務的需求,
它提供了基於日期date、固定時間間隔interval 、以及類似於Linux上的定時任務crontab型別的定時任務。
並且該框架不僅可以新增、刪除定時任務,還可以將任務儲存到資料庫中,實現任務的持久化,所以使用起來非常方便。
官方簡介連結:apscheduler.readthedocs.io/en/3.3.1/
安裝
1)利用pip安裝:(推薦)
pip install apscheduler
複製程式碼
2)基於原始碼安裝:pypi.python.org/pypi/APSche…
python setup.py install
複製程式碼
4種元件
APScheduler有四種元件:
1)triggers(觸發器):
觸發器包含排程邏輯,每一個作業有它自己的觸發器,用於決定接下來哪一個作業會執行,除了他們自己初始化配置外,觸發器完全是無狀態的。
2)job stores(作業儲存):
用來儲存被排程的作業,預設的作業儲存器是簡單地把作業任務儲存在記憶體中,其它作業儲存器可以將任務作業儲存到各種資料庫中,支援MongoDB、Redis、SQLAlchemy儲存方式。
當對作業任務進行持久化儲存的時候,作業的資料將被序列化,重新讀取作業時在反序列化。
3)executors(執行器):
執行器用來執行定時任務,只是將需要執行的任務放在新的執行緒或者執行緒池中執行。
當作業任務完成時,執行器將會通知排程器。
對於執行器,預設情況下選擇ThreadPoolExecutor就可以了,但是如果涉及到一下特殊任務如比較消耗CPU的任務則可以選擇ProcessPoolExecutor,當然根據根據實際需求可以同時使用兩種執行器。
4)schedulers(排程器):
排程器是將其它部分聯絡在一起,一般在應用程式中只有一個排程器,應用開發者不會直接操作觸發器、任務儲存以及執行器,相反排程器提供了處理的介面。
通過排程器完成任務的儲存以及執行器的配置操作,如可以新增。修改、移除任務作業。
APScheduler提供了多種排程器,常用的排程器有:
名稱 | 場景 |
---|---|
BlockingScheduler | 適合於只在程式中執行單個任務的情況 |
BackgroundScheduler | 適合於要求任何在程式後臺執行的情況 |
AsyncIOScheduler | 適合於使用asyncio框架的情況 |
GeventScheduler | 適合於使用gevent框架的情況 |
TornadoScheduler | 適合於使用Tornado框架的應用 |
TwistedScheduler | 適合使用Twisted框架的應用 |
QtScheduler | 適合使用QT的情況 |
簡單的例子
from apscheduler.schedulers.blocking import BlockingScheduler
import time
# 例項化一個排程器
scheduler = BlockingScheduler()
def job1():
print "%s: 執行任務" % time.asctime()
# 新增任務並採用固定時間間隔,觸發方式為3s一次
scheduler.add_job(job1, 'interval', seconds=3)
# 開始執行排程器
scheduler.start()
複製程式碼
執行後的效果:
很簡單有沒有,先初始化,然後add_job,最後start就好了,那下面,再詳細講解下不同元件提供的功能吧;
定時任務
trigger提供任務的觸發方式,共有3種方式:
-
date:只在某個時間點執行一次,用法:run_data(datetime|str)
scheduler.add_job(my_job, 'date', run_date=date(2017, 9, 8), args=[]) scheduler.add_job(my_job, 'date', run_date=datetime(2017, 9, 8, 21, 30, 5), args=[]) scheduler.add_job(my_job, 'date', run_date='2017-9-08 21:30:05', args=[]) sched.add_job(my_job, args=[[])
- interval: 每隔一段時間執行一次,用法:weeks=0 | days=0 | hours=0 | minutes=0 | seconds=0, start_date=None, end_date=None, timezone=None
scheduler.add_job(my_job, 'interval', hours=2) scheduler.add_job(my_job, 'interval', hours=2, start_date='2017-9-8 21:30:00', end_date= '2018-06-15 21:30:00)
@scheduler.scheduled_job('interval', id='my_job_id', hours=2) def my_job(): print("Hello World")
- cron: 使用同linux下crontab的方式,即定時任務;,用法:(year=None, month=None, day=None, week=None, day_of_week=None, hour=None, minute=None, second=None, start_date=None, end_date=None, timezone=None)
sched.add_job(my_job, 'cron', hour=3, minute=30) sched.add_job(my_job, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2017-10-30')
@sched.scheduled_job('cron', id='my_job_id', day='last sun') def some_decorated_task(): print("I am printed at 00:00:00 on the last Sunday of every month!")
一般來說,使用的比較多的是interval方式,可以重點留意下;
定時任務實戰
1)APScheduler怎麼設定範圍時間任務,比如我想要在10:00~11:00這個範圍的時間內隨機一個時間點去執行任務
print(get_time()+"jbtest")
t= random.randint(1,10) # # 1~10秒隨機
scheduler.add_job(myjob, 'interval', seconds=t,start_date='2018-09-05 10:00:00', end_date='2018-09-05 11:00:00') # 估計就滿足你的需求了吧
scheduler.start()
複製程式碼
2)如果不想具體的時間,而是某個範圍的話:
3)區間直接 sched.scheduled_job('cron', day_of_week='mon-fri', hour='0-9', minute='30-59', second='*/3') 在週一到週五其間,每天的0點到9點之間,在30分到59分之間執行,執行頻次為3秒。
任務操作
新增任務add_job
add_job可以返回一個apscheduler.job.Job例項,因而可以對它進行修改或者刪除,而使用修飾器新增的任務新增之後就不能進行修改。
獲得任務列表get_jobs
可以通過get_jobs方法來獲取當前的任務列表,也可以通過get_job()來根據job_id來獲得某個任務的資訊。
並且apscheduler還提供了一個print_jobs()方法來列印格式化的任務列表。
scheduler.add_job(my_job, 'interval', seconds=5, id='my_job_id' name='test_job')
print scheduler.get_job('my_job_id')
print scheduler.get_jobs()
複製程式碼
修改任務 modify_job
修改任務的屬性可以使用apscheduler.job.Job.modify()或者modify_job()方法,可以修改除了id的其它任何屬性。
job = scheduler.add_job(my_job, 'interval', seconds=5, id='my_job' name='test_job')
job.modify(max_instances=5, name='my_job')
複製程式碼
刪除任務remove_job
刪除排程器中的任務有可以用remove_job()根據job ID來刪除指定任務或者使用remove(),
如果使用remove()需要事先儲存在新增任務時返回的例項物件,任務刪除後就不會在執行。
# 根據任務例項刪除
job = scheduler.add_job(myfunc, 'interval', minutes=2)
job.remove()
# 根據任務id刪除
scheduler.add_job(myfunc, 'interval', minutes=2, id='my_job_id')
scheduler.remove_job('my_job_id')
複製程式碼
任務的暫停pause_job和繼續resume_job
暫停與恢復任務可以直接操作任務例項或者排程器來實現。
當任務暫停時,它的執行時間會被重置,暫停期間不會計算時間。
job = scheduler.add_job(myfunc, 'interval', minutes=2)
# 根據任務例項
job.pause()
job.resume()
# 根據任務id暫停
scheduler.add_job(myfunc, 'interval', minutes=2, id='my_job_id')
scheduler.pause_job('my_job_id') # 暫停
scheduler.resume_job('my_job_id') #恢復
複製程式碼
任務的修飾modify和重設reschedule_job
修飾:job.modify(max_instances=6, name='Alternate name')
重設:scheduler.reschedule_job('my_job_id', trigger='cron', minute='*/5')
複製程式碼
排程器操作
開啟 scheduler.start()
可以使用start()方法啟動排程器,BlockingScheduler需要在初始化之後才能執行start(),
對於其他的Scheduler,呼叫start()方法都會直接返回,然後可以繼續執行後面的初始化操作
from apscheduler.schedulers.blocking import BlockingScheduler
def my_job():
print "Hello world!"
scheduler = BlockingScheduler()
scheduler.add_job(my_job, 'interval', seconds=5)
scheduler.start()
複製程式碼
關閉 scheduler.shotdown(wait=True | False)
使用下邊方法關閉排程器:
scheduler.shutdown()
複製程式碼
預設情況下排程器會關閉它的任務儲存和執行器,並等待所有正在執行的任務完成,如果不想等待,可以進行如下操作:
scheduler.shutdown(wait=False)
複製程式碼
暫停 scheduler.pause()
繼續 scheduler.resume()
豆瓣自動回覆
看了那麼多APScheduler的簡介,上面也有例子了,結合第一部分豆瓣的例子,不難寫出下面的程式碼:
import requests
from apscheduler.schedulers.blocking import BlockingScheduler
#豆瓣具體帖子回覆的介面,格式是帖子連結+/add_comment
db_url = "https://www.douban.com/group/topic/121989778//add_comment"
scheduler = BlockingScheduler()
headers = {
"Host": "www.douban.com",
"Referer": "https://www.douban.com/group/topic/121989778/?start=0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/68.0.3440.106 Safari/537.36",
"Cookie":"your cookie" #這裡需要輸入你自己的cookie資訊,如果遇到轉義字元,轉
義字元前面加\即可
}
params = {
"ck": "TXEg",
"rv_comment": "jbtest11111",
}
def my_job():
requests.post(db_url,headers=headers, data=params)
#每隔10S就請求一次
scheduler.add_job(my_job,"interval",seconds=10,id="db")
scheduler.start()
複製程式碼
效果如下:
的確是每隔10S傳送一次,good~
做到這裡,你以為完事了?
嗯,怎麼說呢,如果是一個帖子的話,是完事了,但是如下是以下2種場景之一,還需要折騰:
1)同一個帖子,需要短時間內回覆,這個短時間沒法定義,可能
幾分鐘都算,除非是像正常使用者幾個小時回覆一次就可能沒問題;
2)N個帖子都要回復,而且回覆間隔比較短,類似問題1;
複製程式碼
那這兩種情況,會導致什麼問題?當然是觸發豆瓣的防爬蟲啦:
目前發現,每次評論就算相隔1分鐘,只要滿3次,就一定會彈出這個驗證碼進行驗證;
獲取驗證碼ID
按照一開始的套路,那我們F12看下輸入驗證碼後發起請求的引數:
發現會在請求的帶上一個叫captcha-solution欄位,value就是驗證碼內容,那我們就異想天開的試試,用postman在body上加上這個驗證碼引數值,看能否評論?驗證碼資訊:
postman請求內容:
點選send,然後原來的網頁重新整理一下,結果如下:
對的,內容沒有變,因為這樣肯定是不生效的,哪有那麼容易;
驗證碼場景:
現在有網址T,有使用者A和B兩個人同事訪問T
T給A返回的驗證碼是X,給B返回的驗證碼是Y,這兩個驗證碼都正確
如果A輸入B的驗證碼,是驗證不通過的
複製程式碼
那伺服器怎麼區分A和B?那就是用cookie;
cookie是標示唯一身份的,比如有些網站,登入一次後會自動登入,但是如果清除了cookie,就無法自動登入了,而且這cookie是個別人不一樣的; 說到這裡,伺服器後臺生成驗證碼的流程就很容易理解了:
先隨機生產一個隨機字串
然後和cookie繫結
再寫到圖片上返回給你
複製程式碼
更多的驗證碼生成資訊,可以讀一下這篇文章;
此時,可能你有疑問,用postman的時候,cookie應該是跟PC點選傳送是一樣的,但是為什麼還不行?
因為cookie只是最簡單的繫結條件,這麼看來,豆瓣還有其他條件的,那我們重新看一下,PC點選傳送的時候,除了驗證碼,還有傳送什麼?
------WebKitFormBoundaryDjMAMsD95W3eYF1i
Content-Disposition: form-data; name="ck"
TXEg
------WebKitFormBoundaryDjMAMsD95W3eYF1i
Content-Disposition: form-data; name="rv_comment"
反反覆覆
------WebKitFormBoundaryDjMAMsD95W3eYF1i
Content-Disposition: form-data; name="img"; filename=""
Content-Type: application/octet-stream
------WebKitFormBoundaryDjMAMsD95W3eYF1i
Content-Disposition: form-data; name="captcha-solution"
produce
------WebKitFormBoundaryDjMAMsD95W3eYF1i
Content-Disposition: form-data; name="captcha-id"
woMrwYOVwn67NNfl9lv9vhRz:en
------WebKitFormBoundaryDjMAMsD95W3eYF1i
Content-Disposition: form-data; name="start"
0
------WebKitFormBoundaryDjMAMsD95W3eYF1i
Content-Disposition: form-data; name="submit_btn"
傳送
------WebKitFormBoundaryDjMAMsD95W3eYF1i--
複製程式碼
上面這些資訊都是點選傳送時,請求裡面的body,大致看了下,之前分析的時候,少了captcha-id這個引數,猜測這是就是關鍵;
簡單嘗試了下,發現這個captcha-id每次都會不一樣,而且從請求裡面也看不出啥,既然這個ID可能跟驗證碼有繫結關係,那我們就解析下這個網頁的結構,看下能不能找到這個欄位?
點選F12,定位到驗證碼這塊:
看了下右側的屬性,好像沒有什麼問題,想想,既然不是圖片,又要繫結ID屬性,那可能就是輸入驗證碼那裡了,繼續看~一定位到輸入驗證碼的框裡面,嚶嚶嚶,看發現了啥,這不就是想要的captcha-id嗎?
愛是懷疑,那我們就來用postman驗證一下吧;
頁面重新整理下:哈哈哈哈,可以了,再次嚶嚶嚶~
那現在的邏輯,應該是修改成這樣:
1)開啟帖子頁面,判斷是否需要輸入驗證碼,如果需要,獲取captcha-id跟驗證碼,
然後post請求captcha-id和驗證碼
2)如果不需要輸入驗證碼,那就直接post請求即可
複製程式碼
既然如此,對比下需要輸入驗證碼跟不需要驗證碼時的網頁結構吧;
需要驗證碼:
不需要驗證碼:
對比可知,需要輸入驗證碼的時候,會多了一個div標籤,這個div標籤展開了,二維碼的下載連結也能找到,captcha-id也能找到,既然如此,使用xpath就能判斷了,判斷captcha_image,如果能獲取到,就是需要驗證碼;
簡單做了下實驗,看看能不能獲取到這個captcha,有以下程式碼:
import requests
from lxml import html
response = requests.get(db_url).content
selector = html.fromstring(response)
captcha = selector.xpath("//img[@id=\"captcha_image\"]/@src")
print(captcha)
但是結果返回的是[],意思就是沒有獲取到這個值
複製程式碼
把response列印出來,真的是沒有這個值,這裡且慢,一開始JB的想法是,沒有這個值,就說明這塊資料是JS生成的,那我們研究看下怎麼獲取JS生成的網頁資料,然後就霹靂吧啦的介紹selenium;
事實證明,並不需要那麼複雜(浪費了半天時間了。。),還是上面這串程式碼:
import requests
from lxml import html
response = requests.get(db_url,verify=False).content
print(response)
複製程式碼
把response列印出來,結果發現內容長這樣:
嗯,認真點看,發現都是編碼過的,但是JB一開始並沒有細看,一看獲取是空的,就認定是JS載入的,其他這裡,只需要改成這樣就好了:response = requests.get(db_url,verify=False).content.decode()
複製程式碼
效果圖,這樣就能看到中文了:
這個解決方案,花了半天無意發現的,算是get 到一個小點了,以後request後一定要decode,不用還用selenium就跑遠了;
獲取二維碼下載地址
按照下面的內容,就可以寫出這樣的程式碼:
response = requests.post(db_url,headers=headers, data=params,verify=False).content.decode()
selector = html.fromstring(response)
captcha = selector.xpath("//img[@id=\"captcha_image\"]/@src")
print(captcha)
複製程式碼
然後再想需要回復的帖子回覆三次,讓驗證碼出現,然後再執行這個指令碼,不然驗證碼不出現,就會獲取為[]的:
這樣,就能獲取到驗證碼圖片啦,按照上面說的,如果是有驗證碼,就獲取圖片連結跟驗證碼ID,如果沒有,則直接post請求,因此不難寫出下面的程式碼;
import requests
from apscheduler.schedulers.blocking import BlockingScheduler
from lxml import html
# 豆瓣具體帖子連結
db_url = "https://www.douban.com/note/657346123/"
# 豆瓣具體帖子回覆的介面,格式是帖子連結+/add_comment
db_url_commet = "https://www.douban.com/note/657346123///add_comment"
scheduler = BlockingScheduler()
headers = {
"Host": "www.douban.com",
"Referer": "https://www.douban.com/group/topic/121989778/?start=0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Cookie":"your cookie" #這裡需要輸入你自己的cookie資訊,如果遇到轉義字元,轉
義字元前面加\即可
}
params = {
"ck": "TXEg",
"rv_comment": "jbtest11111",
}
def my_job():
# 獲取網頁資訊
response = requests.post(db_url, headers=headers, data=params, verify=False).content.decode()
selector = html.fromstring(response)
captcha_image = selector.xpath("//img[@id=\"captcha_image\"]/@src")
if(captcha_image):
print(captcha_image)
captcha_id = selector.xpath("//input[@name=\"captcha-id\"]/@value")
print(captcha_id)
else:
# 發起請求請求
requests.post(db_url_commet, headers=headers, data=params, verify=False)
# 每隔10S就請求一次
scheduler.add_job(my_job, "interval", seconds=2, id="db")
scheduler.start()
複製程式碼
效果圖:
Ok,這樣就能獲取到二維碼圖片跟圖片對應的ID了,那接下來要幹嘛?
識別二維碼
既然能獲取二維碼圖片,而請求的時候又要帶上這個欄位,那就意味著,必須先下載這個圖片,然後去識別這種圖片,然後放到請求上一起提交;
下載圖片&命名:
import requests
import re
i = "https://www.douban.com/misc/captcha?id=9iGoXeJXeos3E1JukgkltEVp:en&size=s"
captcha_name = re.findall("id=(.*?):",i) #findall返回的是一個列表
filename = "douban_%s.jpg" % (str(captcha_name[0]))
print("檔名為:"+filename)
#建立檔名
with open(filename, 'wb') as f:
#以二進位制寫入的模式在本地構建新檔案
header = {
'User-Agent': '"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",'
,'Referer': i}
f.write(requests.get(i,headers=header).content)
print("%s下載完成" % filename)
# urllib.request.urlretrieve(requests.get(i,headers=header), "%s%s%s.jpg" %
(dir, image_title, num))
複製程式碼
ok,此時圖片下載完成了,比如上面下載的這張,是這樣的驗證碼:
tesserocr
那我們要識別它,就先試試tesserocr的識別率如何,有關tesserocr的文章,請點選這裡瞭解下,裡面有詳細簡介,這裡不重複說明:
import tesserocr
from PIL import Image
#新建Image物件
image = Image.open("5.jpg")
#進行置灰處理
image = image.convert('L')
#這個是二值化閾值
threshold = 4
table = []
for i in range(256):
if i < threshold:
table.append(0)
else:
table.append(1)
#通過表格轉換成二進位制圖片,1的作用是白色,不然就全部黑色了
image = image.point(table,"1")
image.show()
#呼叫tesserocr的image_to_text()方法,傳入image物件完成識別
result = tesserocr.image_to_text(image)
print(result)
複製程式碼
經過多次除錯處理,發現把二值化閾值調到4,是最優的效果,二值化後的驗證碼長這樣:
而程式碼識別的結果:嘖嘖嘖,這樣定製的二值化都不行,就不再嘗試了,不然每個圖片都這麼定製化去做,還得做?
百度OCR
既然tesserocr效果不好,那就試試百度的OCR吧,(有關百度OCR的文章,請點選這裡檢視):
from aip import AipOcr
from PIL import Image
import os
""" 你的 APPID AK SK """
config = {
"appId": '',
"apiKey":'',
"secretKey":''
}
client = AipOcr(**config)
""" 讀取圖片 """
def get_file_content(filePath):
with open(filePath, 'rb') as fp:
return fp.read()
def get_image_str(image_path):
image = get_file_content(image_path)
""" 呼叫通用文字識別, 圖片引數為本地圖片 """
result = client.basicAccurate(image)
#結果拼接返回輸出s
if 'words_result' in result:
return ''.join([w['words'] for w in result['words_result']])
if __name__ == "__main__":
print(get_image_str("5.jpg"))
複製程式碼
直接執行後的結果:
嘖嘖嘖,果然是BAT,果然是高精準的,能識別呢,那我們繼續試試不同驗證碼,結果發現,下面這張執行後,什麼都識別不出來(看來也不是萬能的);
既然如此,那我們把剛剛tesserocr的二值化處理放到這裡,會不會有效果?程式碼如下:
""" 讀取圖片 """
def get_file_content(filePath):
# 新建Image物件
image = Image.open(filePath)
# 進行置灰處理
image = image.convert('L')
threshold = 15
table = []
for i in range(256):
if i < threshold:
table.append(0)
else:
table.append(1)
# 通過表格轉換成二進位制圖片,1的作用是白色,不然就全部黑色了
image = image.point(table, "1")
image.save(os.path.join(os.getcwd(), os.path.basename(filePath)))
with open(filePath, 'rb') as fp:
return fp.read()
複製程式碼
執行後發現,識別率還是感人,其實之前也測試過,這種驗證碼的確存在部分記錄失敗的情況:
既然百度(免費)的都不行,那我們就換個收費吧,收費的打碼平臺,數超級鷹名氣比較大了;
超級鷹
官網地址:www.chaojiying.com/
開啟官網,有個免費測試,點選後發現要登入,那就先註冊了;
結果發現那個免費測試還是要題分,要關注公眾號繫結賬號才送1000題分,這個就是免費測試,懶得折騰,直接充錢吧;
超級鷹是按量級收費,量大便宜,標準價格:1元=1000題分,不同驗證碼型別,需要的題分不一樣,詳情可以到這裡查詢:www.chaojiying.com/price.html#
充值後,返回到剛剛那個免費測試介面進行測試
等待一會,網頁就會有彈窗:
上傳的驗證碼如下:
對比下,結果完全正確,收費的果然牛逼,這個也是連百度的都搞不定的;
那我們換一種微博的驗證碼:
嘖嘖嘖,無難度啊;
超級鷹也支援接入,首頁底部有個api文件說明,點選進去發現支援各種語言,找到Python,把demo下載下來,api文件連結:www.chaojiying.com/api-14.html
原始碼是基於2.X寫的,不難看懂,但是網上有同學重新整理下,簡潔很多,就把這個程式碼貼出來吧,作者:coder-pig:
from hashlib import md5
import requests
# 超級鷹引數
cjy_params = {
'user': '448975523',
'pass2': md5('你的密碼'.encode('utf8')).hexdigest(),
'softid': '96001',
}
# 超級鷹請求頭
cjy_headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}
# 超級鷹識別驗證碼
def cjy_fetch_code(im, codetype):
cjy_params.update({'codetype': codetype})
files = {'userfile': ('ccc.jpg', im)}
resp = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=cjy_params, files=files,
headers=cjy_headers).json()
# 錯誤處理
if resp.get('err_no', 0) == 0:
return resp.get('pic_str')
# 呼叫程式碼
if __name__ == '__main__':
im = open('captcha.jpg', 'rb').read()
print(cjy_fetch_code(im, 1902))
複製程式碼
執行後,發現驗證碼是對的,同時,一次請求大概是3S左右;
ok,驗證碼破解這塊就到這裡了,想接入超級鷹、百度,tesserocr,又或者是自己寫個人工智慧演算法,任君選擇;
豆瓣自動回覆功能-終
為了效果著想,這裡採用了超級鷹來破解,先看看log:
從log看到,不管是有沒有驗證碼的情況,都是能正常評論的,那去豆瓣看看是不是真成功了?從豆瓣記錄上看,嗯,破解了,good;
上面整合下就是所有的程式碼了;
還可以有的小優化
是不是這樣就完了?
非也,因為現在我們用的是收費的,所以驗證碼的準確率由超級鷹擔保了,但是假如用免費的,識別率感人,從使用者角度,會關心哪些驗證碼失敗,希望有個通知,此時就可以接入server醬,一旦驗證碼驗證失敗就微信通知,效果如下:
感興趣的可以去server醬官網瞭解下:sc.ftqq.com/3.version
18.9.13更新
今天執行指令碼發現,指令碼沒有報錯,但是呢,執行後不會生效(對應帖子沒有相關評論,而且也不會出現驗證碼),jb,一臉懵逼,然後用postman嘗試也不行,後來通過網頁模擬及postman一步步模擬後,發現問題根源是,請求的時候,body的ck欄位的內容是會變化的,之前預設是寫著:
"ck": "TXEg"
複製程式碼
也許有同學好奇什麼時候會變化,經測試發現,當使用者退出登入後,這個欄位就會變化,而對於指令碼來說,肯定不會出去啦,那就轉化成當cookie失效時,這個欄位的內容會發生變化;
而cookie失效的解決方案也是用的,用selenium每次登入獲取cookie,這樣就不會存在cookie失效的情況了,但這裡不介紹這點,後面在寫selenium的時候再介紹;
這裡會簡單介紹下ck的值怎麼獲取的;
在網頁上模擬評論,然後能獲得ck的值,然後複製,去到網頁的HTML搜尋,就會出現這個值是怎麼獲取的了;
沒錯,就是退出按鈕url的最後4個字母,這裡的話,用xpath獲取到這個url,再獲取這4個字母就好了;
於是乎寫了個xpath
selector.xpath("//div/ul/li/div/table/tbody/tr/td/a/@href")
複製程式碼
結果發現,怎麼拿都拿不到,後來response.content的內容輸出看了下,的確沒發現這個退出按鈕的程式碼;
這就意味著,這塊是JS生成的,想獲取JS生成後的HTML,目前只能用selenium,但是這裡還有個問題,selenium想拼接一個自定義的cookie是非常非常的麻煩,之前折騰很久都不行,所以這條路就放棄了;
既然selenium不指望了,那是不是還有其他法子? 所以就拿ck這個key一路去找,結果發現cookie上有這個值:
這裡面有3個值,第一個就是之前程式碼hardcore的內容,對比了下,我們要的值是最後一個ck的value,也做了幾個重新登入的操作,如果cookie最後一個ck值變的話,那這個退出按鈕的ck值也會跟著變,拋開cookie過期的想法,ck這個就直接獲取cookie的最後一個ck內容;
#獲取ck對應的value,通過cookie獲取最後一個ck的值
def get_ck():
# 這個廢了,不能用selenium
# ck_value = selector.xpath("//div/ul/li/div/table/tbody/tr/td/a/@href")
text = re.findall("ck=(.*?);",headers["Cookie"])[-1]
return text
複製程式碼
久違的自動頂貼又回來了
小結
呼,奮鬥到4點,終於擼完了,其實自動回帖功能很簡單,週一下午看到,當天晚上就搞定了,然後在折騰驗證碼的問題,包括想嘗試調優,不用收費的,結果折騰好幾晚讀不行,
再然後就是response的html程式碼問題,一開始以為是js載入的,結果發現是需要decode,跟JS一點關係都沒有,其中還有一晚是整理selenium的知識,因為當時認為跟JS有關係,就搞這玩意,後面會看一本selenium的事,再整一篇selenium介紹文字吧;
回到文章,本文介紹的內容比較多,大概分為3塊如下:
1)如何分析豆瓣評論介面;
2)介紹python定時任務框架APScheduler
3)驗證碼破解(tesserocr、百度OCR、超級鷹)
其實也沒什麼特別好講的,很簡單的東西,只是繁瑣而已,之所以花那麼多時間,是思維被擴散了,就這樣吧,謝謝大家;