實從PHP5開始,PHP就為我們提供了一個強大的解析和生成XML相關操作的類,也就是我們今天要講的 DOMDocument 類。不過我估計大部分人在爬取網頁時還是會喜歡用正則去解析網頁內容,學了今天的這個類下回就可以嘗試下使用這個PHP自帶的方式來進行解析分析了。
// 解析 HTML
$baidu = file_get_contents('https://www.baidu.com');
$doc = new DOMDocument();
@$doc->loadHTML($baidu);
// 百度輸出框
$inputSearch = $doc->getElementById('kw');
var_dump($inputSearch);
// object(DOMElement)#2
// ....
echo $inputSearch->getAttribute('name'), PHP_EOL; // wd
// 獲取所有圖片的鏈接
$allImageLinks = [];
$imgs = $doc->getElementsByTagName('img');
foreach($imgs as $img){
$allImageLinks[] = $img->getAttribute('src');
}
print_r($allImageLinks);
// Array
// (
// [0] => //www.baidu.com/img/baidu_jgylogo3.gif
// [1] => //www.baidu.com/img/bd_logo.png
// [2] => http://s1.bdstatic.com/r/www/cache/static/global/img/gs_237f015b.gif
// )
// 利用 parse_url 分析鏈接
foreach($allImageLinks as $link){
print_r(parse_url($link));
}
// Array
// (
// [host] => www.baidu.com
// [path] => /img/baidu_jgylogo3.gif
// )
// Array
// (
// [host] => www.baidu.com
// [path] => /img/bd_logo.png
// )
// Array
// (
// [scheme] => http
// [host] => s1.bdstatic.com
// [path] => /r/www/cache/static/global/img/gs_237f015b.gif
// )
是不是感覺好清晰,好有面向對象的感覺。就像第一次使用 ORM庫 來進行數據庫操作一樣的感覺。我們一段一段來看。
$baidu = file_get_contents('https://www.baidu.com');
$doc = new DOMDocument();
@$doc->loadHTML($baidu);
首先是加載文檔內容,這個比較好理解,直接使用 loadHTML() 方法加載 HTML 內容。它還提供了其它的幾個方法,分別是:load() 從一個文件加載XML;loadXML() 從字符串加載XML;loadHTMLFile() 從文件加載HTML。
// 百度輸出框
$inputSearch = $doc->getElementById('kw');
var_dump($inputSearch);
// object(DOMElement)#2
// ....
echo $inputSearch->getAttribute('name'), PHP_EOL; // wd
接下來我們使用和前端 JS 一樣的 DOM 操作API來操作HTML里面的元素。這個例子中就是獲取百度的文本框,直接使用 getElementById() 方法獲得id為指定內容的 DOMElement 對象。然后就可以獲取它的值、屬性之類的內容了。
// 獲取所有圖片的鏈接
$allImageLinks = [];
$imgs = $doc->getElementsByTagName('img');
foreach($imgs as $img){
$allImageLinks[] = $img->getAttribute('src');
}
print_r($allImageLinks);
// Array
// (
// [0] => //www.baidu.com/img/baidu_jgylogo3.gif
// [1] => //www.baidu.com/img/bd_logo.png
// [2] => http://s1.bdstatic.com/r/www/cache/static/global/img/gs_237f015b.gif
// )
// 利用 parse_url 分析鏈接
foreach($allImageLinks as $link){
print_r(parse_url($link));
}
// Array
// (
// [host] => www.baidu.com
// [path] => /img/baidu_jgylogo3.gif
// )
// Array
// (
// [host] => www.baidu.com
// [path] => /img/bd_logo.png
// )
// Array
// (
// [scheme] => http
// [host] => s1.bdstatic.com
// [path] => /r/www/cache/static/global/img/gs_237f015b.gif
// )
這一段例子則是獲取HTML文檔中所有的圖片鏈接。相比正則來說,是不是方便很多,而且代碼本身就是自解釋的,不用考慮正則的匹配失效的問題。配合另外一個PHP中自帶的 parse_url() 方法也能非常方便地對鏈接進行分析,提取自己想要的內容。
XML的解析和對HTML的解析也是類似的,都使用 DOMDocument 和 DOMElement 提供的這個方法接口就可以很方便的進行解析了。那么我們想要生成一個標準格式的XML呢?當然也非常的簡單,不需要再去拼接字符串了,使用這個類一樣的進行對象化的操作。
// 生成一個XML文檔
$xml = new DOMDocument('1.0', 'UTF-8');
$node1 = $xml->createElement('First', 'This is First Node.');
$node1->setAttribute('type', '1');
$node2 = $xml->createElement('Second');
$node2->setAttribute('type', '2');
$node2_child = $xml->createElement('Second-Child', 'This is Second Node Child.');
$node2->appendChild($node2_child);
$xml->appendChild($node1);
$xml->appendChild($node2);
print $xml->saveXML();
/*
<?xml version="1.0" encoding="UTF-8"?>
<First type="1">This is First Node.</First>
<Second type="2"><Second-Child>This is Second Node Child.</Second-Child></Second>
*/
其實只要有一點點的前端 JS 的基礎都不難看出這段代碼的含義。使用 createElement() 方法創造 DOMElement 對象,然后就可以為它添加屬性和內容。使用 appendChild() 方法就可以為當前的 DOMElement 或者 DOMDocument 添加下級節點。最后使用 saveXML() 就能夠生成標準的XML格式內容了。
通過上面兩個簡單的小例子,相信大家已經對這個 DOMDocument 操作XML類文件解析的方式非常感興趣了。不過相對于正則解析的方式它們的性能有多大的差異并沒有找到相關的測試,不過一般正常的情況下網站的HMTL文檔都不會太大,畢竟各個網站也會考慮自身的加載速度,如果文檔非常大的話用戶體驗也會很差,所以這套接口用來進行日常爬蟲的分析處理工作基本是沒有任何問題的。
測試代碼: https://github.com/zhangyue0503/dev-blog/blob/master/php/202002/source/PHP%E4%B8%AD%E4%BD%BF%E7%94%A8DOMDocument%E6%9D%A5%E5%A4%84%E7%90%86HTML%E3%80%81XML%E6%96%87%E6%A1%A3.php
參考文檔: https://www.php.net/manual/zh/class.domdocument.php
用AJAX實現的PHP RSS閱讀器示例
示例代碼:
```html
<!DOCTYPE html>
<html>
<head>
<title>AJAX RSS閱讀器</title>
<script>
function loadRSS(url) {
var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
} else {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
document.getElementById("rssFeed").innerHTML = xmlhttp.responseText;
}
}
xmlhttp.open("GET", "read_rss.php?url=" + url, true);
xmlhttp.send();
}
</script>
</head>
<body>
<h2>RSS閱讀器示例</h2>
<form>
<select onchange="loadRSS(this.value)">
<option value="">選擇一個RSS源</option>
<option value="https://example.com/rss1">RSS源1</option>
<option value="https://example.com/rss2">RSS源2</option>
<option value="https://example.com/rss3">RSS源3</option>
</select>
</form>
<div id="rssFeed"></div>
</body>
</html>
```
使用心得:
在開發過程中,我發現使用AJAX實現的PHP RSS閱讀器可以方便地從不同的RSS源中獲取并展示內容。
1. 在示例代碼中,我使用了一個下拉菜單來選擇不同的RSS源。當用戶選擇一個RSS源時,就會調用`loadRSS()`函數,將選中的URL作為參數傳遞給服務器端的讀取腳本。
2. 通過AJAX,可以將選中的URL發送到服務器端,并接收服務器返回的RSS內容。在示例代碼中,我使用了`XMLHttpRequest`對象來實現異步通信,并通過`responseText`屬性獲取服務器返回的RSS內容。
3. 在示例代碼中,我將獲取到的RSS內容展示在頁面上的`rssFeed`元素中。用戶可以通過選擇不同的RSS源,實時獲取并查看對應的內容。
在開發過程中遇到的問題和解決的bug:
在使用AJAX實現的PHP RSS閱讀器的過程中,我曾遇到過一些問題和bug。其中一次遇到的問題是無法正確解析RSS內容。這可能是由于RSS源的格式不正確或解析代碼有誤導致的。解決這個問題的方法是檢查RSS源的格式,并確保解析代碼正確地處理和展示內容。
另外,我還遇到過一些其他的問題,比如RSS源無法訪問、RSS內容顯示不全等。這些問題通??梢酝ㄟ^檢查RSS源鏈接、優化解析代碼等方式來解決。
總結:
使用AJAX實現的PHP RSS閱讀器可以方便地從不同的RSS源中獲取并展示內容。在開發過程中,需要注意RSS源的格式和解析代碼的正確性。在遇到問題時,可以通過檢查RSS源鏈接、優化解析代碼等方式來解決。
(注:以上內容為筆記,非官方文檔)
我是永不低頭的熊,喜歡美食、健身,當然也喜歡寫代碼,每天不定時更新各類編程相關的文章,希望你在碼農這條路上不再孤單!
一章起了個頭,這一章咱們親身做一下這個API的基礎結構。
我們給它叫做“老趙API系統”。
首先,我們要做的這個API系統是私有的,不開源的,不分發給其它人一起用(當然你非要大力推廣,也隨便你)。
其次,我故意遺漏了一個小小的點子,這個點子我自己用,我也是怕我這個辦法泄露后會有安全問題。
就當是拋磚引玉吧。
先說要注意的幾點:
0、不要使用默認首頁
1、不使用SESSION和COOKIE
2、每次訪問都需要驗證用戶名密碼
3、任何單個文件都不允許能正常運行
4、任何單個文件都不允許能單獨實現任何功能
5、未驗證成功身份時不輸出任何錯誤提示
相信很多高人一看這幾條就會知道我要怎么做。也會有一部分人會嗤之以鼻說太初級。
管他呢,反正這是我自己摸索出來的,就是黑客黑我也只能在操作系統方面下手,在我這套系統里他永遠不可能有成功的訪問請求。
下面詳細說:
0、不要使用默認首頁。
如果你的環境里首頁設置了很多,比如index.php,index.html,default.html,default.php之類的,那么你API的接口文件一定不能用這些,比如你可以用iLaoZhao_XX_api.php,這誰猜得到?
1、不使用SESSION和COOKIE。
COOKIE是用明文存在于用戶端的,很容易查看和修改,這個大家都清楚。而PHP的SESSION也依賴于COOKIE來存儲一個SESSION_ID。并且我們的API要每次訪問就返回一次數據,很多訪問方法并不會存儲COOKIE,這會導致SESSION失效。
2、每次訪問都驗證用戶名密碼。
現在流行那個啥叫Token的方式,我懶得研究,倒不如每次都驗證用戶名和密碼,這樣等密碼泄露后只要我改一個密碼,他那邊立馬就不能用了。都不用等他那邊信息過期。當然我們不能明文傳遞這些東西,需要做一定加密處理。
3、任何單個文件都不允許能正常運行。
資源文件夾不能有任何可執行文件。并且盡量多分幾個文件,所有的文件相互依賴,并且把它們互相組織起來的文件又是另一個單獨文件,這樣即便別人知道了我們的文件路徑和文件名,當訪問這個文件時,也無法正常運行。
4、任何單個文件都不允許能單獨實現任何功能。
某個文件要實現功能,必須依賴其它文件內的函數或類,并且當前文件不能知道這些函數和類具體在哪個文件里實現的。這樣即便這個文件的源代碼泄露,對方也不知道里面的依賴關系,不會連累到其它文件。
5、未驗證成功身份時不輸出任何錯誤提示。
在用戶名密碼出錯時,系統不要輸出任何有用的信息,或者輸出一個假的404信息,這樣別人在試探我們的系統時,根本不知道是哪一步出錯了,而我們的客戶端可以根據里面的暗語來判斷狀態。
我做一個小例子來說明一下,這個小例子只是拋磚引玉,并不是我最終的代碼樣子。
文件樹結構:
老趙API文件樹結構
根目錄下只有一個文件夾,整個API系統都通過一個接口文件"DaYeLaiWanA.php"來調用。
怎么樣,不知道文件結構的人根本猜不對你有哪些文件,也就不能訪問任何文件。
下面的代碼沒有使用太先進的語法,比如類和對象啊命名空間啊啥的,新版本PHP添了好多特性,但我比較傾向于不去使用它們,我要盡量的讓代碼對環境沒有要求,在盡可能多的版本環境下都能運行。
畢竟我們要的是安全,而不是讓所有人能讀懂我們的代碼。
我們要盡可能的不使用教科書上的通用的寫法,盡可能搞一套自己的寫法。
下面是DaYeLaiWanA.php的代碼:
<?php
//接口文件,單獨調用這個文件沒任何作用,參數必須得對,差一個字母這個文件都運行不出個毛線來。
//而參數是你自己定的,可隨意加密修改。
//當然你還可以把這個路徑也放到變量里,然后藏起來。
include "./iLaoZhao_funs/postget.php"; //沒有依賴,但也不實現任何具體功能
include "./iLaoZhao_funs/login.php"; //依賴pdo,dbfuns,postget和接口文件
include "./iLaoZhao_funs/curl.php"; //沒有依賴,但也不實現任何具體功能
include "./iLaoZhao_funs/dbfuns.php"; //依賴pdo和接口文件
//上面這些文件里涉及到安全的功能都有依賴,并且他們不知道要用的函數在哪
//全靠這個接口文件把它們組合到一起
//并且上面的函數里也會用到接口文件的函數(就是下面那個)
//這些因素缺一不可,只能由接口文件來整合他們
//上面這些文件和其它的API文件,都無法單獨運行。不是缺這就是缺那。
//下面這倆函數是故意沒單獨放文件里的,為的就是其它文件缺少這個接口文件時在調用這個函數時會出錯。
function output2Die($msg, $code = -1, $data = ''){
$dieMsg['code'] = $code;
$dieMsg['msg'] = $msg;
$dieMsg['data'] = $data;
echo json_encode($dieMsg);
die();
}
function isPhone($phonenumber)
{
return preg_match("/^1[3-9]\d{9}$/", $phonenumber);
}
$area = POST("area");//要使用的API種類
$class = POST("class");//要使用的API小分類
$fun = POST("fun");//要使用的API功能
//清理POST
unset($_POST['area']);
unset($_POST['class']);
unset($_POST['fun']);
if ($class !== FALSE && $fun !== FALSE){
if ($area == false){
$area = "public";
}
//生成API文件全名,這里沒加密,你可以自己變動這里。
$incFile = dirname(__FILE__)."/{$area}/{$class}/{$fun}.php";
if (file_exists($incFile)){ //檢查API文件是否存在,其實不檢查也行。
include "./config/pdodb.php"; //創建數據庫連接,依賴接口文件
include $incFile;//將單獨的API文件導入進來
}else{
//我因為調試的原因,顯示了錯誤信息,要求高的可以去掉。
output2Die("無效的請求", -1);
}
}else{
output2Die("無效的請求", -2);
}
文件我寫了注釋,方便大家看,實際在使用時不能有這些注釋,這些注釋只能方便別人破解我們的系統,并且這個接口文件盡可能進行代碼混淆加密。
然后是一個獲得當前用戶信息的小例子API,這個API文件存在了/iLaoZhao_api/public/login/usrMsg.php里面。在調用時大概URL是這樣:
https://你的域名/iLaoZhao_api/DaYeLaiWanA.php
POST數據為:{
key:"用戶名密碼時間加密字符串運算后的結果",
class:"login",
fun:"usrMsg",
userName:"該用戶的用戶名"
}
API代碼內容usrMsg.php為:
<?php
//示例文件,得到當前用戶的所有信息
//文件依賴:接口文件、數據庫連接文件、dbfuns文件、login文件
//單獨訪問這個文件根本不能運行
//依賴關系完全靠接口文件處理
//這個API讀取post參數中的userName
checkUserNamePassWord(); //檢查用戶名密碼,不對真接就終止運行了,對了的話就可以運行下面的代碼
$dbArr['userName'] = POST("userName");
if($dbArr['userName'] !== false){ //這一步沒必要,能驗證過上面那個函數,這個參數肯定存在,但我就是寫了,就是玩兒
//一定要用PDO的綁定功能訪問數據庫,這種方式能避免SQL注入。
$sql = "select * from tb_users where users_userName = :userName";
$rec = db_query($sql,$dbArr);
if($rec['count'] > 0){
output2Die("成功。",1,$rec);
}else{
output2Die("找不到該用戶。",-1); //要求高的,不要輸出錯誤信息。
}
}
output2Die("失敗",-2); //要求高的,不要輸出錯誤信息。
這里面的checkUserNamePassWord()函數(在iLaoZhao_funs文件夾內的login.php文件里)也比較關鍵:
<?php
/**
* 判斷用戶名密碼的函數
此函數不允許返回東西
調用的時候直接調用
用戶正常自然沒反應
用戶不正常,這個函數直接結束程序運行。
*/
function checkUserNamePassWord(){
$keyStr = "這里是加密字符串,這個串只有你自己知道是什么。";
$key = POST("key"); //這個key是客戶端把用戶名密碼時間加密字符串運算后的結果
//用當前日期當第二密鑰,這個作用是每天的密鑰都不一樣。
//即便被攔截了,也算不出規律來,當然源碼泄露了就不行了。
//不能太精確,因為客戶端與服務器端的時間不可能完全一樣。
$keyStr_Today = date('Ymd',time());
//訪問數據庫,一定要用PDO的這個參數方式。
//這個方式在代入數據與直接寫在SQL語句里不一樣。
//sql注入在這種使用方式下完全不會生效。
$sqlPar['username'] = POST("userName"); //這個沒加密的必要,這個在輸入框里就能看到,而且也會在很多地方顯示。
if($sqlPar['username'] === false || $key === false){
//非法調用
}
//不要在sql里直接比對用戶名密碼
//而且傳過來的數據也沒有密碼
//要取出該用戶的記錄,然后計算后與傳過來的加密身份信息對比
$sql = "select * from tb_users where userName=:username"; //你可以在這limit 1
$rec = db_query($sql, $sqlPar);
if($rec['count'] > 0){
$keyInDB = md5(md5($keyStr).md5($rec['rows'][0]['userName']).md5($keyStr_Today).md5($rec['rows'][0]['passWord']));
//上面這句是加密方式,把加密字符串、用戶名、當前日期、密碼的MD5連接起來并再計算一次MD5
//當然你還可以再加點別的東西,比如字符串反轉,以及其它變量之類的。
//上面這句計算出來的和$key中的一樣時,說明密碼是對的。
if($key == $keyInDB){
//用戶名密碼對
//此時我們啥也不做
//或者你想設置某些變量也行
}else{
//否則
die(); //結束程序運行,
}
}
}
代碼里都有注釋,應該能看清楚。而且還有其它的自定義函數我就不放代碼了。
這樣我們在擴充API時只需要在對應路徑下寫PHP代碼文件就行,而且很多函數可以直接使用,不用include任何文件。
而且這個系統內所有的文件在單獨訪問時都會出錯(可以設置PHP不顯示錯誤信息),而接口文件在參數不正確的情況下也不會有什么具體運行結果。
也沒有默認的首頁文檔可以訪問。
文件路徑和文件名都沒規律,在網上都查不到參考。
那么想黑掉這套API只有兩個辦法:
0、黑掉服務器操作系統,再從文件系統入手。
1、破解客戶端代碼或者攔截訪問數據,找到訪問規律。
這兩點暫時無能為力解決。
**邊寫文檔邊寫代碼,頭有點亂,可能有遺漏的東西,不知道大家能不能看懂。
**此API系統還有很多地方可以進一步加密處理,但我個人感覺沒必要了。已經夠可以了。
**此代碼沒有使用太先進的語法,比如類啊命名空間啊啥的,新版本PHP添了好多特性,但我比較傾向于不去使用它們,我要盡量的讓代碼對環境沒有要求,在盡可能多的版本環境下都能運行。
畢竟我們要的是安全,而不是讓所有人能讀懂我們的代碼。
有啥忘了的以后再補充吧。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。